前言
單源最短路徑是學習圖論算法的入門級臺階,但剛開始看的時候就蒙了,什麼有環沒環,有負權沒負權,下面就來總結一下求單源最短路徑的所有算法以及其適用的情況。
單源最短路徑
設定圖中一個點爲源點,求其他所有點到源點的最短路徑。
先聲明一點:有負環的圖中沒有最短路徑
因爲負環繞一圈的權值和是負的,只要過一遍環,路徑就減小,可以反覆過,無限減小
1. 無環 無負權 圖求單源最短路徑--拓撲排序
求v到其他所有點的最短路徑
歸納假設
已知v到前面n-1個點的最短路徑
求n
假設每一個點帶一個屬性sp,表示該點到源點的最短路徑長度
設第n個點爲z
我們知道所有能到z的點wk,即(wk, z)是圖中的邊
這樣求出來wk.sp + len(wk, z)的最小值
就是z的sp
爲什麼要拓撲排序?
拓撲排序保證看n的時候,與n連接的前面的所有點都看了
複雜度O(V + E)
僞代碼:
const int MAX = MAX_INT
for(all w){
w.sp = MAX
}
init w.indegree DFS
w with 0 indegree, enqueue
while(!queue.empty()){
w = dequeue();
for(all (w, z)){
if(w.sp + len(w, z) < z.sp){
z.sp = w.sp + len(w, z);
}
z.indegree--;
if(z.indegree = 0){
z.enqueue();
}
}
}
2. 有環 無負權 圖求單源最短路徑--Dijkstra算法
前面拓撲排序的求所有點的最短路徑只能是無環圖
有環會導致拓撲排序無法終止
沒法用
Dijkstra算法可以解決有環 無負權最短路徑的問題
Dijkstra算法是求源點到其他所有點的最短路徑
定義兩個集合
一個集合是S, 表示已經確定了最短路徑的點
另一堆是剩下的所有點M的集合
Init-每個點的路徑長度設爲無窮
每次從剩下的點中取路徑長度最小的加入到S中
開始
只有源點在S中
計算源點所有連接點的路徑長度
取出最小x的加入到S中
並對所有連接x的點更新路徑長度
再挑出最小的
反覆
直到所有點都加到S中
解決了
注意:這裏與prim算法有點像
Diljstra算法是不斷從剩下的點中選取路徑最小的加入的S中
prim算法是不斷從剩下的點中選取能到S且邊的權值最小的加入到S中
僞代碼:
const int MAX = MAX_INT;
for(all w except v in G){
w.sp = MAX;
w.mark = false;
}
v.sp = 0;
build a heap for all vetex;
while(there exist unmarked vertex){
w = heap.pop(); //w.sp is minimum
w.mark = true;
for(all (w, z) in G and z is unmarked){
if(w.sp + len(w, z) < z.sp){
z.sp = w.sp + len(w, z);
}
}
heapsort();
}
build the Heap // O(VlogV)
最多需要E次更新,每一次更新O(logv)
從而總時間
O((E+V)logV)
問:爲什麼Dijkstra算法不能求解帶負權的路徑的圖的最短路徑?
答:採用dijkstra算法處理帶有負權邊的圖時有可能出現這樣一種情況:因爲dijktra算法每一次將一個節點加入已訪問集合之中,之後不能在更新,
如圖
算法剛開始執行時,將A添加到集合中標記已訪問,之後選出從A到所有節點中的最短的d,於是把C加入集合中標記已訪問,之後C不能在更新了,而顯然,A與C之間最短路徑權值爲0(A-B-C),發生錯誤。
3. 有環 有負權 圖的單源最短路徑--Floyd算法
可以用於帶負權的圖
Floyd算法
動態規劃
C[i][j] 表示i點到j點的最短距離
小問題可解-所有相鄰的邊的權值已知
大問題分解爲小問題
從i到j
兩種途徑
1.圖中有邊連接i與j
2.經過k點到,C[i][j] = C[i][k] + C[k][j]
1 <= k <= N
兩種的最小值就是C[i][j]
定義矩陣C[N][N]
然後不斷dp
僞代碼:
const int MAX = MAX_INT;
c[N][N]
for(i = 1; i <= N; i++){
for(j = 0; i <= N; j++){
if((i, j) is in G ){
c[i][j] = (i, j).weight;
}
else{
c[i][j] = MAX;
}
}
}
for(k = 1; k <= N; j++){
for(i = 1; i <= N; i++){
for(j = 0; j <= N; j++){
if(c[i][j] > c[i][k] + c[k][j]){
c[i][j] = c[i][k] + c[k][j];
}
}
}
}
複雜度O(n^3)
注意k只能放在最外層的循環
不能放在最裏層
若放在最裏層
計算第一行eg (1, 10)時
要算(1, 5) (5, 10)
但這時(5, 10)還未知
所以確定的(1, 10)點的最短路徑是不準確 的
k放最外面的循環中
保證計算每次看計算C[i][j]時, C[i][k] + C[k][j]都是已經求過的值
4. 有環 有負權 圖單源最短路徑算法--SPFA
帶有負權的圖
不能使用Dijkstra算法
floyd算法又複雜度太高
然後西安交通大學acm大牛想出了SPFA
中華少年英才多啊
SPFA
設立一個隊列
d[i]表示源點到i的最短距離
init所有d[i]都爲無窮
剛開始源點入隊
x出隊
檢查x連接的所有點d[i]值,如果變小,則跟新
更新了的點y,說明與y連接的點也有可能更新
y入隊
之後不斷反覆
因爲點數有限,所以必然一定步驟後隊列會爲空
這時就求得了所有點的最短路徑
真TMD神奇
檢查每個點連接的所有點,總檢查就是邊數E
每個點可能入隊多次,設平均入隊次數爲k
複雜度O(kE)
可以證明k<=2
Spfa 有兩種實現方法:bfs,dfs
bfs判斷起來較麻煩
得計算所有點入隊的次數
如果一個點入隊超過N次,一定有負環
bfs僞代碼:
const int MAX = MAX_INT
spfa_bfs(){
suppose root = s;
d[N]; //The min path length of s to i
vis[N]; //1 -- i is in the queue
count[N]; //The count of i to enqueue
for(i = 1; i <= N; i++){
d[N] = MAX;
}
d[s] = 0;
vis[s] = 1;
count[s] = 1;
enqueue(s);
while(!queue.empty()){
x = dequeue();
vis[s] = 0;
for((x, y) is in G){
if(d[y] > d[x] + len(x, y)){
d[y] = d[x] + len(x, y);
vis[y] = 1;
count[y]++;
if(count[y] >= N){
return -1; // There is a minus circle
}
}
}
}
}
dfs 僞代碼:
判斷負環較快
vis[N]; //1 -- i is checked
int spfa_dfs(point v){
vis[v] = 1;
for((v, w) is in G){
if(d[w] > d[v] + len(v, w)){
d[w] = d[v] + len(v, w);
if(vis[w] == 0){
if(spfa_dfs(w) == -1){
return -1;
} //-1 there is a minus circle
}
else{
return -1;
}
}
}
vis[v] = 0;
}
總結
無環 無負權 圖單源最短路徑--拓撲排序
有環 無負權 圖單源最短路徑--Dijkstra算法
有環 有負權 圖單源最短路徑--Floyd算法
有環 有負權 圖單源最短路徑--SPFA
這裏總結了計算單源最短路徑的四種常見算法,理清楚他們分別適用的情況,以及他們之間的關係便會很好記憶。