Dijkstra算法及其堆優化代碼詳解

迪傑斯特拉算法解決的是帶權重的有向圖上單源最短路徑問題,該算法要求所有邊的權重都爲非負值,其在運行過程中維持的關鍵信息是一組節點集合S。算法重複從結點集V-S中選擇最短路徑估計最小的結點u,將u加入到集合S,然後對所有從u發生的邊進行鬆弛,運行結束後,從源節點到集合S中每個結點之間的最短路徑已經被找到。

下面,通過一個實例講解該過程!

1 實例講解

在這裏插入圖片描述
如圖,是一個有向無環圖,假定出發點爲V1,迪傑斯特拉算法將算出V1到其他所有點的最短路徑,則所求V1到終點的最短路徑也可得到,該算法主要完成以下幾步:

  1. 找到V1
  2. 得到以V1出發的鄰接點的最短距離,將V1加到S集合中(代碼中通過vis數組標記)
  3. 從S集合之外的點中找到距離最短者,對以其爲出發點的鄰接點進行鬆弛操作,若距離被更新,則記錄前驅
  4. 重複3,直到所有點被S集合收錄
  5. 完成後,將得到V1到所有點的最短距離,同時,通過每一個點記錄的前驅得到最短路徑

1.1 問題:

  1. 鬆弛操作是啥?

鬆弛操作意味着比起原來的路徑,找到了一條距離更短的路,則將原來點的距離更新爲新的距離。注意本文中某個點的距離全部指的是從出發點即V1到該點的距離。代碼如下:

 if(dis[j]>dis[k]+map[k][j])
 {
      dis[j]=dis[k]+map[k][j];
      path[j]=k;
 }
  1. 爲啥每個點記錄前驅能用於V1到所有終點?

我的理解是最短路是由最短路+某一條固定路組成,所以前驅適用全部點,比如該圖中V1到V7的最短路徑爲V1->V2->V3->V5->V6->V7,因爲V6->V7的距離固定爲50,所以V7的最短路徑中V1->V6的一段必然是V1->V6的最短路徑,因此每個結點只需記錄一個前驅。要想打印出路徑,從終點開始一次次找前驅即可,可通過遞歸實現。代碼如下:

void print(int x)//x爲終點
{
    if(x == -1) return;
    //遞歸
    print(path[x]);
    printf("%d->",x);
}

1.2 算法過程

程序運行過程中,數據的更新情況如圖所示:
在這裏插入圖片描述
畫這個破圖累死了,紅色數據代表每次迭代中被更新的數據,下標代表了結點前驅。由上圖可得,當所有結點加入S後,就得到了V1到所有結點的最短距離和最短路徑,例如V1到V7的最短距離爲130,V7的前驅爲V6,V6的前驅爲V5,V5的前驅爲V3,V3的前驅爲V2,V2的前驅爲V1,則V1到V7的最短路徑爲V1->V2->V3->V5->V6->V7。

2 代碼詳解

以下爲上述實例的完整代碼,代碼中詳細註釋了該算法中關鍵操作以及容易被誤解的地方。

#include <iostream>
#include <cstring>
#include <cstdio>

/*問題描述:
 * 輸入n和m,代表n個節點,m條邊,然後是m行輸入,每行有x,y,z,代表x到y的路距離爲z。
 * 問題:從1出發到各點的最短路徑。
 * 測試樣例:
7 12
1 2 20
1 3 50
1 4 30
2 3 25
2 6 70
3 4 40
3 6 50
3 5 25
4 5 55
5 6 10
5 7 70
6 7 50
 */
using namespace std;
const int maxn = 100;
int map[maxn][maxn];
int dis[maxn];
int path[maxn];
int vis[maxn];//記錄更新過的點
int n;
void dijk(int s)
{
    //初始化
    memset(path,-1,sizeof(path));
    /*INF使用0x3f3f3f3f的好處:
     * 1:滿足無窮大加一個有窮的數依然是無窮大(在DijKstra算法鬆弛操作中避免了溢出而出現負數)
     * 2:滿足無窮大加無窮大依然是無窮大(兩個0x3f3f3f3f相加並未溢出)
     * 3:初始化時,由於每一個字節爲0x3f,所以只需要memset(buf,0x3f,sizeof(buf))即可
     */
    memset(dis,0x3f,sizeof(dis)); //初始化爲無窮大
    memset(vis,0,sizeof(vis));
    dis[s] = 0; //自身到自身的距離爲0
    while(1)
    {
        int k = 0;
        for(int j = 1; j <= n; j++)
        {
            if(!vis[j]&&dis[j]<dis[k])//找未收錄頂點中dis值最小的
                k=j; //這裏第一次找到的是起點
        }
        if(!k) return; //沒有未收錄的點,則返回
        vis[k] = 1;
        //鬆弛操作
        for(int j = 1; j <= n; j++)
        {
            //第一次循環只有起點的鄰接點距離被更新,每次都更新新找到的點的鄰接點
            if(dis[j]>dis[k]+map[k][j])
            {
                dis[j]=dis[k]+map[k][j];
                path[j]=k;//路徑被改變,重新記錄前驅,最短路是由最短路+某一條固定路組成,所以前驅是有效的
            }
        }
    }
}

void print(int x)//x爲終點
{
    if(x == -1) return;
    //遞歸
    print(path[x]);
    printf("%d->",x);
}
int main()
{
    int m,x,y,z,order;
    scanf("%d%d",&n,&m);
    memset(map,0x3f,sizeof(map));
    for(int i = 0; i < m; i++)
    {
        scanf("%d%d%d",&x,&y,&z);
        map[x][y] = z;
    }
    dijk(1);
    scanf("%d",&order);//order爲終點
    print(path[order]);
    printf("%d\n",order);
    //打印最短距離
    printf("%d\n",dis[order]);
    return 0;
}

代碼之所以能這麼寫,是因爲初始化的時候置不連通的點之間的距離爲INF,在尋找最短路徑估計最小的結點u以及在是否鬆弛的判斷中這點至關重要,請讀者細細體會。

補充:基於堆優化的dijkstra算法代碼 可以把時間複雜度從O(v^2)降到O(elogv)。

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1e2 + 5;

//基於堆優化的dijkstra算法
struct edge{
    int to;//終點
    int cost;//權重
    edge(int x, int y):to(x), cost(y){}
};
typedef pair<int, int> P;//first是最短距離 second是頂點的編號
vector<edge> graph[MAXN];//鄰接表
int dist[MAXN];//距離
int path[MAXN];//記錄前驅
void dijkstra(int s){
    //初始化前驅
    memset(path,-1,sizeof(path));
    //默認對pair.first的大小進行排序 從小到大
    priority_queue<P, vector<P>, greater<P>> que;
    //初始化距離
    memset(dist, 0x3f, sizeof(dist));
    dist[s] = 0;
    que.push(P(0,s));//把起點壓入隊列
    while(!que.empty()){
        P p = que.top();
        que.pop();
        //頂點的編號
        int v = p.second;
        //dist[v]經過鬆弛操作變小,壓入堆中的路徑失去價值
        if(dist[v] < p.first) continue;
        //利用最短邊進行鬆弛
        for(int i= 0; i < graph[v].size(); ++i){
            edge e = graph[v][i];
            if(dist[e.to] > dist[v] + e.cost){
                dist[e.to] = dist[v] + e.cost;
                que.push(P(dist[e.to],e.to));
                path[e.to] = v;
            }
        }
    }
}
//打印路徑
void print(int x)//x爲終點
{
    if(x == -1) return;
    //遞歸
    print(path[x]);
    printf("%d->",x);
}
int main()
{
    //讀入數據
    int m, n, x, y, z;
    int order;
    scanf("%d%d", &n, &m);
    for(int i = 0; i < m; ++i){
        scanf("%d%d%d",&x,&y,&z);
        graph[x].push_back(edge(y, z));
    }
    dijkstra(1);
    scanf("%d",&order);//order爲終點
    print(path[order]);
    printf("%d\n",order);
    //打印最短距離
    printf("%d\n",dist[order]);
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章