廣度/寬度優先搜索(BFS)

1.前言

廣度優先搜索(也稱寬度優先搜索,縮寫BFS,如下采用廣度來描述)是連通圖的一種遍歷策略。由於它的思想是從一個頂點V0開始,輻射狀地優先遍歷其周圍較廣的區域,故得名。 算法

通常能夠用它作什麼呢?一個最直觀經典的例子就是走迷宮,咱們從起點開始,找出到終點的最短路程,不少最短路徑算法就是基於廣度優先的思想成立的。編程

算法導論裏邊會給出很多嚴格的證實,我想盡可能寫得通俗一點,所以採用一些直觀的講法來假裝成證實,關鍵的point可以幫你get到就好。數組

2.圖的概念

剛剛說的廣度優先搜索是連通圖的一種遍歷策略,那就有必要將圖先簡單解釋一下。網絡

 

圖2-1 連通圖示例圖app

如圖2-1所示,這就是咱們所說的連通圖,這裏展現的是一個無向圖,連通即每2個點都有至少一條路徑相連,例如V0到V4的路徑就是V0->V1->V4。spa

通常咱們把頂點用V縮寫,把邊用E縮寫。.net

3.廣度優先搜索


3.1.算法的基本思路

經常咱們有這樣一個問題,從一個起點開始要到一個終點,咱們要找尋一條最短的路徑,從圖2-1舉例,若是咱們要求V0到V6的一條最短路(假設走一個節點按一步來算)【注意:此處你能夠選擇不看這段文字直接看圖3-1】,咱們明顯看出這條路徑就是V0->V2->V6,而不是V0->V3->V5->V6。先想一想你本身剛剛是怎麼找到這條路徑的:首先看跟V0直接鏈接的節點V一、V二、V3,發現沒有V6,進而再看剛剛V一、V二、V3的直接鏈接節點分別是:{V0、V4}、{V0、V一、V6}、{V0、V一、V5}(這裏畫刪除線的意思是那些頂點在咱們剛剛的搜索過程當中已經找過了,咱們不須要從新回頭再看他們了)。這時候咱們從V2的連通節點集中找到了V6,那說明咱們找到了這條V0到V6的最短路徑:V0->V2->V6,雖然你再進一步搜索V5的鏈接節點集合後會找到另外一條路徑V0->V3->V5->V6,但顯然他不是最短路徑。指針

你會看到這裏有點像輻射形狀的搜索方式,從一個節點,向其旁邊節點傳遞病毒,就這樣一層一層的傳遞輻射下去,知道目標節點被輻射中了,此時就已經找到了從起點到終點的路徑。htm

咱們採用示例圖來講明這個過程,在搜索的過程當中,初始全部節點是白色(表明了全部點都還沒開始搜索),把起點V0標誌成灰色(表示即將輻射V0),下一步搜索的時候,咱們把全部的灰色節點訪問一次,而後將其變成黑色(表示已經被輻射過了),進而再將他們所能到達的節點標誌成灰色(由於那些節點是下一步搜索的目標點了),可是這裏有個判斷,就像剛剛的例子,當訪問到V1節點的時候,它的下一個節點應該是V0和V4,可是V0已經在前面被染成黑色了,因此不會將它染灰色。這樣持續下去,直到目標節點V6被染灰色,說明了下一步就到終點了,不必再搜索(染色)其餘節點了,此時能夠結束搜索了,整個搜索就結束了。而後根據搜索過程,反過來把最短路徑找出來,圖3-1中把最終路徑上的節點標誌成綠色。blog

整個過程的實例圖如圖3-1所示。

初始所有都是白色(未訪問)

即將搜索起點V0(灰色)

已搜索V0,即將搜索V一、V二、V3

……終點V6被染灰色,終止

找到最短路徑

圖3-1 尋找V0到V6的過程

3.2.廣度優先搜索流程圖

 

圖3-2 廣度優先搜索的流程圖

在寫具體代碼以前有必要先舉個實例,詳見第4節。

4.實例

第一節就講過廣度優先搜索適用於迷宮類問題,這裏先給出POJ3984《迷宮問題》。

 

《迷宮問題》

定義一個二維數組: 
int maze[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,
};
它表示一個迷宮,其中的1表示牆壁,0表示能夠走的路,只能橫着走或豎着走,不能斜着走,要求編程序找出從左上角到右下角的最短路線。 

 

題目保證了輸入是必定有解的。

也許你會問,這個跟廣度優先搜索的圖怎麼對應起來?BFS的第一步就是要識別圖的節點跟邊!

4.1.識別出節點跟邊

節點就是某種狀態,邊就是節點與節點間的某種規則。

對應於《迷宮問題》,你能夠這麼認爲,節點就是迷宮路上的每個格子(非牆),走迷宮的時候,格子間的關係是什麼呢?按照題目意思,咱們只能橫豎走,所以咱們能夠這樣看,格子與它橫豎方向上的格子是有連通關係的,只要這個格子跟另外一個格子是連通的,那麼兩個格子節點間就有一條邊。

若是說本題再修改爲斜方向也能夠走的話,那麼就是格子跟周圍8個格子均可以連通,因而一個節點就會有8條邊(除了邊界的節點)。

4.2.解題思路

對應於題目的輸入數組:

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,

咱們把節點定義爲(x,y),(x,y)表示數組maze的項maze[x][y]。

因而起點就是(0,0),終點是(4,4)。按照剛剛的思路,咱們大概手工梳理一遍:

初始條件:

起點Vs爲(0,0)

終點Vd爲(4,4)

灰色節點集合Q={}

初始化全部節點爲白色節點

開始咱們的廣度搜索!

手工執行步驟【PS:你能夠直接看圖4-1】:

1.起始節點Vs變成灰色,加入隊列Q,Q={(0,0)}

2.取出隊列Q的頭一個節點Vn,Vn={0,0},Q={}

3.把Vn={0,0}染成黑色,取出Vn全部相鄰的白色節點{(1,0)}

4.不包含終點(4,4),染成灰色,加入隊列Q,Q={(1,0)}

5.取出隊列Q的頭一個節點Vn,Vn={1,0},Q={}

6.把Vn={1,0}染成黑色,取出Vn全部相鄰的白色節點{(2,0)}

7.不包含終點(4,4),染成灰色,加入隊列Q,Q={(2,0)}

8.取出隊列Q的頭一個節點Vn,Vn={2,0},Q={}

9.把Vn={2,0}染成黑色,取出Vn全部相鄰的白色節點{(2,1), (3,0)}

10.不包含終點(4,4),染成灰色,加入隊列Q,Q={(2,1), (3,0)}

11.取出隊列Q的頭一個節點Vn,Vn={2,1},Q={(3,0)}

12. 把Vn={2,1}染成黑色,取出Vn全部相鄰的白色節點{(2,2)}

13.不包含終點(4,4),染成灰色,加入隊列Q,Q={(3,0), (2,2)}

14.持續下去,知道Vn的全部相鄰的白色節點中包含了(4,4)……

15.此時得到了答案

 

起始你很容易模仿上邊過程走到終點,那爲何它就是最短的呢?

怎麼保證呢?

咱們來看看廣度搜索的過程當中節點的順序狀況:

 

圖4-1 迷宮問題的搜索樹

你是否觀察到了,廣度搜索的順序是什麼樣子的?

圖中標號即爲咱們搜索過程當中的順序,咱們觀察到,這個搜索順序是按照上圖的層次關係來的,例如節點(0,0)在第1層,節點(1,0)在第2層,節點(2,0)在第3層,節點(2,1)和節點(3,0)在第3層。

咱們的搜索順序就是第一層->第二層->第三層->第N層這樣子。

咱們假設終點在第N層,所以咱們搜索到的路徑長度確定是N,並且這個N必定是所求最短的。

咱們用簡單的反證法來證實:假設終點在第N層上邊出現過,例如第M層,M<N,那麼咱們在搜索的過程當中,確定是先搜索到第M層的,此時搜索到第M層的時候發現終點出現過了,那麼最短路徑應該是M,而不是N了。

因此根據廣度優先搜索的話,搜索到終點時,該路徑必定是最短的。

4.3.代碼

我給出如下代碼用於解決上述題目(僅僅只是核心代碼):

 

[cpp] view plain copy

  1. /** 
  2.  * 廣度優先搜索 
  3.  * @param Vs 起點 
  4.  * @param Vd 終點 
  5.  */  
  6. bool BFS(Node& Vs, Node& Vd){  
  7.     queue<Node> Q;  
  8.     Node Vn, Vw;  
  9.     int i;  
  10.   
  11.     //用於標記顏色當visit[i][j]==true時,說明節點訪問過,也就是黑色  
  12.     bool visit[MAXL][MAXL];  
  13.   
  14.     //四個方向  
  15.     int dir[][2] = {  
  16.         {0, 1}, {1, 0},  
  17.         {0, -1}, {-1, 0}  
  18.     };  
  19.   
  20.     //初始狀態將起點放進隊列Q  
  21.     Q.push(Vs);  
  22.     visit[Vs.x][Vs.y] = true;//設置節點已經訪問過了!  
  23.   
  24.     while (!Q.empty()){//隊列不爲空,繼續搜索!  
  25.         //取出隊列的頭Vn  
  26.         Vn = Q.front();  
  27.         Q.pop();  
  28.   
  29.         for(i = 0; i < 4; ++i){  
  30.             Vw = Node(Vn.x+dir[i][0], Vn.y+dir[i][1]);//計算相鄰節點  
  31.   
  32.             if (Vw == Vd){//找到終點了!  
  33.                 //把路徑記錄,這裏沒給出解法  
  34.                 return true;//返回  
  35.             }  
  36.   
  37.             if (isValid(Vw) && !visit[Vw.x][Vw.y]){  
  38.                 //Vw是一個合法的節點而且爲白色節點  
  39.                 Q.push(Vw);//加入隊列Q  
  40.                 visit[Vw.x][Vw.y] = true;//設置節點顏色  
  41.             }  
  42.         }  
  43.     }  
  44.     return false;//無解  
  45. }  


 

 

5.核心代碼

爲了方便適用於大多數的題解,抽取核心代碼以下:

 

[cpp] view plain copy

  1. /** 
  2.  * 廣度優先搜索 
  3.  * @param Vs 起點 
  4.  * @param Vd 終點 
  5.  */  
  6. bool BFS(Node& Vs, Node& Vd){  
  7.     queue<Node> Q;  
  8.     Node Vn, Vw;  
  9.     int i;  
  10.   
  11.     //初始狀態將起點放進隊列Q  
  12.     Q.push(Vs);  
  13.     hash(Vw) = true;//設置節點已經訪問過了!  
  14.   
  15.     while (!Q.empty()){//隊列不爲空,繼續搜索!  
  16.         //取出隊列的頭Vn  
  17.         Vn = Q.front();  
  18.   
  19.         //從隊列中移除  
  20.         Q.pop();  
  21.   
  22.         while(Vw = Vn經過某規則可以到達的節點){  
  23.             if (Vw == Vd){//找到終點了!  
  24.                 //把路徑記錄,這裏沒給出解法  
  25.                 return true;//返回  
  26.             }  
  27.   
  28.             if (isValid(Vw) && !visit[Vw]){  
  29.                 //Vw是一個合法的節點而且爲白色節點  
  30.                 Q.push(Vw);//加入隊列Q  
  31.                 hash(Vw) = true;//設置節點顏色  
  32.             }  
  33.         }  
  34.     }  
  35.     return false;//無解  
  36. }  


 

 

對於一個題目來講,要標誌節點是否訪問過,用數組是一種很快速的方法,但有時數據量太大,很難用一個大數組來記錄時,採用hash是最好的作法。實際上visit數組在這裏也是充當hash的做用。(PS:至於hash是什麼?得本身去了解,它的做用是在O(1)的時間複雜度內取出某個值)

6.其餘實例

6.1.題目描述

給定序列1 2 3 4 5 6,再給定一個k,咱們給出這樣的操做:對於序列,咱們能夠將其中k個連續的數所有反轉過來,例如k = 3的時候,上述序列通過1步操做後能夠變成:3 2 1 4 5 6 ,若是再對序列 3 2 1 4 5 6進行一步操做,能夠變成3 4 1 2 5 6.

那麼如今題目就是,給定初始序列,以及結束序列,以及k的值,那麼你可以求出從初始序列到結束序列的轉變至少須要幾步操做嗎?

6.2.思路

本題能夠採用BFS求解,已經給定初始狀態跟目標狀態,要求之間的最短操做,其實也很明顯是用BFS了。

咱們把每次操做完的序列當作一個狀態節點。那每一次操做就產生一條邊,這個操做就是規則。

假設起始節點是:{1 2 3 4 5 6},終點是:{3 4 1 2 5 6}

去除隊列中的起始節點時,將它的相鄰節點加入隊列,其相鄰節點就是對其操做一次的全部序列:

{3 2 1 4 5 6}、{1 4 3 2 5 6}、{1 2 5 4 3 6}、{1 2 3 6 5 4}

而後繼續搜索便可獲得終點,此時操做數就是搜索到的節點所在的層數2。

7.OJ題目

題目分類來自網絡:

sicily:1048 1444 1215 1135 1150 1151 1114

pku:1136 1249 1028 1191 3278 1426 3126 3087 3414 

8.總結

假設圖有V個頂點,E條邊,廣度優先搜索算法須要搜索V個節點,所以這裏的消耗是O(V),在搜索過程當中,又須要根據邊來增長隊列的長度,因而這裏須要消耗O(E),總得來講,效率大約是O(V+E)。

其實最影響BFS算法的是在於Hash運算,咱們前面給出了一個visit數組,已經算是最快的Hash了,但有些題目來講可能Hash的速度要退化到O(lgn)的複雜度,固然了,具體仍是看實際狀況的。

BFS適合此類題目:給定初始狀態跟目標狀態,要求從初始狀態到目標狀態的最短路徑。

9.擴展

進而擴展的話就是雙向廣度搜索算法,顧名思義,便是從起點跟終點分別作廣度優先搜索,直到他們的搜索過程當中有一個節點相同了,因而就找到了起點跟終點的一條路徑。

騰訊筆試題目:假設每一個人平均是有25個好友,根據六維理論,任何人之間的聯繫必定能夠經過6我的而間接認識,間接經過N我的認識的,那他就是你的N度好友,如今要你編程驗證這個6維理論。

此題若是直接作廣度優先搜索,那麼搜索的節點數可能達到25^6,若是是用雙向的話,兩個樹分別只須要搜索到3度好友便可,搜索節點最多爲25^3個,可是用雙向廣度算法的話會有一個問題要解決,就是你如何在搜索的過程當中判斷第一棵樹中的節點跟第二棵樹中的節點有相同的呢?按個人理解,能夠用Hash,又或者放進隊列的元素都是指向原來節點的指針,而每一個節點加入一個color的屬性,這樣再搜索過程當中就能夠根據節點的color來判斷是否已經被搜索過了。

相關文章
相關標籤/搜索