本系列是這本算法教材的擴展資料:《算法競賽入門到進階》(京東 當當 ) 清華大學出版社
PDF下載地址:https://github.com/luoyongjun999/code 其中的“補充資料”
如有建議,請聯系:(1)QQ 群,567554289;(2)作者QQ,15512356
《算法競賽入門到進階》的第4章“搜索技術”,講解了遞歸、BFS、DFS的原理,以及雙向廣搜、A*算法、剪枝、迭代加深搜索、IDA*的經典例題,適合入門搜索算法。
本文分幾篇專題介紹搜索擴展內容、講解更多習題,便於讀者深入掌握搜索技術。
第1篇:搜索基礎。
第2篇:剪枝。
第3篇:廣搜進階。
第4篇:迭代加深、A*、IDA*。
本文是第3篇。
本篇深入地講解了雙向廣搜、BFS+優先隊列、BFS+雙端隊列的算法思想和應用,幫助讀者對BFS的理解更上一層樓。
1 雙向廣搜
1.1 雙向廣搜的原理和復雜度分析
雙向廣搜的應用場合:有確定的起點和終點,並且能把從起點到終點的單個搜索,變換為分別從起點出發和從終點出發的“相遇”問題,可以用雙向廣搜。
從起點s(正向搜索)和終點t(逆向搜索)同時開始搜索,當兩個搜索產生相同的一個子狀態v時就結束。得到的s-v-t是一條最佳路徑,當然,最佳路徑可能不止這一條。
注意,和普通BFS一樣,雙向廣搜在搜索時並沒有“方向感”,所謂“正向搜索”和“逆向搜索”其實是盲目的,它們從s和t逐層擴散出去,直到相遇為止。
與只做一次BFS相比,雙向BFS能在多大程度上改善算法復雜度?下面以網格圖和樹形結構為例,推出一般性結論。
(1)網格圖。
用BFS求下面圖中兩個黑點s和t間的最短路。左圖是一個BFS;右圖是雙向BFS,在中間的五角星位置相遇。

設兩點的距離是k。左邊的BFS,從起點s擴展到t,一共訪問了\(2k(k+1)≈2k^2\)個結點;右邊的雙向BFS,相遇時一共訪問了約\(k^2\)個結點。兩者差2倍,改善並不明顯。
在這個網格圖中,BFS擴展的第k和第k+1層,結點數量相差(k+1)/k倍,即結點數量是線性增長的。
(2)樹形結構。

以二叉樹為例,求根結點s到最后一行的黑點t的最短路。
左圖做一次BFS,從第1層到第k-1層,共訪問\(1 + 2 +...+ 2^{k-1} ≈ 2^k\)個結點。右圖是雙向BFS,分別從上往下和從下往上BFS,在五角星位置相遇,共訪問約\(2×2^{k/2}\)個結點。雙向廣搜比做一次BFS改善了\(2^{k/2}\)倍,優勢巨大。
在二叉樹的例子中,BFS擴展的第k和第k+1層,結點數量相差2倍,即結點數量是指數增長的。
從上面2個例子可以得到一般性結論:
(1)做BFS擴展的時候,下一層結點(一個結點表示一個狀態)數量增加越快,雙向廣搜越有效率。
(2)是否用雙向廣搜替代普通BFS,除了(1)以外,還應根據總狀態數量的規模來決定。雙向BFS的優勢,從根本上說,是它能減少需要搜索的狀態數量。有時雖然下一層數量是指數增長的,但是由於去重或者限制條件,總狀態數並不多,也就沒有必要使用雙向BFS。例如后面的例題“hdu 1195 open the lock”,密碼范圍1111~9999,共約9000種,用BFS搜索時,最多有9000個狀態進入隊列,就沒有必要使用雙向BFS。而例題HDU 1401 Solitaire,可能的棋局狀態有1500萬種,走8步棋會擴展出\(16^8\)種狀態,大於1500萬,相當於擴展到所有可能的棋局,此時應該使用雙向BFS。
很多教材和網文講解雙向廣搜時,常用八數碼問題做例子。下圖引用自《算法競賽入門到進階》4.3.2節,演示了從狀態A移動到狀態F的搜索過程。
八數碼共有9! = 362880種狀態,不太多,用普通BFS也行。不過,用雙向廣搜更好,因為八數碼每次擴展,下一層的狀態數量是上一層的2~4倍,比二叉樹的增長還快,效率的提升也就更高。

1.2 雙向廣搜的實現
雙向廣搜的隊列,有兩種實現方法:
(1)合用一個隊列。正向BFS和逆向BFS用同一個隊列,適合正反2個BFS平衡的情況。正向搜索和逆向搜索交替進行,兩個方向的搜索交替擴展子狀態,先后入隊。直到兩個方向的搜索產生相同的子狀態,即相遇了,結束。這種方法適合正反方向擴展的新結點數量差不多的情況,例如上面的八數碼問題。
(2)分成兩個隊列。正向BFS和逆向BFS的隊列分開,適合正反2個BFS不平衡的情況。讓子狀態少的BFS先擴展下一層,另一個子狀態多的BFS后擴展,可以減少搜索的總狀態數,盡快相遇。例題見后面的“洛谷p1032 字串變換”。
和普通BFS一樣,雙向廣搜在擴展隊列時也需要處理去重問題。把狀態入隊列的時候,先判斷這個狀態是否曾經入隊,如果重復了,就丟棄。
1.3 雙向廣搜例題
1.hdu 1195 open the lock
http://acm.hdu.edu.cn/showproblem.php?pid=1195
題目描述:打開密碼鎖。密碼由四位數字組成,數字從1到9。可以在任何數字上加上1或減去1,當'9'加1時,數字變為'1',而'1'減1時,數字變為'9'。相鄰的數字可以交換。每個動作是一步。任務是使用最少的步驟來打開鎖。注意:最左邊的數字不是最右邊的數字的鄰居。
輸入:輸入文件以整數T開頭,表示測試用例的數量。
每個測試用例均以四位數N開頭,指示密碼鎖定的初始狀態。然后緊跟另一行帶有四個下標M的行,指示可以打開鎖的密碼。每個測試用例后都有一個空白行。
輸出:對於每個測試用例,一行中打印最少的步驟。
樣例輸入:
2
1234
2144
1111
9999
樣例輸出:
2
4
題解:
題目中的4位數字,走一步能擴展出11種情況;如果需要走10步,就可能有\(11^10\)種情況,數量非常多,看起來用雙向廣搜能大大提高搜索效率。不過,這一題用普通BFS也行,因為並沒有\(11^10\)種情況,密碼范圍1111~9999,只有約9000種。用BFS搜索時,最多有9000個狀態進入隊列,沒有必要使用雙向廣搜。
密碼進入隊列時,應去重,去掉重復的密碼。去重用hash最方便。
讀者可以用這一題練習雙向廣搜。
2.HDU 1401 Solitaire
經典的雙向廣搜例題。
http://acm.hdu.edu.cn/showproblem.php?pid=1401
題目描述:8×8的方格,放4顆棋子在初始位置,給定4個最終位置,問在8步內是否能從初始位置走到最終位置。規則:每個棋子能上下左右移動,若4個方向已經有一棋子則可以跳到下一個空白位置。例如,圖中(4,4)位置的棋子有4種移動方法。

題解:
在8×8的方格上放4顆棋子,有64×63×62×61≈1500萬種布局。走一步棋,4顆棋子共有16種走法,連續走8步棋,會擴展出\(16^8\)種棋局,\(16^8\)大於1500萬,走8步可能會遍歷到1500萬棋局。
此題應該使用雙向BFS。從起點棋局走4步,從終點棋局走4步,如果能相遇就有一個解,共擴展出\(2×16^4=131072\)種棋局,遠遠小於1500萬。
本題也需要處理去重問題,擴展下一個新棋局時,看它是否在隊列中處理過。用hash的方法,定義char vis[8][8][8][8][8][8][8][8]表示棋局,其中包含4個棋子的坐標。當vis=1時表示正向搜過這個棋局,vis=2表示逆向搜過。例如4個棋子的坐標是(a.x, a.y)、(b.x, b.y)、(c.x, c.y)、(d.x, d.y),那么:
vis[a.x][a.y][b.x][b.y][c.x][c.y][d.x][d.y] = 1
表示這個棋局被正向搜過。
4個棋子需要先排序,然后再用vis記錄。如果不排序,一個棋局就會有很多種表示,不方便判重。
char vis[8][8][8][8][8][8][8][8] 用了\(8^8\) = 16M空間。如果定義為int,占用64M空間,超過題目的限制。
3.HDU 3095 Eleven puzzle
http://acm.hdu.edu.cn/showproblem.php?pid=3095
題目描述:如圖是13個格子的拼圖,數字格可以移動到黑色格子。左圖是開始局面,右圖是終點局面。一次移動一個數字格,問最少移動幾次可以完成。

題解:
(1)可能的局面有13!,極大。
(2)用一個BFS,復雜度過高。每次移動1個黑格,移動方法最少1種,最多8種。如果移動10次,那么最多有\(8^{10}\) ≈ 10億種。
(3)用雙向廣搜,能減少到\(2×8^5\) = 65536種局面。
(4)判重:可以用hash,或者用STL的map。
4.洛谷p1032 字串變換
https://www.luogu.com.cn/problem/P1032
題目描述:已知有兩個字串A,B及一組字串變換的規則(至多6個規則):
A1->B1
A2->B2
規則的含義為:在A中的子串 A1可以變換為B1,A2可以變換為 B2…。
例如:A=abcd,B=xyz,
變換規則為:
abc→xu,ud→y,y→yz
則此時,A可以經過一系列的變換變為B,其變換的過程為:
abcd→xud→xy→xyz。
共進行了3次變換,使得A變換為B。
輸入輸出:給定字串A、B和變換規則,問能否在10步內將A變換為B,輸出最少的變換步數。字符串長度的上限為20。
題解:
(1)若用一個BFS,每層擴展6個規則,經過10步,共\(6^{10}\) ≈ 6千萬次變換。
(2)用雙向BFS,可以用\(2×6^5\) = 15552次變換搜完10步。
(3)用兩個隊列分別處理正向BFS和逆向BFS。由於起點和終點的串不同,它們擴展的下一層數量也不同,也就是進入2個隊列的串的數量不同,先處理較小的隊列,可以加快搜索速度。2個隊列見下面的代碼示例[完整代碼參考:https://blog.csdn.net/qq_45772483/article/details/104504951]。
void bfs(string A, string B){ //起點是A,終點是B
queue <string> qa, qb; //定義2個隊列
qa.push(A); //正向隊列
qb.push(B); //逆向隊列
while(qa.size() && qb.size()){
if (qa.size() < qb.size()) //如果正向BFS隊列小,先擴展它
extend(qa, ...); //擴展時,判斷是否相遇
else //否則擴展逆向BFS
extend(qb, ...); //擴展時,判斷是否相遇
}
}
5.poj 3131 Cubic Eight-Puzzle
http://poj.org/problem?id=3131
立體八數碼問題。狀態多、代碼長,是一道難題。
2 BFS + 優先隊列
2.1 優先隊列
普通隊列中的元素是按先后順序進出隊列的,先進先出。在優先隊列中,元素被賦予了優先級,每次彈出隊列的,是具有最高優先級的元素。優先級根據需求來定義,例如定義最小值為最高優先級。
優先隊列有多種實現方法。最簡單的是暴力法,在n個數中掃描最小值,復雜度是O(n)。暴力法不能體現優先隊列的優勢,真正的優先隊列一般用堆這種數據結構實現[堆的概念和代碼實現,見https://www.cnblogs.com/luoyj/p/12409990.html],插入元素和彈出最高優先級元素,復雜度都是O(logn)。
雖然基於堆的優先隊列很容易手寫,不過競賽中一般不用自己寫,而是直接用STL的priority_queue。
2.2 最短路問題
BFS 結合優先隊列,可解決最短路徑問題。
1.算法描述
下面描述“BFS+優先隊列”求最短距離的算法步驟。以下圖為例,起點是A,求A到其它結點的最短路。圖的結點總數是V,邊的總數是E。

算法的過程,用到了貪心的思想。從起點A開始,逐層擴展它的鄰居,放到優先隊列里,並從優先隊列中彈出距離A最近的點,就得到了這個點到A的最短距離;當新的點放進隊列里時,如果經過它,使得隊列里面的它的鄰居到A更近,就更這些鄰居點的距離。
以圖4為例,步驟是:
(1)開始時,把起點A放到優先隊列Q里:{\(A_0\)}。下標表示從A出發到這個點的路徑長度,A到自己的距離是0。
(2)從隊列中彈出最小值,即A,擴展A的鄰居結點,放到優先隊列Q里:{\(B_6, C_3\)}。下標表示從A出發到這個點的路徑長度。一條路徑上包含了多個結點。Q中記錄的是各結點到起點A的路徑長度,其中有一個最短,優先隊列Q可以快速取出它。
(3)從優先隊列Q中彈出最小值,即距離起點A最短的結點,這次是C。在這一步,找到了A到C的最短路徑長度,C是第一個被確定最短路徑的結點。考察C的鄰居,其中的新鄰居D、E直接放到Q里:{\(B_5, D_6, E_7\)};隊列里的舊鄰居B,看經過C到B是否距離更短,如果更短,就更新,所以\(B_6\)更新為\(B_5\),現在A經過C到B,總距離是5。
(4)繼續從優先隊列Q中取出距離最短的結點,這次是B,在這一步,找到了A到B的最短路徑長度,路徑是A-C-B。考察B的鄰居,B沒有新鄰居放進Q;B在Q中的舊鄰居D,通過B到它也並沒有更近,所以不用更新。Q現在是{\(D_6, E_7\)}。
繼續以上過程,每個結點都會進入Q並彈出,最后Q為空時結束。
在優先隊列Q里找最小值,也就是找距離最短的結點,復雜度是\(O(logV)\)。“BFS+優先隊列”求最短路徑,算法的總復雜度是\(O((V+E)logV)\)。共檢查V+E次,每次優先隊列是\(O(logV)\)。
如果不用優先隊列,直接在V個點中找最小值,是O(V)的,總復雜度\(O(V^2)\)。
\(O(V^2)\)是否一定比\(O((V+E)logV)\)好?下面將討論這個問題。
(1)稀疏圖中,點和邊的數量差不多,V ≈ E,用優先隊列的復雜度\(O((V+E)logV)\)可以寫成\(O(VlogV)\),它比\(O(V^2)\)好,是非常好的優化。
(2)稠密圖中,點少於邊,\(V < E\)且\(V^2 ≈ E\),復雜度\(O((V+E)logV)\)可以寫成\(O(V^2logV)\),它比\(O(V^2)\)差。這種情況下,用優先隊列,反而不如直接用暴力搜。
2. BFS與Dijkstra
讀者如果學過最短路徑算法Dijkstra[參考《算法競賽入門到進階》10.9.4 Dijkstra,詳細地解釋了Dijkstra算法,給出了模板代碼],就會發現,實際上這就是上一節中用優先隊列實現的BFS,即:“Dijkstra + 優先隊列 = BFS + 優先隊列(隊列中放的是從起點到當前點的距離)”。
上面括號中的“隊列中放的是從起點到當前點的距離”的注解,說明了它們的區別,即“Dijkstra + 優先隊列”和“BFS + 優先隊列”並不完全相同。例如,如果在BFS時進入優先隊列的是“從當前點到終點的距離”,那么就是貪心最優搜索(Greedy Best First Search)。
根據前面的討論,Dijkstra 算法也有下面的結論:
(1)稀疏圖,用“Dijkstra + 優先隊列”,復雜度\(O((V+E)logV) = O(VlogV)\);
(2)稠密圖,如果\(V^2 ≈ E\),不用優先隊列,直接在所有結點中找距離最短的那個點,總復雜度\(O(V^2)\)。
稀疏圖的存儲用鄰接表或鏈式前向星,稠密圖用鄰接矩陣。
2.3 例題
下面幾個題目都是“BFS+優先隊列”求最短路。
1. hdu 3152 Obstacle Course
http://acm.hdu.edu.cn/showproblem.php?pid=3152
題目描述:一個N*N的矩陣,每個結點上有一個費用。從起點[0][0]出發到終點[N-1][N-1],求最短的路徑,即經過的結點的費用和最小。每次移動,可以沿上下左右四個方向走一步。
輸入:第一行是N,后面跟着N行,每一行有N個數字。最后一行是0,表示終止。2<=N<=125。
輸出:最小費用。
輸入樣例:
3
5 5 4
3 9 1
3 2 7
5
3 7 2 0 1
2 8 0 9 1
1 2 1 8 1
9 8 9 2 0
3 6 5 1 5
0
輸出樣例:
Problem 1: 20
Problem 2: 19
題解:
最短路徑問題[題目一般不會要求打印路徑,因為可能有多條最短路徑,不方便系統測試。如果需要打印出最短路徑,參考《算法競賽入門到進階》“10.9 最短路”,給出了路徑打印的代碼]。N很小,用矩陣存圖。
下面是代碼。
#include<bits/stdc++.h>
using namespace std;
const int maxn=150, INF=1<<30;
int dir[4][2]={{0,1},{1,0},{0,-1},{-1,0}};
int n, graph[maxn][maxn], vis[maxn][maxn]; //vis記錄到起點的最短距離
struct node{
int x,y,sum;
friend bool operator <(node a,node b) {
return a.sum > b.sum;
}
};
int bfs(){ //dijkstra
fill(&vis[0][0], &vis[maxn][0], INF);
vis[0][0] = graph[0][0]; //起點到自己的距離
priority_queue <node> q;
node first = {0, 0, graph[0][0]};
q.push(first); //起點進隊
while(q.size()) {
node now = q.top(); q.pop(); //每次彈出已經找到最短距離的結點
if(now.x==n-1 && now.y==n-1) //終點:右下角
return now.sum; //返回
for(int i=0; i<4; i++){ //上下左右
node t = now; //擴展now的鄰居
t.x += dir[i][0];
t.y += dir[i][1];
if(0<=t.x && t.x<n && 0<=t.y && t.y<n) { //在圖內
t.sum += graph[t.x][t.y];
if(vis[t.x][t.y] <= t.sum) continue;
//鄰居已經被搜過,並且距離更短,不用更新
if(vis[t.x][t.y] == INF) q.push(t); //如果沒進過隊列,就進隊
vis[t.x][t.y] = t.sum; //更新這個結點到起點的距離
}
}
}
return -1;
}
int main(){
int k = 1;
while(cin>>n, n){
for(int i=0; i<n; i++)
for(int j=0; j<n; j++)
cin >> graph[i][j];
cout<<"Problem "<< k++ <<": "<< bfs() << endl;
}
return 0;
}
2. 其他例題
類似的題目,練習:poj 1724、poj 1729、hdu 1026。
3 BFS + 雙端隊列
在“簡單數據結構”這一節中,講解了“雙端隊列和單調隊列”。雙端隊列是一種具有隊列和棧性質的數據結構,它能而且只能在兩端進行插入和刪除。雙端隊列的經典應用是實現單調隊列。下面講解雙端隊列在BFS中的應用。
“BFS + 雙端隊列”可以解決一種特殊圖的最短路問題:圖的結點和結點之間的邊權是0或者1。
一般求解最短路,高效的算法是Dijkstra,或者“BFS+優先隊列”,復雜度O((V+E)logV),V是結點數,E是邊數。但是,在這類特殊圖中,用“BFS+雙端隊列”可以在O(V)時間內求得最短路。
雙端隊列的經典應用是單調隊列,“BFS+雙端隊列”的隊列也是一個單調隊列。
下面的例題,詳細解釋了算法。
洛谷 P4667 https://www.luogu.com.cn/problem/P4667
Switch the Lamp On
時間限制150ms;內存限制125.00MB。
題目描述:Casper正在設計電路。有一種正方形的電路元件,在它的兩組相對頂點中,有一組會用導線連接起來,另一組則不會。有 N×M 個這樣的元件,你想將其排列成N行,每行M 個。電源連接到板的左上角。燈連接到板的右下角。只有在電源和燈之間有一條電線連接的情況下,燈才會亮着。為了打開燈,任何數量的電路元件都可以轉動90°(兩個方向)。

在上面的左圖中,燈是關着的。在右圖中,右數第二列的任何一個電路元件被旋轉90°,電源和燈都會連接,燈被打開。現在請你編寫一個程序,求出最小需要多少旋轉多少電路元件。
輸入格式:
輸入的第一行包含兩個整數N和M,表示盤子的尺寸。 在以下N行中,每一行有M個符號\或/,表示連接對應電路元件對角線的導線的方向。 1≤N, M≤500。
輸出格式:
如果可以打開燈,那么輸出一個整數,表示最少轉動電路元件的數量。
如果不可能打開燈,輸出"NO SOLUTION"。
樣例輸入:
3 5
\/\
\///
/\\
樣例輸出:
1
題解:
(1)建模為最短路徑問題
題目可以轉換為最短路徑問題。把起點s到終點t的路徑長度,記錄為需要轉的元件數量。從一個點到鄰居點,如果元件不轉,距離是0,如果需要轉元件,距離是1。題目要求找s到t的最短路徑。樣例的網絡圖如下圖,其中實線是0,虛線是1。

(2)BFS +優先隊列
用上一節的最短路徑算法“BFS+優先隊列”,復雜度是O((V+E)logV)。題目中結點數V = N×M = 250,000,邊數E = 2×N×M = 500,000,O((V+E)logV) ≈ 1.5千萬,題目給的時間限制是150ms,超時。
(3)BFS + 雙端隊列
如果讀者透徹理解“BFS + 優先隊列”的思想,就能知道優先隊列的作用,是在隊列中找到距離起點最短的那個結點,並彈出它。使用優先隊列的原因是,每個結點到起點的距離不同,需要用優先隊列來排序,找最小值。
在特殊的情況下,有沒有更快的辦法找到最小值?
這種特殊情況就是本題,邊權是0或者1。簡單地說,就是:“邊權為0,插到隊頭;邊權為1,插入隊尾”,這樣就省去了排序操作。
下面解釋“BFS + 雙端隊列”計算最短路徑的過程。
1)把起點s放進隊列。
2)彈出隊頭s。擴展s的直連鄰居g,邊權為0的距離最短,直接插到隊頭;邊權為1的直接插入隊尾。在樣例中,當前隊列是:{\(g_0\)},下標記錄結點到起點s的最短距離。
3)彈出隊頭\(g_0\),擴展它的鄰居b、n、q,現在隊列是:{\(q_0,b_1,n_1\)},其中的\(q_0\),因為邊權為0,直接放到了隊頭。g被彈出,表示它到s的最短路已經找到,后面不再進隊。
4)彈出\(q_0\),擴展它的鄰居g、j、x、z,現在隊列是{\(j_0, z_0, b_1, n_1, x_1\)},其中\(j_0\)、\(z_0\)邊權為0,直接放到隊頭。
等等。
下面的表格給出了完整的過程。
步驟 | 出隊 | 鄰居 | 進隊 | 當前隊列 | 最短路 | 說明 |
---|---|---|---|---|---|---|
1 | \(s\) | {\(s\)} | ||||
2 | \(s\) | \(g\) | \(g\) | {\(g_0\)} | \(s\)-\(s\): 0 | |
3 | \(g_0\) | \(s、b、n、q\) | \(b、n、q\) | {\(q_0,b_1,n_1\)} | \(s\)-\(g\): 0 | \(s\)已經進過隊,不再進隊 |
4 | \(q_0\) | \(g、j、x、z\) | \(j、x、z\) | {\(j_0,z_0,b_1,n_1,x_1\)} | \(s\)-\(q\): 0 | \(g\)不再進隊 |
5 | \(j_0\) | \(b、d、q、u\) | \(d、u\) | {\(z_0,b_1,n_1,x_1,d_1,u_1\)} | \(s\)-\(j\): 0 | \(q、b\)已經進過隊,不再進隊 |
6 | \(z_0\) | \(q、u\) | {\(b_1,n_1,x_1,d_1,u_1\)} | \(s\)-\(z\): 0 | \(q、u\)已經進過隊,不再進隊 | |
7 | \(b_1\) | \(g、j\) | {\(n_1,x_1,d_1,u_1\)} | \(s\)-\(b\): 1 | \(g、j\)不再進隊 | |
8 | \(n_1\) | \(g、x\) | {\(x_1,d_1,u_1\)} | \(s\)-\(n\): 1 | \(g、x\)不再進隊 | |
9 | \(x_1\) | \(n、q\) | {\(d_1,u_1\)} | \(s\)-\(x\): 1 | \(n、q\)不再進隊 | |
10 | \(d_1\) | \(j、m\) | \(m\) | {\(m_1,u_1\)} | \(s\)-\(d\): 1 | \(m\)放隊首,但距離是1,\(s-d_1-m_0\) |
11 | \(m_1\) | \(d、u\) | {\(u_1\)} | \(s\)-\(m\): 1 | \(d、u\)不再進隊 | |
12 | \(u_1\) | \(m、z、j、t\) | \(t\) | {\(t_1\)} | \(s\)-\(u\): 1 | \(m、z、j\)不再進隊 |
13 | \(t_1\) | \(u\) | {} | \(s\)-\(t\): 1 | 隊列空,停止 |
注意幾個關鍵:
1)如果允許結點多次進隊,那么先進隊時算出的最短距離,大於后進隊時算出的最短距離。所以后進隊的結點,出隊時直接丟棄。當然,最好不允許結點再次進隊,在代碼中加個判斷即可,代碼中的dis[nx][ny] > dis[u.x][u.y] + d起到了這個作用。
2)結點出隊時,已經得到了它到起點s的最短路。
3)結點進隊時,應該計算它到s的路徑長度再入隊。例如u出隊,它的鄰居v進隊,進隊時,v的距離是s-u-v,也就是u到s的最短距離加上(u,v)的邊權。
為什么“BFS+雙端隊列”的算法過程是正確的?仔細思考可以發現,出隊的結點到起點的最短距離是按0、1、2...的順序輸出的,也就是說,距離為0的結點先輸出,然后是距離為1的結點.....這就是雙端隊列的作用,它保證距離更近的點總在隊列前面,隊列是單調的。
算法的復雜度,因為每個結點只入隊和出隊一次,所以復雜度是O(V),V是結點數量。
下面是代碼[改編自:https://www.luogu.com.cn/blog/hje/solution-p4667],其中的雙端隊列用STL的deque實現。
#include<bits/stdc++.h>
using namespace std;
const int dir[4][2] = {{-1,-1},{-1,1},{1,-1},{1,1}}; //4個方向的位移
const int ab[4] = {2,1,1,2}; //4個元件期望的方向
const int cd[4][2] = {{-1,-1},{-1,0},{0,-1},{0,0}}; //4個元件編號的位移
int graph[505][505],dis[505][505]; //dis記錄結點到起點s的最短路
struct P{
int x,y,dis;
}u;
int Get(){
char c;
while((c=getchar())!='/' && c != '\\') ; //字符不是'/'和'\'
return c=='/'?1:2;
}
int main(){
int n, m; cin >>n >>m;
memset(dis,0x3f,sizeof(dis));
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
graph[i][j] = Get();
deque <P> dq;
dq.push_back((P){1,1,0});
dis[1][1]=0;
while(!dq.empty()){
u = dq.front(), dq.pop_front(); //front()讀隊頭,pop_front()彈出隊頭
int nx,ny;
for(int i=0;i<=3;++i) { //4個方向
nx = u.x+dir[i][0];
ny = u.y+dir[i][1];
int d = 0; //邊權
d = graph[u.x+cd[i][0]][u.y+cd[i][1]]!=ab[i]; //若方向不相等,則d=1
if(nx && ny && nx<n+2 && ny<m+2 && dis[nx][ny]>dis[u.x][u.y]+d){
//如果一個結點再次進隊,那么距離應該更小。實際上,由於再次進隊時,距離肯定更大,所以這里的作用是阻止再次入隊。
dis[nx][ny] = dis[u.x][u.y]+d;
if(d==0) dq.push_front((P){nx, ny, dis[nx][ny]}); //邊權為0,插到隊頭
else dq.push_back ((P){nx, ny, dis[nx][ny]}); //邊權為1,插到隊尾
if(nx==n+1 && ny==m+1) //到終點退出。不退也行,隊列空自動退出
break;
}
}
}
if(dis[n+1][m+1] != 0x3f3f3f3f) //可能無解,即s到t不通
cout << dis[n+1][m+1];
else
cout <<"NO SOLUTION";
return 0;
}
致謝
謝勇,湘潭大學算法競賽教練:討論最短路徑算法的復雜度。
曾貴勝,四川省成都市石室中學信息學競賽教練:討論最短路徑算法。