程式設計訓練(三)
關於深度優先演算法(dfs)和廣度優先演算法(bfs)的一點東西。
文章目錄
深度優先遍歷
深度優先,本質應該是圖的遍歷,從某個頂點出發,訪問圖中的每一個頂點,而深度優先就意味著它是優先選擇更深層次的頂點,用樹來看得的話,那就是最快到達某個葉子結點。 一般步驟:
1)選取圖中某一頂點Vi為出發點,訪問並標記該頂點; 2)以Vi為當前頂點,依次搜尋Vi的每個鄰接點Vj,若Vj未被訪問過,則訪問和標記鄰接點Vj,若Vj已被訪問過,則搜尋Vi的下一個鄰接點; 3)以Vj為當前頂點,重複步驟2),直到圖中和Vi有路徑相通的頂點都被訪問為止; 4)若圖中尚有頂點未被訪問過(非連通的情況下),則可任取圖中的一個未被訪問的頂點作為出發點,重複上述過程,直至圖中所有頂點都被訪問。
通俗來講,深度優先就像是地下尋寶,好東西都在地下深處,我們要一直往下挖,差不多就留個標記,不能往下挖了,我們就往前後左右挖,然後再試著往下挖,都不行的話,我們就往上爬回去一點,再往前後左右挖,直到去過所有能去的地方,回到地面上。
經典例子
我們來看一個圖
我們按照深度優先來排列這個無向圖的拓撲序列,其中字母按ABCDEFG順序儲存。從標記頂點A並從它出發,訪問A的鄰接點C、D、F,在同深度的情況下,按照儲存順序優先選擇C並標記;然後繼續訪問C的鄰接點B、D,這裡優先選B並標記;在B無鄰接點的情況下回退一個節點,接著訪問C的另一個鄰接點D並標記;一直回退到A,因為D已經被標記,所以選擇A的最後一個鄰接點F,按照之前的步驟訪問,最後得到的序列為:A -> C -> B -> D -> F -> G -> E
實際應用
單詞接龍
單詞接龍是一個與我們經常玩的成語接龍相類似的遊戲,現在我們已知一組單詞,且給定一個開頭的字母,要求出以這個字母開頭的最長的“龍”(每個單詞都最多在“龍”中出現兩次),在兩個單詞相連時,其重合部分合為一部分,例如 beast和astonish,如果接成一條龍則變為beastonish,另外相鄰的兩部分不能存在包含關係,例如at 和 atide 間不能相連。
輸入輸出格式
輸入格式: 輸入的第一行為一個單獨的整數n (n≤20)表示單詞數,以下n每行有一個單詞,輸入的最後一行為一個單個字元,表示“龍”開頭的字母。你可以假定以此字母開頭的“龍”一定存在. 輸出格式: 只需輸出以此字母開頭的最長的“龍”的長度
看程式碼
#include<iostream>
#include<string>
#define maxSize 30
using namespace std;
int n,maxLength;
int repeat[maxSize][maxSize],visit[maxSize];
string words[maxSize];
//預處理,把重複長度存到repeat
void pre(string a,string b,int x,int y){
int length_a = a.size()-1,length_b = b.size()-1;
for(int i = 0;i <= length_b;i++){ //列舉
if(a[0] == b[i]){
int pos=0,re=0; //pos是當前a的第幾個字母,re是a和b的重合部分長度
for(int j = i;j <= length_b;j++){
if(a[pos] == b[j]){
re++;
pos++;
if(j == length_b && re != min(length_a,length_b)+1){ //不能包含,找重合最短的
repeat[x][y]=re;
}
}
else{
break;
}
}
}
}
}
void dfs(int pos,int sum){
maxLength=max(maxLength,sum);
for(int i = 1;i <= n;i++){
if(repeat[i][pos] && visit[i]){
visit[i]--;
int length=sum+words[i].size()-repeat[i][pos]; //新的長度
dfs(i,length); //接上新單詞繼續搜尋
visit[i]++;
}
}
}
int main(){
char ch;
int i,j;
cin>>n;
for(int i = 1;i <= n;i++){
cin>>words[i];
visit[i] = 2; //每個單詞能用兩次
}
cin>>ch;
for(i = 1;i <= n;i++){
for(j = 1;j <= n;j++){
pre(words[i],words[j],i,j); //預處理repeat陣列
}
}
for(i = 1;i <= n;i++){
if(words[i][0] == ch){ //找到開始部分
visit[i]--;
dfs(i,words[i].size()); //深搜
visit[i]++;
}
}
cout<<maxLength<<endl;
system("pause");
return 0;
}
廣度優先遍歷
廣度優先,也可以叫寬度優先,更像是樹的層序遍歷,是一層一層的去訪問結點的,只有在訪問完一層以後,再去訪問下一層結點,一直到訪問葉子結點所在的層。 一般步驟:
1、從圖中某個頂點V0出發,並訪問此頂點; 2、從V0出發,訪問V0的各個未曾訪問的鄰接點W1,W2,…,Wk;然後,依次從W1,W2,…,Wk出發訪問各自未被訪問的鄰接點; 3、重複步驟2,直到全部頂點都被訪問為止。
舉個通俗的例子,廣度優先遍歷就是魔塔遊戲,你要先把這一層裡的每一個位置的怪物都打死(相當於留下標記),然後才能拿到鑰匙去到下一層。
經典例子
再看剛剛那個圖 我們可以先給這個圖簡單的分一下層:
- 第一層:A
- 第二層:C、D、F
- 第三層:B、G
- 第四層:E
然後讓我們來遍歷它。
先訪問根結點A並標記,然後按字典順序依次訪問第二層的C、D、F,並標記;接著訪問第三層的B、G,標記;最後一層的E,標記;最後的拓撲序列就是:A -> C -> D -> F -> B -> G -> E
。
實際應用
01迷宮
有一個僅由數字0與1組成的n×n格迷宮。若你位於一格0上,那麼你可以移動到相鄰4格中的某一格1上,同樣若你位於一格1上,那麼你可以移動到相鄰4格中的某一格0上。 你的任務是:對於給定的迷宮,詢問從某一格開始能移動到多少個格子(包含自身)。
輸入輸出格式
輸入格式: 第1行為兩個正整數n,m; 下面n行,每行n個字元,字元只可能是0或者1,字元之間沒有空格; 接下來m行,每行2個用空格分隔的正整數i,j對應了迷宮中第i行第j列的一個格子,詢問從這一格開始能移動到多少格。 輸出格式: m行,對應每個詢問輸出相應答案。
來看程式碼
#include<iostream>
#include<cstring>
#define maxSize 1000001
#define size 1001
using namespace std;
int father[maxSize],child[maxSize]; //fa為根,ch為子節點
int n;
int u[4] = {1,-1,0,0};
int v[4] = {0,0,1,-1}; //四個方向
char s[size][size]; //存迷宮
//查
int find(int x){
return father[x] == x ? x : father[x] = find(father[x]);
}
//並
void union_(int x,int y){
int r1 = find(x),r2 = find(y);
if(r1 != r2){
child[r1] += child[r2],father[r2] = father[r1];
}
}
//構造點到節點一一對映:(i,j)=i*n+j;
int dfs(int x,int y){
if(father[x*n+y] != -1){
return find(x*n+y);
}
father[x*n+y] = x*n+y,child[x*n+y] = 1; //構造對映
for(int k = 0;k < 4;k++){
int dx = x+u[k],dy = y+v[k];
if(dx >= 0 && dx < n && dy >= 0 && dy < n && s[x][y] != s[dx][dy]){
union_(x*n+y,dfs(dx,dy));
}
}
return find(x*n+y);
}
int main(){
int m;
cin>>n>>m;
memset(father,-1,4*n*n); //初始化集合,負數為根
for(int i = 0;i < n;i++)
cin>>s[i];
int i[size],j[size];
for(int k = 0;k < m;k++){
cin>>i[k]>>j[k]; //起始位置
}
for(int k = 0;k < m;k++){
cout<<child[dfs(i[k]-1,j[k]-1)]<<endl;
}
system("pause");
return 0;
}
以上!