1. 程式人生 > 其它 >最短路徑——Dijkstra堆優化演算法

最短路徑——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