最短路徑——Dijkstra堆優化演算法
最短路徑——Dijkstra演算法
轉自:https://www.cnblogs.com/fusiwei/p/11390537.html
DIJ演算法的堆優化
DIJ演算法的時間複雜度是O(n2)的,在一些題目中,這個複雜度顯然不滿足要求。所以我們需要繼續探討DIJ演算法的優化方式。
堆優化的原理
堆優化,顧名思義,就是用堆進行優化。我們通過學習樸素DIJ演算法,明白DIJ演算法的實現需要從頭到尾掃一遍點找出最小的點然後進行鬆弛。這個掃描操作就是坑害樸素DIJ演算法時間複雜度的罪魁禍首。所以我們使用小根堆,用優先佇列來維護這個“最小的點”。從而大大減少DIJ演算法的時間複雜度。
堆優化的程式碼實現
說起來容易,做起來難。
我們明白了上述的大體思路之後,就可以動手寫這個程式碼,但是我們發現這個程式碼有很多細節問題沒有處理。
首先,我們需要往優先佇列中push最短路長度,但是它一旦入隊,就會被優先佇列自動維護離開原來的位置,換言之,我們無法再把它與它原來的點對應上,也就是說沒有辦法形成點的編號到點權的對映。
我們用pair解決這個問題。
pair是C++自帶的二元組。我們可以把它理解成一個有兩個元素的結構體。更刺激的是,這個二元組有自帶的排序方式:以第一關鍵字為關鍵字,再以第二關鍵字為關鍵字進行排序。所以,我們用二元組的first位存距離,second位存編號即可。
然後我們發現裸的優先佇列其實是大根堆,我們如何讓它變成小根堆呢?
有兩種方法,第一種是把第一關鍵字取相反數,取出來的時候再取相反數。第二種是重新定義優先佇列:
priority_queue<int,vector<int>,greater<int> >q;
解決了這些問題,我們愉快地繼續往下寫,後來我們發現,寫到鬆弛的時候,我們很顯然要把鬆弛後的新值也壓入優先佇列中去,這樣的話,我們又發現一個問題:優先佇列中已經存在一個同樣編號的二元組(即第二關鍵字相同),我們沒有辦法刪去它,也沒有辦法更新它。那麼在我們的佇列和程式執行的時候,一定會出現bug。
怎麼辦呢??
我們在進入迴圈的時候就開始判斷:如果有和堆頂重複的二元組,就直接pop掉,成功維護了優先佇列元素的不重複。
所以我們得到了堆優化的程式碼:
priority_queue<pair<int,int> >q;
void dijkstra(int start)
{
memset(dist,0x3f,sizeof(dist));
memset(v,0,sizeof(v));
dist[start]=0;
q.push(make_pair(0,start));
while(!q.empty())
{
while(!q.empty() && (-q.top().first)>dist[q.top().second])
q.pop();
if(!q.empty())
return;
int x=q.top().second;
q.pop();
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(dist[y]>dist[x]+val[i])
{
dist[y]=dist[x]+val[i];
q.push(make_pair(-dist[y],y));
}
}
}
}
UPD:2020.10.28
現在又回頭來看這個模板,還是覺得很麻煩的。至少很多東西實現的時候很是繁瑣。
其實我們完全可以使用標記陣列來避免重複關鍵字的多次更新。
所以我們得到了新的Dijkstra模板。
void dijkstra()
{
memset(dist,127,sizeof(dist));
dist[1]=0;
q.push(make_pair(0,1));
while(!q.empty())
{
int x=q.top().second;
q.pop();
if(v[x])
continue;
v[x]=1;
for(int i=head[x];i;i=nxt[i])
{
int y=to[i];
if(dist[y]>dist[x]+val[i])
{
dist[y]=dist[x]+val[i];
q.push(make_pair(-dist[y],y));
}
}
}
}
思考
以下為筆者自己的思考
參考《挑戰程式設計競賽》中的程式碼,寫出程式碼如下,在優先順序佇列中沒有使用pair而是定義了結構體Point,並重載了小於號
const int MAXN = 101;
const int MAXM = 3010;
const int INF = 1e5+10;
struct Edge{
int to;
int length;
Edge(int t, int l): to(t), length(l) {}
};
vector<Edge> graph[MAXN*MAXN];
struct Point{
int num; //頂點編號v
int length; //dist[v]
Point(int n, int l): num(n), length(l) {}
bool operator < (const Point& p) const{
return length > p.length;
}
};
int dist[MAXN];
void Dijkstra(int start){
//初始化
fill(dist, dist + MAXN, INF);
dist[start] = 0;
//預設是大根堆,由於Point結構體過載了小於號,定義dist小的優先順序高
//因此,dist小的會被調整在堆頂
priority_queue<Point> q;
q.push(Point(start, dist[start]));
while(!q.empty()){
Point p = q.top(); //獲得堆頂元素
q.pop();
int u = p.num; //u為堆頂元素的結點編號
//!!!注1
if(dist[u] < p.length){
continue;
}
//遍歷u的所有臨界邊
for(int i = 0; i < graph[u].size(); ++i){
Edge e = graph[u][i];
//鬆弛操作
if(dist[u] + e.length < dist[e.to]){
dist[e.to] = dist[u] + e.length;
q.push(Point(e.to, dist[e.to]));
}
}
}
}
注1:
在初讀時,不能理解演算法下面這條if語句的作用
if(dist[u] < p.length){
continue;
}
直到讀到了上面這位大佬的部落格
寫到鬆弛的時候,我們很顯然要把鬆弛後的新值也壓入優先佇列中去,這樣的話,我們又發現一個問題:優先佇列中已經存在一個同樣編號的二元組(即第二關鍵字相同)
優先順序佇列中記錄的頂點的編號及其在dist中的值,對於一個頂點,在進行疏鬆操作之後,要將其壓入到優先順序佇列中,但是在優先順序佇列中可能存在該頂點此次疏鬆操作之前的記錄。在僅僅只求最短路徑長度的情況下,不考慮這一點貌似也不會出現問題。但是當求最短路徑的條數時,如果忽略這點可能會出現問題,用下面的列子來理解一下。
根據堆優化的最短路徑Dijkstra演算法,新增num陣列即可求得源點到各頂點最短路徑的條數。
初始時num[start] = 1,num[v] = 0 (v = {V - start})
若dist[u] + w(u,v) < dist[v],則num[v] = num[u]
若dist[u] + w(u,v) = dist[v],則num[v] += num[u]
程式碼如下
const int MAXN = 101;
const int MAXM = 3010;
const int INF = 1e5+10;
struct Edge{
int to;
int length;
Edge(int t, int l): to(t), length(l) {}
};
vector<Edge> graph[MAXN*MAXN];
struct Point{
int num; //頂點編號v
int length; //dist[v]
Point(int n, int l): num(n), length(l) {}
bool operator < (const Point& p) const{
return length > p.length;
}
};
int dist[MAXN];
int num[MAXN];
void Dijkstra(int start){
//初始化
fill(dist, dist + MAXN, INF);
memset(num, 0, sizeof(num));
dist[start] = 0;
num[start] = 1;
priority_queue<Point> q;
q.push(Point(start, dist[start]));
while(!q.empty()){
Point p = q.top();
q.pop();
int u = p.num;
//注1
/*if(dist[u] < p.length){
continue;
}*/
for(int i = 0; i < graph[u].size(); ++i){
Edge e = graph[u][i];
if(dist[u] + e.length < dist[e.to]){
dist[e.to] = dist[u] + e.length;
q.push(Point(e.to, dist[e.to]));
num[e.to] = num[u];
}
else if(dist[u] + e.length == dist[e.to]){
num[e.to] += num[u];
}
}
}
}
當沒有考慮上述問題時(即註釋掉注1
的程式碼),現在我們考慮下面的輸入樣例
4 4
1 2 10
1 3 20
2 3 5
3 4 15
1 | 2 | 3 | 4 | |
---|---|---|---|---|
初始 | (0,1) | (inf,0) | (inf,0) | (inf,0) |
鬆弛1 | (10,1) | (20,1) | (inf,0) | |
鬆弛2 | (15,1) | (inf,0) | ||
鬆弛3(15,1) | (30,1) | |||
鬆弛3(20,1) | (30,2) |
當進行鬆弛3
的操作時,優先順序佇列中有兩條頂點3的記錄
- 鬆弛頂點1時,push進優先順序佇列的頂點3記錄(20,1)
- 鬆弛頂點2時,push進優先順序佇列的頂點3記錄(15,1)
因此,在鬆弛頂點3時,會進行兩次鬆弛操作,第一次確定dist[4]=30,num[4]=1,第二次會導致num[4]=2,顯然出現了錯誤。
解決方法即新增注1
程式碼
if(dist[u] < p.length){
continue;
}
每當從優先順序佇列中選取堆頂元素之後進行判斷,若取出來的堆頂記錄中dist[v](即p.length)大於當前dist[v],則此頂點是重複出現的頂點,直接continue即可
實戰
https://acm.ecnu.edu.cn/problem/1818/
在最短路徑長度的基礎上輸出最短路徑的條數
若沒有考慮注1
,提交程式碼後第二個測試點錯誤
可以執行第二個測試點,輸出最短路徑條數為3,而正確答案應該是1
考慮到注1
,新增if條件,再次提交,AC