廣度優先搜索
廣度優先搜索的過程
廣度優先搜索算法(又稱寬度優先搜索)是最簡便的圖的搜索算法之一,這一算法也是很多重要的圖的算法的原型。
Dijkstra單源最短路徑算法和Prim最小生成樹算法都采用了和寬度優先搜索類似的思想。
廣度優先算法的核心思想是:從初始節點開始,應用算符生成第一層節點,檢查目標節點是否在這些后繼節點中,若沒有,再用產生式規則將所有第一層的節點逐一擴展,得到第二層節點,並逐一檢查第二層節點中是否包含目標節點。若沒有,再用算符逐一擴展第二層的所有節點……,如此依次擴展,檢查下去,直到發現目標節點為止。即
- 從圖中的某一頂點V0開始,先訪問V0;
- 訪問所有與V0相鄰接的頂點V1,V2,......,Vt;
- 依次訪問與V1,V2,......,Vt相鄰接的所有未曾訪問過的頂點;
- 循此以往,直至所有的頂點都被訪問過為止。
這種搜索的次序體現沿層次向橫向擴展的趨勢,所以稱之為廣度優先搜索。
廣度優先搜索算法描述:
int Bfs()
{
初始化,初始狀態存入隊列;
隊列首指針head=0; 尾指針tail=1;
do
{
指針head后移一位,指向待擴展結點;
for (int i=1;i<=max;++i) //max為產生子結點的規則數
{
if (子結點符合條件)
{
tail指針增1,把新結點存入列尾;
if (新結點與原已產生結點重復) 刪去該結點(取消入隊,tail減1);
else
if (新結點是目標結點) 輸出並退出;
}
}
}while(head<tail); //隊列為空
}
廣度優先搜索注意事項:
1、每生成一個子結點,就要提供指向它們父親結點的指針。當解出現時候,通過逆向跟蹤,找到從根結點到目標結點的一條路徑。當然不要求輸出路徑,就沒必要記父親。
2、生成的結點要與前面所有已經產生結點比較,以免出現重復結點,浪費時間和空間,還有可能陷入死循環。
3、如果目標結點的深度與“費用”(如:路徑長度)成正比,那么,找到的第一個解即為最優解,這時,搜索速度比深度搜索要快些,在求最優解時往往采用廣度優先搜索;如果結點的“費用”不與深度成正比時,第一次找到的解不一定是最優解。
4、廣度優先搜索的效率還有賴於目標結點所在位置情況,如果目標結點深度處於較深層時,需搜索的結點數基本上以指數增長。
例題講解
【例1】圖4表示的是從城市A到城市H的交通圖。從圖中可以看出,從城市A到城市H要經過若干個城市。現要找出一條經過城市最少的一條路線。
【算法分析】
看到這圖很容易想到用鄰接距陣來表示,0表示能走,1表示不能走。如圖。
首先想到的是用隊列的思想。a數組是存儲擴展結點的隊列,a[i]記錄經過的城市,b[i]記錄前趨城市,這樣就可以倒推出最短線路。具體過程如下:
-
將城市A入隊,隊首為0、隊尾為1。
-
將隊首所指的城市所有可直通的城市入隊(如果這個城市在隊列中出現過就不入隊,可用一布爾數組s[i]來判斷),將入隊城市的前趨城市保存在b[i]中。然后將隊首加1,得到新的隊首城市。重復以上步驟,直到搜到城市H時,搜索結束。利用b[i]可倒推出最少城市線路。
#include<iostream>
#include<cstring>
using namespace std;
int ju[9][9]={{0,0,0,0,0,0,0,0,0},
{0,1,0,0,0,1,0,1,1},
{0,0,1,1,1,1,0,1,1},
{0,0,1,1,0,0,1,1,1},
{0,0,1,0,1,1,1,0,1},
{0,1,1,0,1,1,1,0,0},
{0,0,0,1,1,1,1,1,0},
{0,1,1,1,0,0,1,1,0},
{0,1,1,1,1,0,0,0,1}};
int a[101],b[101];
bool s[9]; //初始化
int out(int d) //輸出過程
{
cout<<char(a[d]+64);
while (b[d])
{
d=b[d];
cout<<"--"<<char(a[d]+64);
}
cout<<endl;
}
void doit()
{
int head,tail,i;
head=0;tail=1; //隊首為0、隊尾為1
a[1]=1; //記錄經過的城市
b[1]=0; //記錄前趨城市
s[1]=1; //表示該城市已經到過
do //步驟2
{
head++; //隊首加一,出隊
for (i=1;i<=8;i++) //搜索可直通的城市
if ((ju[a[head]][i]==0)&&(s[i]==0)) //判斷城市是否走過
{
tail++; //隊尾加一,入隊
a[tail]=i;
b[tail]=head;
s[i]=1;
if (i==8)
{
out(tail);head=tail;break; //第一次搜到H城市時路線最短
}
}
}while (head<tail);
}
int main() //主程序
{
memset(s,false,sizeof(s));
doit(); //進行Bfs操作
return 0;
}
【例2】一矩形陣列由數字0到9組成,數字1到9代表細胞,細胞的定義為沿細胞數字上下左右還是細胞數字則為同一細胞,求給定矩形陣列的細胞個數。如:
陣列
4 10
0234500067
1034560500
2045600671
0000000089
有4個細胞。
【算法分析】
-
從文件中讀入m*n矩陣陣列,將其轉換為boolean矩陣存入bz數組中;
-
沿bz數組矩陣從上到下,從左到右,找到遇到的第一個細胞;
-
將細胞的位置入隊h,並沿其上、下、左、右四個方向上的細胞位置入隊,入隊后的位置bz數組置為flase;
-
將h隊的隊頭出隊,沿其上、下、左、右四個方向上的細胞位置入隊,入隊后的位置bz數組置為flase;
-
重復4,直至h隊空為止,則此時找出了一個細胞;
-
重復2,直至矩陣找不到細胞;
-
輸出找到的細胞數。
#include<cstdio>
using namespace std;
int dx[4]={-1,0,1,0},
dy[4]={0,1,0,-1};
int bz[100][100],num=0,n,m;
void doit(int p,int q)
{
int x,y,t,w,i;
int h[1000][10];
num++;bz[p][q]=0;
t=0;w=1;h[1][1]=p;h[1][2]=q; //遇到的第一個細胞入隊
do
{
t++; //隊頭指針加1
for (i=0;i<=3;i++) //沿細胞的上下左右四個方向搜索細胞
{
x=h[t][1]+dx[i];y=h[t][2]+dy[i];
if ((x>=0)&&(x<m)&&(y>=0)&&(y<n)&&(bz[x][y])) //判斷該點是否可以入隊
{
w++;
h[w][1]=x;
h[w][2]=y;
bz[x][y]=0;
} //本方向搜索到細胞就入隊
}
}while (t<w); //直至隊空為止
}
int main()
{
int i,j;
char s[100],ch;
scanf("%d%d\n",&m,&n);
for (i=0; i<=m-1;i++ )
for (j=0;j<=n-1;j++ )
bz[i][j]=1; //初始化
for (i=0;i<=m-1;i++)
{
gets(s);
for (j=0;j<=n-1;j++)
if (s[j]=='0') bz[i][j]=0;
}
for (i=0;i<=m-1;i++)
for (j=0;j<=n-1;j++)
if (bz[i][j])
doit(i,j); //在矩陣中尋找細胞
printf("NUMBER of cells=%d",num);
return 0;
}
【例3】最短路徑(1995年高中組第4 題)
如下圖所示,從入口(1)到出口(17)的可行路線圖中,數字標號表示關卡。
現將上面的路線圖,按記錄結構存儲如下圖6:
請設計一種能從存儲數據中求出從入口到出口經過最少關卡路徑的算法。
【算法分析】
該題是一個路徑搜索問題,根據圖示,從入口(1)到出口(17)可能有多條途徑,其中最短的路徑只有一條,那么如何找最短路徑呢?
根據題意,用數組NO存儲各關卡號,用數組PRE存儲訪問到某關卡號的前趨關卡號。其實本題是一個典型的圖的遍歷問題,我們可以采用圖的廣度優先遍歷,並利用隊列的方式存儲頂點之間的聯系。
從入口(1)開始先把它入隊,然后把(1)的所有關聯頂點都入隊,即訪問一個頂點,將其后繼頂點入隊,並存儲它的前趨頂點,……,直到訪問到出口(17)。
最后,再從出口的關卡號(17)開始回訪它的前趨關卡號,……,直到入口的關卡號(1),則回訪的搜索路徑便是最短路徑。從列表中可以看出出口關卡號(17)的被訪問路徑最短的是:
(17)← (16)←(19)←(18)←(1)
由此,我們得到廣度優先遍歷求最短路徑的基本方法如下:
假設用鄰接矩陣存放路線圖(a[I][j]=1表示I與j連通,a[I][j]=0表示I與j不連通)。 再設一個隊列和一個表示拓展到哪個頂點的指針變量pos。
(1)從入口開始,先把(1)入隊,並且根據鄰接矩陣,把(1)的后繼頂點全部入隊,並存儲這些后繼頂點的前趨頂點為(1);再把pos后移一個,繼續拓展它,將其后繼頂點入隊,並存儲它們的前趨頂點,……,直到拓展到出口(目的地(17));
注意后繼頂點入隊前,必須要檢查這個頂點是否已在隊列中,如果已經在了就不要入隊了;這一步可稱為圖的遍歷或拓展;
(2)從隊列的最后一個關卡號(出口(17))開始,依次回訪它的前驅頂點,倒推所得到的路徑即為最短路徑。主要是依據每個頂點的前趨頂點倒推得到的。實現如下:
i=1 ;
WHILE (NO[I]!=17) ++i ;
DO
{
cout<<"("<<no[i]<<")";
cout<<" ←";
i=pre[i] ;
}
while ( I!=0);
【例4】迷宮問題
如下圖所示,給出一個N*M的迷宮圖和一個入口、一個出口。
編一個程序,打印一條從迷宮入口到出口的路徑。這里黑色方塊的單元表示走不通(用-1表示),白色方塊的單元表示可以走(用0表示)。只能往上、下、左、右四個方向走。如果無路則輸出“no way.”。
【算法分析】
只要輸出一條路徑即可,所以是一個經典的回溯算法問題,本例給出了回溯(深搜)程序和廣搜程序。實現見參考程序。
【深搜參考程序】
#include <iostream>
using namespace std;
int n,m,desx,desy,soux,souy,totstep,a[51],b[51],map[51][51];
bool f;
int move(int x, int y,int step)
{
map[x][y]=step; //走一步,作標記,把步數記下來
a[step]=x; b[step]=y; //記路徑
if ((x==desx)&&(y==desy))
{
f=1;
totstep=step;
}
else
{
if ((y!=m)&&(map[x][y+1]==0)) move(x,y+1,step+1); //向右
if ((!f)&&(x!=n)&&(map[x+1][y]==0)) move(x+1,y,step+1); //往下
if ((!f)&&(y!=1)&&(map[x][y-1]==0)) move(x,y-1,step+1); //往左
if ((!f)&&(x!=1)&&(map[x-1][y]==0)) move(x-1,y,step+1); //往上
}
}
int main()
{
int i,j;
cin>>n>>m; //n行m列的迷宮
for (i=1;i<=n;i++) //讀入迷宮,0表示通,-1表示不通
for (j=1;j<=m;j++)
cin>>map[i][j];
cout<<"input the enter:";
cin>>soux>>souy; //入口
cout<<"input the exit:";
cin>>desx>>desy; //出口
f=0; //f=0表示無解;f=1表示找到了一個解
move(soux,souy,1);
if (f)
{
for (i=1;i<=totstep;i++) //輸出直迷宮的路徑
cout<<a[i]<<","<<b[i]<<endl;
}
else cout<<"no way."<<endl;
return 0;
}
【廣搜參考程序】
#include <iostream>
using namespace std;
int u[5]={0,0,1,0,-1},
w[5]={0,1,0,-1,0};
int n,m,i,j,desx,desy,soux,souy,head,tail,x,y,a[51],b[51],pre[51],map[51][51];
bool f;
int print(int d)
{
if (pre[d]!=0) print (pre[d]); //遞歸輸出路徑
cout<<a[d]<<","<<b[d]<<endl;
}
int main()
{
int i,j;
cin>>n>>m; //n行m列的迷宮
for (i=1;i<=n;i++) //讀入迷宮,0表示通,-1表示不通
for (j=1;j<=m;j++)
cin>>map[i][j];
cout<<"input the enter:";
cin>>soux>>souy; //入口
cout<<"input the exit:";
cin>>desx>>desy; //出口
head=0;
tail=1;
f=0;
map[soux][souy]=-1;
a[tail]=soux; b[tail]=souy; pre[tail]=0;
while (head!=tail) //隊列不為空
{
head++;
for (i=1;i<=4;i++) //4個方向
{
x=a[head]+u[i]; y=b[head]+w[i];
if ((x>0)&&(x<=n)&&(y>0)&&(y<=m)&&(map[x][y]==0))
{ //本方向上可以走
tail++;
a[tail]=x; b[tail]=y; pre[tail]=head;
map[x][y]=-1;
if ((x==desx)&&(y==desy)) //擴展出的結點為目標結點
{
f=1;
print(tail);
break;
}
}
}
if (f) break;
}
if (!f) cout<<"no way."<<endl;
return 0;
}