搜尋演算法合集 - By DijkstraPhoenix

DijkstraPhoenix發表於2024-10-06

搜尋演算法合集

By DijkstraPhoenix

深度優先搜尋 (DFS)

引入

如果現在有一個迷宮,如何走路徑最短?

方法

走迷宮最簡單粗暴的方法式什麼呢?當然是把所有路都走一遍啦!

如果是手動計算的話,可能會把你手指累得抽筋,但電腦不會,電腦具有強大的算力,這種暴力的事情當然是交給電腦做啦。

深搜的本質:一條路走到底,走到死衚衕再往回走,回到上一個岔口繼續走,直到找到正確的路

實際上,任何一條路都可以看做是一個只有一個岔口的分岔路,所以不需要把路和岔口分開計算。

那麼剛才的例子應該是這麼走(數字代表第幾次嘗試)實際上岔口走的順序是任意的,方法不唯一。

概念:從死衚衕返回的步驟叫做回溯

由於深搜不能保證第一次找到的路徑為最短路徑,所以需要統計所有路線

深搜一般使用遞迴實現,走過的每個位置都要打上標記,同一條路不能再走一遍

四聯通和八連通:

有些走迷宮題目是四聯通的(即上下左右),也有些是八連通的(即上、下、左、右、左上、左下、右上、右下),需要仔細觀察題目要求

四聯通位移陣列:dx[]={1,0,-1,0}; dy[]={0,1,0,-1};

八連通位移陣列:dx[]={1,0,-1,0,1,1,-1,-1}; dy[]={0,1,0,-1,1,-1,1,-1};

主演算法程式碼:

int maze[MAXN][MAXN];//儲存迷宮 0表示當前節點可以走,1表示不能走
bool vis[MAXN][MAXN];//打標記
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};//位移陣列,分別對應 上右下左(如果是八向移動的話要改成對應的)
int n,m,stx,sty,edx,edy;//地圖長寬以及起點和終點的座標
int ans=0x7f7f7f7f;//最短距離,要初始化為極大值

void dfs(int x,int y,int z)//x和y是當前位置的座標,z是走過的步數
{
    if(x==edx&&y==edy)//到了終點
    {
        ans=min(ans,z);//更新答案(如果答案還是極大值,說明無法到達終點)
        return;
    }
    vis[x][y]=true;//打標記
    for(int i=0;i<4;i++)//列舉四個方向
    {
        int nx=x+dx[i],ny=y+dy[i];//下一個應該走到的位置
        if(nx<1||nx>n||ny<1||ny>m)continue;//不能走出地圖(這個要寫在靈魂拷問的最前面,否則訪問陣列要越界)
        if(maze[nx][ny]==1)continue;//不能卡牆裡
        if(vis[nx][ny])continue;//不能走你走過的路
        dfs(nx,ny,z+1);//走到下一個節點
    }
    vis[x][y]=false;//重點!回溯時要清除標記!
}

例題

連通塊問題

求1的連通塊數量

這是一類重要的題目!

本題可以使用 DFS 從每一個點開始將每一個連通塊都標記並計數

上圖就是標記出的連通塊

我們可以從連通塊的任意一個位置開始,遍歷整個連通塊,並把這個連通塊的所有點打上標記(防止重複計算)

這個方法也叫洪水填充法(Flood Fill)

強烈建議在網上找幾篇專門講連通塊的部落格學一下

#include<bits/stdc++.h>
using namespace std;
int maze[105][105];
bool vis[105][105];
int n,m,ans;
void dfs(int x,int y)
{
    vis[x][y]=true;
    for(int i=0;i<4;i++)
    {
        int nx=x+dx[i],ny=y+dy[i];
        if(nx<1||nx>n||ny<1||ny>m)continue;
        if(maze[nx][ny]!=1)continue;
        if(vis[nx][ny])continue;
        dfs(nx,ny);
    }
    //注意!!!此處不要回溯(標記是給後面的連通塊看的)
}
int main(void)
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
            cin>>maze[i];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=m;j++)
        {
            if(!vis[i][j]&&maze[i][j]==1)//如果這個點沒找過並且是連通塊的一部分,那麼就是一個新的連通塊
            {
                ans++;
                dfs(i,j);//為整個連通塊打上標記
            }
        }
    }
    cout<<ans;
    return 0;
}

八皇后問題

洛谷 P1219

本題的每一步都決定一個皇后的位置,由輸出格式就可以看出,我們可以按每一列的順序計算。一個皇后會獨佔一行、一列、兩斜線,因為是按列計算的,不需要給列打標記,則需要 3 個標記陣列。

(其實可以看一下洛谷上的題解)

#include<bits/stdc++.h>
using namespace std;
bool vis[15],vis1[35],vis2[35];
int n;
int nod[15];
int sum=0;
void dfs(int k)
{
    if(k>n)
    {
        sum++;
        if(sum<=3)//前3個要輸出方案
        {
            for(int i=1;i<=n;i++)cout<<nod[i]<<" ";
            cout<<endl;
        }
        return;
    }
    for(int i=1;i<=n;i++)
    {
        if(vis[i])continue;
        if(vis1[i+k-1])continue;
        if(vis2[i-k+13])continue;
        vis[i]=true;
        vis1[i+k-1]=true;
        vis2[i-k+13]=true;//可以手動模擬一下行列座標和斜座標的關係,加13是防止計算出負數
        nod[k]=i;//儲存方案
        dfs(k+1);
        vis[i]=false;
        vis1[i+k-1]=false;
        vis2[i-k+13]=false;
    }
}
int main(void)
{
    cin>>n;
    dfs(1);
    cout<<sum;
    return 0;
}

全排列問題

這是一個重要的題目!

洛谷 P1706

按照題意模擬搜尋即可

#include<iostream>
#include<cstdio>
using namespace std;
int n,a[1000],vis[1000];
void dfs(int step)
{
    if(step==n+1)
    {
        for(int i=1;i<=n;i++)
        {
            printf("%5d",a[i]);//題目要求格式化輸出
        }
        cout<<endl;
    }
    for(int i=1;i<=n;i++)
    {
        if(vis[i]==1)continue;
        a[step]=i;
        vis[i]=1;
        dfs(step+1);
        vis[i]=0;
    }
}
int main(void)
{
    cin>>n;
    dfs(1);
    return 0;
}

一些建議練習的題

求細胞數量
提示:聯通塊問題,不要清除標記,從每個未標記且是細胞的塊出發,將整個塊打上標記

小貓爬山

選數

單詞接龍

廣度優先搜尋 (BFS)

引入

還是剛才的迷宮問題

方法

除了把每一條路走一遍,其實還可以用水把迷宮給淹了。這樣可以順著水找到路徑。

BFS 模擬洪水往外擴散的樣子,按層遍歷。

這樣做有一個好處:如果像迷宮這樣,每兩個相鄰的方塊之間的距離都一樣的話,第一次找到的路徑就是最短路徑(每一層走過的路程都一樣),如果沒有這個條件就不保證最短

以下是 BFS 和 DFS 策略上的對比:

放到迷宮裡就是這個樣子:

(圖中深綠色代表路徑,淺綠色代表訪問過的點,線表示遍歷的每一層)

重點!因為同一層的點無法直接像 DFS 那樣轉移,所以會把應該訪問的節點放到一個佇列裡,挨個處理

主要程式碼:

struct node
{
    int x,y,step;//節點座標和已經走過的步數
};
queue<node>Q; //佇列
bool vis[MAXN][MAXN];//標記陣列
int maze[MAXN][MAXN];//儲存地圖,0可以走,1不能走
int stx,sty,edx,edy;
const int dx[]={1,0,-1,0};
const int dy[]={0,1,0,-1};
int n,m;//地圖長寬

void bfs(void)//bfs其實不需要封裝在函式里,它不需要遞迴
{
    Q.push(node{stx,sty,0});
    vis[stx][sty]=true;//初始狀態
    while(!Q.empty())//直到所有的狀態都處理完畢
    {
        int x=Q.front().x,y=Q.front().y,st=Q.front().step;//取出隊首元素
        Q.pop();//很重要!不要忘了出佇列
        for(int i=0;i<4;i++)
        {
            int nx=x+dx[i],ny=y+dy[i];
            if(nx<1||nx>n||ny<1||ny>m)continue;
            if(vis[nx][ny])continue;
            if(maze[nx][ny]==1)continue;//靈魂三問
            Q.push(node{nx,ny,st+1});//加入處理佇列
            vis[nx][ny]=true;//打標記  注意!BFS沒有回溯!
            if(nx==edx&&ny==edy)//到終點了
            {
                cout<<st+1;
                return 0;
            }
        }
    }
}

例題

Catch That Cow S

洛谷 P1588

本題的轉移有三種:前進1步,後退1步,乘2

其實本題程式碼很簡單(別看有點長)實際上是三個轉移

#include<bits/stdc++.h>
using namespace std;
int t,x,y;
bool vis[100005];
queue<int>Q,step;
int main(void)
{
    cin>>t;
    for(int test(1);test<=t;test++)
    {
        cin>>x>>y;
        Q.push(x); //入隊
        step.push(0); //初始他一步也沒走,step入隊0
        vis[x]=true; //標記
        while(!Q.empty()) //step和Q是同步的,不需要額外判斷
        {
            //取隊首元素
            int s=Q.front();
            int st=step.front();
            Q.pop(); //出隊
            step.pop();
            int nst=st+1;
            int ns;
            //找可以的情況
            ns=s+1;//前進1步
            if(ns>=1&&ns<=100000&&vis[ns]!=true) //條件成立
            {
                vis[ns]=true; //標記
                Q.push(ns); //入隊
                step.push(nst);
                if(ns==y) //找到了
                {
                    //因為BFS第一個找到的一定是最短的路徑,直接輸出
                    cout<<nst;
                    break;
                }
            }
            ns=s-1;//後退1步
            if(ns>=1&&ns<=100000&&vis[ns]!=true) //條件成立
            {
                vis[ns]=true; //標記
                Q.push(ns); //入隊
                step.push(nst);
                if(ns==y) //找到了
                {
                    //因為BFS第一個找到的一定是最短的路徑,直接輸出
                    cout<<nst;
                    break;
                }
            }
            ns=s*2;//乘2
            if(ns>=1&&ns<=100000&&vis[ns]!=true) //條件成立
            {
                vis[ns]=true; //標記
                Q.push(ns); //入隊
                step.push(nst);
                if(ns==y) //找到了
                {
                    //因為BFS第一個找到的一定是最短的路徑,直接輸出
                    cout<<nst;
                    break;
                }
            }
        }
    }
}

字串變換

洛谷 P1032

本題需要尋找可以變換的部分進行轉移。字串長度不長,可以暴力配對

#include<bits/stdc++.h>
using namespace std;
#define int long long

string ap[25],bp[25];
int le;
queue<string>q;
queue<int>step;//本程式碼沒有使用結構體而是使用兩個佇列
map<string,bool>vis;

signed main(void)
{
    string a,b;
    string ia,ib;
    cin>>a>>b;
    while(cin>>ia)
    {
        cin>>ib;
        ap[++le]=ia;
        bp[le]=ib;
    }
    q.push(a);
    step.push(0);
    vis[a]=true;
    while(!q.empty())
    {
        string s=q.front();q.pop();
        int st=step.front();step.pop();
        if(st==10)continue;
        for(int i=1;i<=le;i++)
        {
            int start=0;
            while(true)
            {
                int fd=s.find(ap[i],start);//尋找匹配字串
                if(fd==string::npos)break;
                start=fd+1;
                string tmp=s.substr(0,fd);
                tmp+=bp[i];
                tmp+=s.substr(fd+ap[i].length());
                if(vis[tmp])continue;
                q.push(tmp);//轉移
                step.push(st+1);
                if(tmp==b)//找到了
                {
                    cout<<st+1;
                    return 0;
                }
            }
        }
    }
    cout<<"NO ANSWER!";
    return 0;
} 

一些建議的題

奇怪的電梯

填塗顏色

棋盤

最後的迷宮

--還沒寫完呢--

相關文章