數據結構-圖的最短路徑之Djikstra算法(迪傑斯特拉算法)


一. Djikstra算法定義

  1. 形式:用來解決單源最短路徑的問題,即給出圖G和起點s,通過算法到達每個頂點的最短距離。
  2. 基本思想: 對圖G(V, E)設置集合S, 存放已被訪問的頂點,然后每次從集合V-S中選擇與起點s的最短距離最小的一個頂點u,訪問並加入集合S。之后,令頂點u為中介點, 優化起點和所有的從u能到達的頂點v之間的最短距離。這樣的操作執行n(頂點的個數)次。
  3. 偽代碼:
//G為圖, 一般設置為全局變量,數組d為源點到達各點的最短路徑長度,s為起點
Djikstra(G, d[], s){
    初始化
    for(循環n次){
        u = 是d[u]最小的還未被訪問的頂點的標號
        記u已被訪問
        for(從u出發能到達的所有頂點v){
            if(v未被訪問&&以U為中介使得s到頂點v的最短距離d[v]更優){
                優化d[v]
            }
        }
    }
}

二、具體實現

1. 鄰接矩陣版
const int MAXV = 1000;//最大頂點數
const int INF = 10000000000;//設INF為一個很大數

//適用於點數不大的情況
int n, G[MAXV][MAXV];
int d[MAXV];
bool vis[MAXV];

void Dijkstra(int s){
    fill(d, d+MAXV, INF);
    d[s] = 0;
    for(int i = 0; i < n; i++){
        int u = -1. MIN = INF;//u使d[u]最小, MIN存放該最小d[u]
        for(int j = 0; j < n; j++){
            if(vis[j] == false && d[j] < MIN){
                u = j;
                MIN = d[j];
            }
        }
        //找不到小於INF的d[u],說明剩下的頂點與s不連通
        if(u == -1) return;
        vis[u] = true;//標記u為已訪問
        for(int v = 0; v < n; v++){
            //如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
            if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
                d[v] = d[u] + G[u][v];//優化d[v]
            }
        }
    }
}
2. 鄰接表版
struct node{
    int v, dis;//v為邊的目標頂點,dis為邊權
};

vector<node> Adj[MAXV];
int n;
int d[MAXn];
bool vis[MAXV] = {false};

void Dijkstra(int s){
    fill(d, d+MAXV, INF);
    d[s] = 0;
    for(int i = 0; i < n; i++){
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++){
            if(vis[j] == false && d[j] < MIN){
                u = j;
                MIN = d[j];
            }
        }
        if(u == -1) return;
        vis[u] = true;
        //只有這個部分與鄰接矩陣自而發不同
        for(int j = 0; j < Adj[u].size(); j++){
            int v = Adj[u][j].v//通過鄰接表直接獲得u能夠到達的v
            if(vis[v] == false && d[u] + Adj[u][j].dis < d[v]){
                d[v] = d[u] + Adj[u][j].dis;
            }
        }
    }
}
3. 最短路徑的求法(在上面的基礎上)
  • 如果題目中給出的是無向圖,那么根據鄰接表和鄰接矩陣自行改變即可,如果是鄰接矩陣可以在兩邊同時加上一樣的權值;如果是鄰接表那么使用push_back,也是同時交換輸入。
<1>以鄰接矩陣為例
const int MAXV = 1000;//最大頂點數
const int INF = 10000000000;//設INF為一個很大數

//適用於點數不大的情況
int n, G[MAXV][MAXV];
int d[MAXV];
bool vis[MAXV];
int pre[MAXV];//表示從起點到頂點v的最短路徑上v的前一個頂點(新添加)
void Dijkstra(int s){
    fill(d, d+MAXV, INF);
    d[s] = 0;
    for(int i = 0; i < n; i++){
        int u = -1. MIN = INF;//u使d[u]最小, MIN存放該最小d[u]
        for(int j = 0; j < n; j++){
            if(vis[j] == false && d[j] < MIN){
                u = j;
                MIN = d[j];
            }
        }
        //找不到小於INF的d[u],說明剩下的頂點與s不連通
        if(u == -1) return;
        vis[u] = true;//標記u為已訪問
        for(int v = 0; v < n; v++){
            //如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
            if(vis[v] == false && G[u][v] != INF && d[u] + G[u][v] < d[v]){
                d[v] = d[u] + G[u][v];//優化d[v]
                //就是在這里改變,添加了一條語句
                pre[v] = u;//新添加
            }
        }
    }
}

//如何打印,就是遞歸打印
void dfs(int s, int v){
    if(v == s){
        printf("%d\n", s);
        return;
    }
    dfs(s, pre[v]);
    printf("%d\n", v);
}
4. 會出現的其他情況,即附加條件
  • 即出現除了第一個條件最短路徑外,可能還會有其他的條件限制,如有多條路徑
  • 每條邊增加一個邊權(花費);
  • 每個點增加一個點權(如每個城市可以收到的物資);
  • 直接問有多少條最短路徑。
<1>新增邊權。以新增邊權花費為例,都是在最后判斷出進行修改,其余均不需改動,因為是新增邊權,所以在存儲上和原來的最短路徑是一樣的,就是新開辟數組cost[][],然后設立數組c,用於存儲最小花費。初始化c[s]=0,其余都初始化為c[u]=INF.
        for(int v = 0; v < n; v++){
            //如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
            if(vis[v] == false && G[u][v] != INF){
                if(d[u] + G[u][v] < d[v]){
                     d[v] = d[u] + G[u][v];//優化d[v]
                     c[v] = c[u] + cost[u][v];
                }else if(d[u] + G[u][v] == d[v] && c[u] + cost[u][v] < c[v]){
                    c[v] = c[u] + cost[u][v];
                }
            }
        }
<2>新增點權,以新增的點權代表城市中能收集到的物資為例,用weight[u]表示城市u中物資數目,並增加一個數組w[],令其為起點s到到達頂點u可以收集的最大物資w[u].初始化w[s] = weight[s], 其余w[u] = 0.
        for(int v = 0; v < n; v++){
            //如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
            if(vis[v] == false && G[u][v] != INF){
                if(d[u] + G[u][v] < d[v]){
                     d[v] = d[u] + G[u][v];//優化d[v]
                     w[v] = w[u] + weight[v];
                }else if(d[u] + G[u][v] == d[v] && w[u] + weight[v] > w[v]){
                    w[v] = w[u] + weight[v];
                }
            }
        }
<3>求最短路徑的條數。只需要增加一個數組num[],初始時num[s] = 1,其余都為num[u] = 0.
        for(int v = 0; v < n; v++){
            //如果v未訪問&&u能夠到達v&&以u為中介點可以使d[v]更優
            if(vis[v] == false && G[u][v] != INF){
                if(d[u] + G[u][v] < d[v]){
                     d[v] = d[u] + G[u][v];//優化d[v]
                     num[v] = num[u];
                }else if(d[u] + G[u][v] == d[v]){
                    num[v] += num[u];
                }
            }
        }
五、更優的遍歷模板(Djikstra+DFS)
  • 只考慮最短路徑(距離)的Djikstra算法,然后從這些最短距離中選出一條第二尺度最優的路徑出來。
1. 使用Djikstra算法記錄所有最短的路徑
  1. 我們需要使用vector,來記錄最短路徑的前驅結點,因為最短路徑的條數可能不止一條。
  2. 通過vector類型的數組就可以通過DFS來獲取所有的最短路徑。
  3. 首先講解如何獲取這個pre數組,本部分的Djikstra算法只需考慮距離這一因素,不必受第二尺度的干擾,之前的寫法中,數組初始化pre[i] = i,表示在初始條件下的前驅為自身,但是在這不需要進行初始化,因為可能會進行更新操作。
  4. 代碼:
vector<int> pre[MAXN];
void Djikstra(int s){
    fill(d, d+MAXN, INF);
    d[s] = 0;
    for(int i = 0; i < n; i++){
        int u = -1, MIN = INF;
        for(int j = 0; j < n; j++){
            if(vis[j] == false && d[j] < MIN){
                u = j;
                MIN = d[j];
            }
        }
        if(u == -1) return;
        vis[u] = true;
        for(int v = 0; v < n; v++){
            if(vis[v] == false && G[u][v] != INF){
                if(d[u] + G[u][v] < d[v]){
                    d[v] = d[u] + G[u][v];
                    pre[v].clear();//更新清空操作,這就是為啥不用初始化。
                    pre[v].push_back(u);
                }else if(d[u] + G[u][v] == d[v]){
                    pre[v].push_back(u);
                }
            }
        }
    }
}
2. 遍歷所有最短路徑找出一條使第二尺度最優的路徑
  • 與之前不同的是,之前的寫法是使用一個遞歸來獲取最短路徑,,但是現在可能存在多個前驅結點,所以遍歷的過程就是會形成一棵遞歸樹。
  • 當對於這棵樹進行遍歷的時候,每次到達葉子結點,也就是起點,就會產生一條完整的最短路徑。
  • 得到最短路徑后,可以進行第二尺度的計算,然后進行跟新,找出最優的最短路徑。
  • 接下來考慮如何進行DFS函數的書寫:
  1. 最為全局變量的最優值optValue
  2. 記錄最優路徑的數組path(使用vector)
  3. 臨時記錄DFS遍歷到葉子結點時的路徑tempPath(也使用vector記錄)
  4. 然后就是遞歸邊界和遞歸式
  5. 遞歸邊界,如果當前訪問的結點是葉子結點(也就是路徑的起點st),那么說明到達遞歸邊界,此時tempPath,存放了一天最短路徑,求出第二尺度value的值,與optValue進行比較,如果更優那么進行覆蓋。
  6. 對於遞歸式,只需對於pre[v]中的所有前驅結點進行遍歷即可。
  7. 遞歸過程中tempPath是如何生成的,只需在訪問結點v時,將v添加到tempPath中即可,然后遍歷遞歸,等pre[v]中所有結點遍歷完畢后,把tempPath最后面的v彈出。
  8. 需要注意的是,葉子結點(也就是路徑的起點st),沒有辦法通過上的寫法直接加入tempPath,因此在訪問到葉子節點的時候臨時加入。
  9. 代碼:
int optValue;
vector<int> pre[MAXN];
vector<int> path, tempPath;
void DFS(int v){//當前訪問結點v
    //遞歸邊界
    if(st == v){//如果到達葉子結點st(即路徑起點)
        tempPath.push_back(v);
        int value;//存放臨時路徑tempPath的第二尺度的值
        計算路徑tempPath上的value
        if(value 優於 optValue){//更新第二尺度最優值與最優路徑
            optValue = value;
            path = tempPath;
        }
        tempPath.pop_back();//將剛加入的結點刪除
        return;
    }
    //遞歸式
    tempPath.push_back(v);//將當前訪問結點加入臨時路徑的最后
    for(int i = 0; i < pre[v].size(); i++){
        DFS(pre[v][i]);
    }
    tempPath.pop_back();//遍歷完所有前驅結點,將當前結點v刪除
}
  • 對於上面部分,我們只需要針對於題目的要求,對計算路徑tempPath的value進行修改即可,而這個地方一般會涉及路徑邊權或者點權的計算,需要注意的是,由於遞歸的原因,存放在tempPath中的路徑結點是逆序的,因此訪問結點需要倒着進行,當然如果僅是對於邊權或點權進行求和,正着訪問也是沒有問題的。
//邊權之和
int value = 0;
for(int i = tempPath.size()-1; i > 0; i--){
    //當前結點id, 下一個結點idNext
    int id = tempPath[i], idNext = tempPath[i-1];
    value += V[id][idNext];//value增加邊id->idNext的邊權
}

//點權之和
int value = 0;
for(int i = tempPath.size()-1; i >= 0; i--){
    int id = tempPath[i];
    value += W[id];
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM