本篇博客是我對《算法競賽入門經典》一書上內容的理解和提煉,代碼以及邏輯都是源於此書,若想更細緻的學習,還請移步原作。
注:Dijkstra算法適用於邊權爲正的無向和有向圖,不適用於有負邊權的圖!(原因)
用途:
用於求圖中指定兩點之間的最短路徑,或者是指定一點到其它所有點之間的最短路徑。實質上是貪心算法。
基本思想:
1.將圖上的初始點看作一個集合S,其它點看作另一個集合
2.根據初始點,求出其它點到初始點的距離d[i] (若相鄰,則d[i]爲邊權值;若不相鄰,則d[i]爲無限大)
3.選取最小的d[i](記爲d[x]),並將此d[i]邊對應的點(記爲x)加入集合S
(實際上,加入集合的這個點的d[x]值就是它到初始點的最短距離)
4.再根據x,更新跟 x 相鄰點 y 的d[y]值:d[y] = min{ d[y], d[x] + 邊權值w[x][y] },因爲可能把距離調小,所以這個更新操作叫做鬆弛操作。
(仔細想想,爲啥只更新跟x相鄰點的d[y],而不是更新所有跟集合 s 相鄰點的 d 值? 因爲第三步只更新並確定了x點到初始點的最短距離,集合內其它點是之前加入的,也經歷過第 4 步,所以與 x 沒有相鄰的點的 d 值是已經更新過的了,不會受到影響)
5.重複3,4兩步,直到目標點也加入了集合,此時目標點所對應的d[i]即爲最短路徑長度。
(注:重複第三步的時候,應該從所有的d[i]中尋找最小值,而不是隻從與x點相鄰的點中尋找。想想爲什麼?)
圖解:(動圖很快,不容易理解,最好結合上面的步驟自己畫一個圖,一步一步消化)
原理:這裏不進行嚴格證明,Dijkstra的大致思想就是,根據初始點,挨個的把離初始點最近的點一個一個找到並加入集合,集合中所有的點的d[i]都是該點到初始點最短路徑長度,由於後加入的點是根據集合S中的點爲基礎拓展的,所以也能找到最短路徑。
僞代碼:
清除所有點的標號;
設d[0]=0,其他d[i]=INF;//INF是一個很大的值,用來替代正無窮
循環n次 {
在所有未標號結點中,選出d值最小的結點x;
給結點x標記;
對於從x出發的所有邊(x,y),更新d[y] = min{d[y], d[x]+w(x,y)}
}
實現代碼:(C++)
以下算法用於求所有點到初始點的最短距離(保存在d[i]中),n是節點數,m是邊的數量。
memset(v, 0, sizeof(v));
for(int i = 0; i < n; i++) d[i] = (i==0 ? 0 : INF);
for(int i = 0; i < n; i++) {
int x, m = INF;
//3.如果y沒有被加入集合,且d[y]是最小的,則把y加入集合且x = y
for(int y = 0; y < n; y++)
if(!v[y] && d[y] <= m) m = d[y], x = y;
v[x] = 1; //新的點加入集合(這是更新之後的新x)
//4.更新x相鄰的點的d[i],實際上這裏更新的是所有點,但是與x未相鄰的w[x][y]值是無窮大,不可能被更新
for(int y = 0; y < n; y++) d[y] = min(d[y], d[x] + w[x][y]);
}
稍微修改一下,便可以把路徑存儲起來(將當前點y的前一個點x存儲在p[y]中),之後就方便遞歸打印了。
將d[y] = min(d[y], d[x] + w[x][y])改成:
if(d[x] + w[x][y] < d[y]) {
d[y] = d[x] + w[x][y];
p[y] = x;
類似於d[y] = min(d[y], d[x] + w[x][y])這種操作,它每次都可能把距離更新成更小的值,所以這類操作又叫作”鬆弛操作“。
優化代碼:(vector & 鄰接表)
上述代碼的複雜度爲O(n^2),這裏可以用鄰接表將其優化爲O(mlogn),之所以爲優化,是因爲m往往遠小於n^2。爲了方便,我們把邊封裝在結構體中:
struct Edge {
int from, to, dist;
Edge(int u, int v, int d):from(u),to(v),dist(d) {}
}
又因爲Dijkstra算法中,每次循環需要提取出最小d[i]對應的點,所以這裏可以用到優先隊列 priority_queue,隊列內部是根據d值進行排序的,又需要d值對應的點的信息(u),所以這裏可以用另一個結構體存儲放在優先隊列裏的元素(d相當於d[i],u相當於i)。
struct DistNode {
int d, u;
bool operator < (const HeapNode& rhs) const {
return d > rhs.d; //這樣一來,隊列中在最頂層的是最小值
}
}
接下來我們再建立一個統一的鄰接表數據結構,用於接收數據並構造鄰接表,並能Dijkstra。
struct Dijkstra {
int n, m;
vector<Edge> edges;//相當於數組r[i],用於存儲每條邊
vector<int> G[maxn];//鄰接表,只需存儲邊的標號即可
book done[maxn];//用於判斷是否已經處理過該節點
int d[maxn];距離
int p[maxn];上一條弧
//清除垃圾數據,初始化鄰接表和deges
void setn(int n) {
this->n = n;
for(int i = 1; i <= n; i++) G[i].clear();
edges.clear();
}
//添加一條邊
void addedge(int from, int to , int dist) {
edges.push_back(Edge(from, to, dist));
m = edges.size();
G[from].push_back(m - 1);
}
//Dijkstra
void dijkstra(int s) {
}
}
接下來實現dijkstra函數:
void dijkstra(int s) {
priority_queue<DistNode> Q;
Q.push_back(DistNode{0, s});
for(int i = 1; i <= n; i++) d[i] = INF;//把d[i]都設置爲最大值
d[s] = 0;
memset(done, 0, sizeof(done));
while(!Q.empty()) {
DistNode x = Q.top(); Q.pop();
int u = x.u;
if(done[u]) continue;//如果這個點已經被提出過了,直接拋棄(適用於那種鬆弛之後重複放入隊列的點)
done[u] = true;
for(int i = 0; i < Q[u].size(); i++)
Edge& e = edges(Q[u][i]);
if(e.dist + d[u] < d[e.to] && d[u] < INF) {
d[e.to] = e.dist + d[u];
p[e.to] = G[u][i];
Q.push_back(Edge(d[e.to], e.to));//把鬆弛過後點的d值重新放入隊列
}
}
}
}