前言
- \(SPFA\)算法由於它上限 \(O(NM) = O(VE)\)的時間復雜度,被卡掉的幾率很大.在算法競賽中,我們需要一個更穩定的算法:\(dijkstra\).
什么是\(dijkstra\)?
- \(dijkstra\)是一種單源最短路徑算法,時間復雜度上限為\(O(n^2)\)(朴素),在實際應用中較為穩定\(;\)加上堆優化之后更是具有\(O((n+m)\log_{2}n)\)的時間復雜度,在稠密圖中有不俗的表現.
\(dijkstra\)的原理/流程?
- \(dijkstra\)本質上的思想是貪心,它只適用於不含負權邊的圖.
- 我們把點分成兩類,一類是已經確定最短路徑的點,稱為"白點",另一類是未確定最短路徑的點,稱為"藍點"
- \(dijkstra\)的流程如下\(:\)
- \(1.\) 初始化\(dis[start] = 0,\)其余節點的\(dis\)值為無窮大.
- \(2.\) 找一個\(dis\)值最小的藍點\(x,\)把節點\(x\)變成白點.
- \(3.\) 遍歷\(x\)的所有出邊\((x,y,z),\)若\(dis[y] > dis[x] + z,\)則令\(dis[y] = dis[x] + z\)
- \(4.\) 重復\(2,3\)兩步,直到所有點都成為白點\(.\)
- 時間復雜度為\(O(n^2)\)
\(dijkstra\)為什么是正確的
- 當所有邊長都是非負數的時候,全局最小值不可能再被其他節點更新.所以在第\(2\)步中找出的藍點\(x\)必然滿足\(:dis[x]\)已經是起點到\(x\)的最短路徑\(.\)我們不斷選擇全局最小值進行標記和拓展,最終可以得到起點到每個節點的最短路徑的長度
圖解
- (令\(start = 1\))
- 開始時我們把\(dis[start]\)初始化為\(0\),其余點初始化為\(inf\)
- 第一輪循環找到\(dis\)值最小的點\(1\),將\(1\)變成白點,對所有與\(1\)相連的藍點的\(dis\)值進行修改,使得\(dis[2]=2,dis[3]=4,dis[4]=7\)
- 第二輪循環找到\(dis\)值最小的點\(2\),將\(2\)變成白點,對所有與\(2\)相連的藍點的\(dis\)值進行修改,使得\(dis[3]=3,dis[5]=4\)
- 第三輪循環找到\(dis\)值最小的點\(3\),將\(3\)變成白點,對所有與\(2\)相連的藍點的\(dis\)值進行修改,使得\(dis[4]=4\)
- 接下來兩輪循環分別將\(4,5\)設為白點,算法結束,求出所有點的最短路徑
- 時間復雜度\(O(n^2)\)
為什么\(dijkstra\)不能處理有負權邊的情況?
- 我們來看下面這張圖
- \(2\)到\(3\)的邊權為\(-4\),顯然從\(1\)到\(3\)的最短路徑為\(-2\) \((1->2->3).\)但在循環開始時程序會找到當前\(dis\)值最小的點\(3\),並標記它為白點.
- 這時的\(dis[3]=1,\)然而\(1\)並不是起點到\(3\)的最短路徑.因為\(3\)已經被標為白點,所以\(dis[3]\)不會再被修改了.我們在邊權存在負數的情況下得到了錯誤的答案.
\(dijkstra\)的堆優化?
-
觀察\(dijkstra\)的流程,發現步驟\(2\)可以優化
-
怎么優化呢?
-
我會zkw線段樹!我會斐波那契堆! -
我會堆!
-
我們可以用堆對\(dis\)數組進行維護,用\(O(\log_{2}n)\)的時間取出堆頂元素並刪除,用\(O(\log_{2}n)\)遍歷每條邊,總復雜度\(O((n+m)\log_{2}n)\)
-
范例代碼:
#include<bits/stdc++.h>
const int MaxN = 100010, MaxM = 500010;
struct edge
{
int to, dis, next;
};
edge e[MaxM];
int head[MaxN], dis[MaxN], cnt;
bool vis[MaxN];
int n, m, s;
inline void add_edge( int u, int v, int d )
{
cnt++;
e[cnt].dis = d;
e[cnt].to = v;
e[cnt].next = head[u];
head[u] = cnt;
}
struct node
{
int dis;
int pos;
bool operator <( const node &x )const
{
return x.dis < dis;
}
};
std::priority_queue<node> q;
inline void dijkstra()
{
dis[s] = 0;
q.push( ( node ){0, s} );
while( !q.empty() )
{
node tmp = q.top();
q.pop();
int x = tmp.pos, d = tmp.dis;
if( vis[x] )
continue;
vis[x] = 1;
for( int i = head[x]; i; i = e[i].next )
{
int y = e[i].to;
if( dis[y] > dis[x] + e[i].dis )
{
dis[y] = dis[x] + e[i].dis;
if( !vis[y] )
{
q.push( ( node ){dis[y], y} );
}
}
}
}
}
int main()
{
scanf( "%d%d%d", &n, &m, &s );
for(int i = 1; i <= n; ++i)dis[i] = 0x7fffffff;
for( register int i = 0; i < m; ++i )
{
register int u, v, d;
scanf( "%d%d%d", &u, &v, &d );
add_edge( u, v, d );
}
dijkstra();
for( int i = 1; i <= n; i++ )
printf( "%d ", dis[i] );
return 0;
}
例題
- 入門模板:P3371
- 進階模板:P4779
- 其余例題請右轉洛谷題庫,搜索"最短路"
后記
- 本文部分內容摘自李煜東《算法競賽進階指南》和《信息學競賽一本通》
- 友情提示:正權圖請使用\(dijkstra\)算法,負權圖請使用\(SPFA\)算法