1. 程式人生 > >廣度優先搜尋演算法詳解

廣度優先搜尋演算法詳解

說到廣度優先搜尋,大家可能先想到的是廣度優先遍歷,其實廣度優先搜尋就是利用了廣度優先遍歷的一種搜尋演算法。我個人總結的該演算法包含以下幾個關鍵點,掌握了這幾個點,該演算法也就掌握的很好了。下面也基本上是圍繞這幾個關鍵點展開的。

1.狀態

2.狀態轉移方式 

3.有效狀態

4.佇列

5.標記

我們先來看看一點有意思的吧!

說有一天公主被大魔王抓了,關進了一個迷宮裡,需要你這位勇士去營救(當然成功了就自然是升職加薪贏取白富美啦),這個迷宮以二維陣列的形式給出(暫定為5*5的迷宮)例:

[    0 1 0 0 0
     0 1 0 1 0
     0 0 0 0 0
     0 1 1 1 0

     0 0 0 1 0    ]

其中0表示這裡可以走,1表示這裡是堵牆不能走,每次只能往上下左右四個方向走一個單位,迷宮入口為左上角,而公主在最右下角,問你是否能找到一條最短的路徑成功救出公主,如果能輸出其路徑(每一個路徑以座標形式給出,如(0,0)),不能輸出-1。

當看到什麼最短時間啦,最短路徑啦(當然不是圖那種最短路徑),這就適合用廣度優先遍歷。廣度優先遍歷其實是對狀態的一種遍歷,這裡就要搞清楚什麼是狀態。我們先自己想一下,你看到這道題會有什麼樣的想法,從第一個點出發,可以往右、下走,然後如果往右走,又會延伸出往右、下走,我們用一棵樹來表示((1,2,3)表示x=1,y=2,路徑長度也可以叫時間為3)

                               

這裡我們便可以看到一個點加上一個時間t便構成了一個所謂的狀態,而我們廣度優先搜素就是要按層,一層一層地搜尋這些狀態,直到找到要求狀態為止。上面的圖我們還應該修改修改,因為,第二次到達的點所需要的時間肯定比第一次到達所需要的時間多,如上圖的(0,0,3),所以,我們設定一個標記,凡是我們到達過的點,便不進行第二次的搜尋與延伸,此之謂剪枝

,這樣一來狀態的總數便等於A*B(A為總行數,B為總列數),只要這個時間複雜度在我們的接受範圍之內,我們便可以採用廣度優先搜尋。

狀態轉移呢,是指由一個狀態可以延伸出其他哪些狀態,也就是可以往哪些地方走的問題。

說了這麼多,是時候上點乾貨了,我們看看程式碼

#include<iostream>
#include<queue>
#include<stack>
using namespace std;
int migong[5][5];//儲存迷宮圖
bool flag[5][5];//標記改點是否到達過
class Stat
{
	public:
		int x,y;
		int t;
		Stat* father;//指向其父狀態,用於逆向尋找路徑
};
int R[4][2]={
	-1,0,
	1,0,
	0,-1,
	0,1
};//用於狀態擴充套件
Stat* BFS()//返回終點狀態
{
	 queue<Stat*> Q;//用於儲存還沒有被擴充套件的(即將擴充套件)狀態
	 int x=0,y=0,t=0;
	 Stat* start = new Stat();//這裡是我栽過大坑,由於是在函式內部定義的物件,必須要用new來分配記憶體與堆中
	 start->x=start->y=0;
	 start->t = 0;
	 start->father = NULL;//開始狀態沒有父狀態
	 Q.push(start);
	 while(Q.empty() == false)//直到搜尋完所有狀態退出迴圈
	 {
 		Stat* temp = Q.front();
 		Q.pop();
 		for(int i=0;i<4;i++)//這裡就是狀態的擴充套件,向上下左右四個方向擴充套件
 		{
		 	x = temp->x+R[i][0];
 			y = temp->y+R[i][1];	
 			if(x<0||y<0||x>4||y>4)//超出邊界,便直接捨棄該狀態
 				continue;
			if(flag[x][y] == true)//到達過該狀態,也直接捨棄
				continue;
			if(migong[x][y] == 1)//沒有路,也直接捨棄
				continue;
			Stat* tempS = new Stat();//建立新狀態
			tempS->x = x;
			tempS->y = y;
			tempS->t = temp->t+1;//時間加一
			tempS->father = temp;//指向父節點
			if(x == 4 && y == 4)//如果搜尋到了目標狀態,便返回
			{
				return tempS;
			}
			Q.push(tempS);//將新狀態加入佇列中
			flag[x][y] = true;//標記該狀態已經到達過	
	 	}
 	}
 	return start;
}

int main()
{
	for(int i=0;i<5;i++)//迴圈輸入迷宮
	{
		for(int j=0;j<5;j++)
		{
			cin>>migong[i][j];
			flag[i][j] = false;
		}
	}
	Stat* p = BFS();
	stack<Stat*> S;//放入棧中,主要是為了讓其反序,不然從目標狀態找其父節點遍歷的話,是反的
	while(p != NULL)
	{
		S.push(p);
		p = p->father;	
	}
	while(S.empty() == false)
	{
		Stat* temp = S.top();
		S.pop();
		cout<<"("<<temp->x<<","<<temp->y<<")"<<endl;
	}
}
/*
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
*/

相信大家看了程式碼,也就大概瞭解的八九不離十了,但這裡需要強調的是這裡面佇列的作用,主要是利用了佇列的先進先出的特性,當一個狀態向4個方向擴充套件時,依次將其放入佇列中,再由這四個狀態繼續擴充套件,這樣佇列狀態的取出便是按照層次的順序,一層一層地遍歷的,這才是廣度優先搜尋的的關鍵。

我們來看看結果

                                             

最後,大家一定要記住我踩過的坑(在函式內定義物件時,如果在以後還要用到的話,一定要用new關鍵字分配內存於堆中)

ps: 這是我第一次寫部落格,諸多問題,還請大家指出,海涵。