1. 程式人生 > >廣度/寬度優先搜尋(BFS)詳解

廣度/寬度優先搜尋(BFS)詳解

1.前言

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

一般可以用它做什麼呢?一個

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

演算法導論裡邊會給出不少嚴格的證明,我想盡量寫得通俗一點,因此採用一些直觀的講法來偽裝成證明,關鍵的point能夠幫你get到就好。

2.圖的概念

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

圖2-1連通圖示例圖

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

一般我們把頂點用V縮寫,把邊用E縮寫。

3.廣度優先搜尋


3.1.演算法的基本思路

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

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

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

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

初始全部都是白色(未訪問)

即將搜尋起點V0(灰色)

已搜尋V0,即將搜尋V1、V2、V3

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

找到最短路徑

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

3.2.廣度優先搜尋流程圖

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

在寫具體程式碼之前有必要先舉個例項,詳見第4節。

4.例項

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

《迷宮問題》

定義一個二維陣列:
intmaze[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了。< p="">

所以根據廣度優先搜尋的話,搜尋到終點時,該路徑一定是最短的。

4.3.程式碼

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

?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 /** * 廣度優先搜尋