1. 程式人生 > 實用技巧 >[演算法入門]——廣度優先搜尋(BFS)

[演算法入門]——廣度優先搜尋(BFS)

廣度優先搜尋

廣度優先搜尋,也就是BFS(Breadth First Search),又稱寬度優先搜尋(不過這個才是正式名稱吧)。BFS不像DFS,DFS是在一條路上越走越遠,稍有不慎便有放飛自我的可能,而BFS是將當前節點附近的節點全部訪問一遍,再去訪問下一個節點附近的節點。這麼聽來可能會有點拗口,不過我們先畫一張圖來便於理解:畫圖.exe

你從1點出發,目標是遍歷所有的節點。你先將1點新增到佇列中(紅色,然後依次遍歷1點旁邊的鄰居(附件節點),將其新增到佇列中:

然後,你再依次遍歷2節點的鄰居,但是2節點附近的1節點和3節點已經被新增到佇列中,所以不予新增節點。

依次訪問完1、2、3、4後,發現節點5附近還有一個節點6,於是也將6新增到已訪問。

而所有節點訪問所需要的最短路徑如下圖:

看上去很簡單,我們只需要依次遍歷所有已經訪問過的節點,再將已經訪問過的節點的鄰居加入到佇列中。

在BFS中有一個重要的資料結構:佇列。那麼,什麼是佇列呢?

佇列好比生活中的隊伍,保持著先進先出的思想。下圖是一個沒有節點的佇列:

步驟1,在剛才舉的例子中,我們先將1節點新增到佇列中:

步驟2,隨後,我們獲取佇列中的第一個元素,將它的鄰居新增到佇列中:

步驟3,我們彈出(類似刪除)隊首元素:

重複步驟2和步驟3的行為,直到佇列為空。

或許會發現,有沒有可能一個節點被反覆新增到佇列中,導致死迴圈呢?這種情況是務必避免的。我們可以引入一個數組vis,如果i節點被新增過,於是vis[i] = 1(表示已經入過隊),這要只需要在新增節點的過程中,判斷該節點是否進過佇列。

大致模板:

 1 void BFS()
 2 {
 3     初始化,新增開始節點(例如1)
 4     while (佇列不為空) 
 5     {
 6         獲取隊首元素,將vis設定為1    //保險 
 7         for (遍歷該元素所有鄰居)
 8         {
 9             if(該鄰居進過隊)
10             {
11                 入隊,將vis設定為1 //保險 
12             }
13         }
14         彈出隊首元素 
15     }
16 }

可能說得不好理解,所以這裡給出一份流程圖讓大家慢慢琢磨:

這裡再強調一下,鄰居入隊時要判斷該鄰居是否已經入過隊,並且每次迴圈最後一定要彈出隊首元素。不過,或許有人發現了——為什麼DFS有回溯,但是BFS卻不用回溯呢?DFS所得出來的每一個解,都不一定是最優解,所以需要回溯。但是BFS的特性已經保證所得出的解是最優解,除非你寫錯了。但是證明這一點非常困難,需要很長篇幅,而且BFS不能處理一些例外的題目,比如帶邊權圖。

我們針對剛才的例子,來寫一份從1到達所有節點所需步數的程式碼:

 1 #include <iostream>
 2 #include <cstring>
 3 #include <queue>//佇列 
 4 using namespace std;
 5 struct Edge
 6 {
 7     int next, to;
 8 }edge[200];
 9 int EdgeNum = 0;//邊的數量
10 int head[100]; 
11 void Add(int from, int to)
12 {
13     EdgeNum++;
14     edge[EdgeNum].next = head[from];
15     edge[EdgeNum].to = to;
16     head[from] = EdgeNum;
17 }
18 int vis[100], num[100];
19 /***主體部分,其他可省略不看***/
20 //變數說明:edge->邊,vis->是否入過隊,num->所需步數
21 void BFS()
22 {
23     memset(vis, 0, sizeof(vis));//0代表未入隊 
24     memset(num, 0, sizeof(num));//所需要的的步數 
25     //佇列,儲存索引 
26     queue<int> q;
27     //入隊  
28     q.push(1);
29     while(!q.empty())//佇列不為空
30     {
31         int front = q.front();//獲取隊首元素
32         vis[front] = 1;//記錄為入過隊 
33         for (int i = head[front];i;i = edge[i].next)//遍歷該節點的鄰居 
34         {
35             int to = edge[i].to;//獲取鄰居節點的索引(這樣寫方便閱讀,可省略)
36             if (vis[to] == 0)//如果未入隊
37             {
38                 q.push(to);//將該鄰居入隊 
39                 vis[to] = 1;//記錄為入過隊 
40                 num[to] = num[front] + 1;//更新所需步數 
41             }
42         }
43         q.pop();//注意,必須出隊!沒寫的話會一直死迴圈到世界末日 
44     }
45 }
46 /***主體部分,其他可省略不看***/
47 int main()
48 {
49     //n->節點數,q->邊數 
50     int n, q;
51     cin >> n >> q;
52     //建邊
53     for (int i = 1;i <= q;i++)
54     {
55         int u, v;
56         cin >> u >> v;
57         Add(u, v);
58         Add(v, u);
59     }
60     BFS();
61     //列印結果
62     for (int i = 1;i <= n;i++)
63         cout << num[i] << " "; 
64     return 0;//華麗結尾 
65 }

引入例題

快樂的講解例題時間又到了~(≧▽≦)/~

這裡引入的是洛谷P1135 奇怪的電梯(話說是哪個人可以想到這麼奇怪的電梯的)

題目描述

呵呵,有一天我做了一個夢,夢見了一種很奇怪的電梯。大樓的每一層樓都可以停電梯,而且第ii層樓(1 \le i \le N)(1iN)上有一個數字K_i(0 \le K_i \le N)Ki(0KiN)。電梯只有四個按鈕:開,關,上,下。上下的層數等於當前樓層上的那個數字。當然,如果不能滿足要求,相應的按鈕就會失靈。例如:3, 3 ,1 ,2 ,53,3,1,2,5代表了K_i(K_1=3,K_2=3,…)Ki(K1=3,K2=3,),從11樓開始。在11樓,按“上”可以到44樓,按“下”是不起作用的,因為沒有-22樓。那麼,從AA樓到BB樓至少要按幾次按鈕呢?

輸入格式

共二行。

第一行為33個用空格隔開的正整數,表示N,A,B(1≤N≤200, 1≤A,B≤N)N,A,B(1N200,1A,BN)。

第二行為NN個用空格隔開的非負整數,表示K_iKi

輸出格式

一行,即最少按鍵次數,若無法到達,則輸出-1

輸入輸出樣例

輸入 #1 5 1 5 3 3 1 2 5 輸出 #1 3

不過經過我的實際嘗試,這道題可以用DFS寫。但是為了不砸自己場子,先講講如何用BFS寫。

這道題就是一個十分經典的最短路的問題。作為一道黃題,不是很難,但是很香(BB啥快講啊)。

我們先大致整理一下,每一層樓的鄰居是誰?在這道題中,每層樓有至多兩個鄰居,分別是向上走的樓層和向下走的樓層。當然,題目中有一個隱含限制條件:每層樓必須在1~N之間。換句話說,如果這個鄰居層數超過N或小於1,那麼這個鄰居就是不可用的,就不能新增到佇列中。

將這個限制條件加上vis來判斷是否訪問過,就可以很好解決這個問題了:

 1 #include <iostream>
 2 #include <queue>
 3 using namespace std;
 4 const int SIZE = 10000;
 5 int N, A, B, 
 6 k[SIZE],//電梯上的數字 
 7 vis[SIZE],//這裡有沒有走過 
 8 num[SIZE];//到每一層所需要的次數 
 9 int BFS()
10 {
11     queue<int> q;//佇列,記錄電梯的索引(層數)
12     q.push(A);
13     num[A] = 0;
14     while(!q.empty())
15     {
16         int front = q.front(); //front代表層數 
17         vis[front] = 1;
18         if (front == B)//確認過眼神,你就是B層!
19             return num[B]; 
20         int up = front + k[front], down = front - k[front];//up代表向上走所到的層數,down代表朝下走所到的層數
21         //如果所到的樓層在1-N之間,且沒有被走過 
22         if (up >= 1 && up <= N && !vis[up]) vis[up] = 1, q.push(up), num[up] = num[front] + 1;
23         if (down >= 1 && down <= N && !vis[down]) vis[down] = 1, q.push(down), num[down] = num[front] + 1;
24         q.pop();//出隊 
25     }
26     return -1;//到頭沒找到B層_(:з」∠)_ 
27 }
28 int main()
29 {
30     cin >> N >> A >> B;//輸入N,A,B 
31     for (int i = 1;i <= N;i++)
32         cin >> k[i];//每層樓上的數字 
33     //開啟你的BFS之旅
34     cout << BFS();//列印結果
35 }

484非常簡單?其實可以有一點點優化,比如在22行和23行中,我們可以發現,如果電梯朝上走的話,就一定不會小於1,朝下走的話就一定不會大於N,所以可以刪去大約14個字元(有必要這麼斤斤計較嗎)。這道題還可以用DFS來寫,但是程式碼是我很久以前寫的,或許很多地方沒寫清(關鍵是沒打註釋)

 1 #include <cstdio>
 2 int n, a, b, k[205], to[205] = {}, yes = 0x7ffffff;
 3 int x (int cing, int cs)
 4 {
 5     if (cs > yes)
 6         return 0;
 7     if (cing == b)
 8         if (yes > cs)
 9             yes = cs;
10     to[cing] = 1;
11     if(cing + k[cing] <= n && !to[cing + k[cing]])
12     {    
13         x(cing + k[cing], cs + 1);
14     }
15     if(cing - k[cing] > -1 && !to[cing - k[cing]])
16     {
17         x(cing - k[cing], cs + 1);
18     }
19     to[cing] = 0;
20 }
21 int main ()
22 {
23     scanf ("%d%d%d", &n, &a, &b);
24     for (int i = 1;i <= n;i++)
25     {
26         scanf ("%d", &k[i]);
27         to[i] = 0;
28     }
29     to[a] = 1;
30     x (a, 0);
31     if (yes != 0x7ffffff) 
32         printf ("%d", yes);
33     else
34         printf ("-1"); 
35 }
用深搜寫啦

BFS與DFS的區別,以及優缺點

BFS在本質上和DFS一樣,都是暴力演算法,暴力求解。而且泛用性很廣。許多題目都可以用BFS或DFS騙上20、30甚至50分,但是BFS與DFS往往會是空間或時間開銷很大,甚至無法控制。

BFS與DFS的區別,通俗點來說,BFS就是浪費空間來獲取時間上的優勢,比如一張10*10的平面圖,BFS起碼需要一個二維陣列來儲存,並且使用100單位的記憶體。DFS就是浪費時間來節省空間,就像剛才的10*10的平面圖一樣,DFS最多需要40單位的記憶體,如果有優化甚至只需要20單位,但是時間開銷極大,因為同一個節點會被訪問許多次。

BFS

優點:速度相對來說會快很多,並且可以暴力做出許多題目。

缺點:但是遇到資料刁鑽的情況,就得不到用武之地。而且沒有做好優化就MLE讓你懷疑人生。速度不一定會比DFS快。一道非常經典的題目“埃及分數”就很難用BFS做出來,就算做出來了空間開銷也無法讓人承受(評測姬:我太難了)。

DFS

優點:空間開銷很小,並且思路容易想到,在有剪枝優化或記憶化搜尋之後,可以很輕鬆做出許多題目。

缺點:太慢了,太慢了,太慢了!動不動就return 3221225725(棧溢位),資料大(超過100左右)那麼一點點就掛了。

關於學習DFS

如果你想看看DFS的用法,可以參考我的另外一篇部落格,或者以下我推薦的部落格:

https://www.cnblogs.com/DWVictor/p/10048554.html(例子超多)

https://www.jianshu.com/p/bff70b786bb6(講解詳細)

https://www.cnblogs.com/skywang12345/p/3711483.html(原始碼多)

https://www.cnblogs.com/TheAzureDeepSpace/p/13454102.html(我寫的,不解釋。手動滑稽)