最短路知識點總結(Dijkstra,Floyd,SPFA,Bellman-Ford)


Dijkstra算法:

解決的問題:

    帶權重的有向圖上單源最短路徑問題。且權重都為非負值。如果采用的實現方法合適,Dijkstra運行時間要低於Bellman-Ford算法。

思路:

    如果存在一條從i到j的最短路徑(Vi.....Vk,Vj),Vk是Vj前面的一頂點。那么(Vi...Vk)也必定是從i到k的最短路徑。為了求出最短路徑,Dijkstra就提出了以最短路徑長度遞增,逐次生成最短路徑的算法。譬如對於源頂點V0,首先選擇其直接相鄰的頂點中長度最短的頂點Vi,那么當前已知可得從V0到達Vj頂點的最短距離dist[j]=min{dist[j],dist[i]+matrix[i][j]},應用了貪心的思想。根據這種思路,直接給出Dijkstra算法的偽代碼,他可用於計算正權圖的單源最短路徑,同時適用於無向圖和有向圖。

清除所有點的標號

設d[0]=0,其他d[i]=INF

循環n次

{

    在所有點的標號中,選出d值最小的結點x

    給結點x標記

    對於從x出發的所有邊(x,y),更新d[y]=min{d[y],d[x]+w(x,y)}

}

除了求出最短路的長度外,使用Dijkstra算法也能很方便地打印出結點0到所有節點的最短路本身.

代碼實現:

 

void dijkstra(int start)//從start點開始
{
    int i,j,k;
    memset(vis,0,sizeof(vis));//標記是否訪問過
    for(i=1; i<=n; i++)//n為總點數
    {
        if(i==start)
            dis[i]=0;
        else
            dis[i]=INF;
    }
    for(i=1; i<=n; i++)
    {
        int r;
        int min=INF;
        for(j=1; j<=n; j++)
            if(!vis[j]&&dis[j]<min)
            {
                min=dis[j];
                r=j;
            }
        vis[r]=1;
        for(k=1; k<=n; k++)//對所有從r出發的邊進行松弛
            if(dis[k]<(dis[r]+g[r][k]))
                dis[k]=dis[k];
            else
                dis[k]=dis[r]+g[r][k];
    }
    return;
}

 

  Floyd算法:

         負權重的邊可以存在,但不能存在權重為負值的環路

         算法考慮的是一條最短路徑上的中間結點。

              算法核心思想:  三圈for循環

                 

for (int k = 0; k < graph.getNumVex(); k++) {

 

                     for (int v = 0; v < graph.getNumVex(); v++) {

 

                            for (int w = 0; w < graph.getNumVex(); w++) {

 

                                   if (d[v][w] > d[v][k] + d[k][w]) {

 

                                          d[v][w] = d[v][k] + d[k][w];

 

                                          p[v][w] = p[v][k];// p[v][w]是v--w最短路徑上 v的下一頂點


                                   }
 

                            }
 

                     }
 

              }

  

 

第一層 k是作為中間頂點

第二層 v是作為起始頂點

第三層 w是作為終點頂點

內層核心代碼

 

以v為起點,w為終點,再以k作為v和w之間的中間點,去判斷d[v][ w]和d[v][k] + d[k][w]的大小關系,如果d[v][w] > d[v][k] + d[k][w],說明找到從v→w的更短路徑了,此時更改d[v][w]的值為d[v][k] + d[k][w]。

 

p[v][w]的值也要相應改成p[v][k]的值,因為 p[v][k]的值是v→k最短路徑上v的后繼頂點,而v→w這段最短路徑是連接在v→k這段路徑后面的,所以令所當然p[v][w]也要指向p[v][k]。

 

注意:最外層的k循環,前面的n此循環的結果跟后面n+1次循環的錯做過程是息息相關,

 

       三次循環完成后,各個頂點之間的最短路徑權重會存儲在d矩陣中:d[i][j]表示i→j的最短路徑權重。

 

鄰接矩陣算法實現:

void Floyd(MGraph g)
{
   int A[MAXV][MAXV];
   int path[MAXV][MAXV];
   int i,j,k,n=g.n;
   for(i=0;i<n;i++)
      for(j=0;j<n;j++)
      {   
             A[i][j]=g.edges[i][j];
            path[i][j]=-1;
       }
   for(k=0;k<n;k++)
   { 
        for(i=0;i<n;i++)
           for(j=0;j<n;j++)
               if(A[i][j]>(A[i][k]+A[k][j]))
               {
                     A[i][j]=A[i][k]+A[k][j];
                     path[i][j]=k;
                } 
     } 
}

 Bellman-Ford算法 

   解決的問題:

             一般情況下的單源最短路徑問題,這里權重可以為負值。

Bellman-ford算法返回一個布爾值,一表明是否存在一個從源結點可以到達的權重為負的環路。如果存在這樣一個環路,算法將告訴我們不存在解決方案,如果沒有這種環路的存在算法將給出最短路徑和他們的權重。

   Bellman-Ford算法的流程如下:
             給定圖G(V, E)(其中V、E分別為圖G的頂點集與邊集),源點s,數組Distant[i]記錄從源點s到頂點i的路徑長度,初始化數組Distant[n]為, Distant[s]為0;

    以下操作循環執行至多n-1次,n為頂點數:
    對於每一條邊e(u, v),如果Distant[u] + w(u, v) < Distant[v],則另Distant[v] = Distant[u]+w(u, v)。w(u, v)為邊e(u,v)的權值;
    若上述操作沒有對Distant進行更新,說明最短路徑已經查找完畢,或者部分點不可達,跳出循環。否則執行下次循環;

    為了檢測圖中是否存在負環路,即權值之和小於0的環路。對於每一條邊e(u, v),如果存在Distant[u] + w(u, v) < Distant[v]的邊,則圖中存在負環路,即是說改圖無法求出     單源最短路徑。否則數組Distant[n]中記錄的就是源點s到各頂點的最短路徑長度。

    可知,Bellman-Ford算法尋找單源最短路徑的時間復雜度為O(V*E).

  Bellman-Ford算法可以大致分為三個部分
    第一,初始化所有點。每一個點保存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
    第二,進行循環,循環下標為從1到n-1(n等於圖中點的個數)。在循環內部,遍歷所有的邊,進行松弛計算。
    第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:
    d(v) > d (u) + w(u,v)
    則返回false,表示途中存在從源點可達的權為負的回路。
 
之所以需要第三部分的原因,是因為,如果存在從源點可達的權為負的回路。則 應為無法收斂而導致不能求出最短路徑。 

#include<iostream>
#include<cstdio>
using namespace std;
#define MAX 0x3f3f3f3f
#define N 1010
int nodenum, edgenum, original; //點,邊,起點
typedef struct Edge //邊
{
    int u,v;
    int cost;
} Edge;
Edge edge[N];
int dis[N], pre[N];
bool Bellman_Ford()
{
    for(int i = 1; i <= nodenum; ++i)
    {
        if(i==original)
            dis[i]=0;
        else
            dis[i]=MAX;
    }
    for(int i = 1; i <= nodenum - 1; ++i)//循環n-1次
        for(int j = 1; j <= edgenum; ++j)//遍歷每條邊
        {
            if(dis[edge[j].v] > dis[edge[j].u] + edge[j].cost) //松弛(順序一定不能反~)
            {
                dis[edge[j].v] = dis[edge[j].u] + edge[j].cost;
                printf("%d ",dis[edge[j].v]);
                pre[edge[j].v] = edge[j].u;
            }
            printf("%d ",dis[edge[j].v]);
        }
    bool flag = 1; //判斷是否含有負權回路
    for(int i = 1; i <= edgenum; ++i)
        if(dis[edge[i].v] > dis[edge[i].u] + edge[i].cost)
        {
            flag = 0;
            break;
        }
    return flag;
}
void print_path(int root) //打印最短路的路徑(反向)
{
    while(root != pre[root]) //前驅
    {
        printf("%d-->", root);
        root = pre[root];
    }
    if(root == pre[root])
        printf("%d\n", root);
}
int main()
{
    scanf("%d%d%d", &nodenum, &edgenum, &original);
    pre[original] = original;
    for(int i = 1; i <= edgenum; ++i)
    {
        scanf("%d%d%d", &edge[i].u, &edge[i].v, &edge[i].cost);
    }
    if(Bellman_Ford())
        for(int i = 1; i <= nodenum; ++i) //每個點最短路
        {
            printf("%d\n", dis[i]);
            printf("Path:");
            print_path(i);
        }
    else
        printf("have negative circle\n");
    return 0;
}

spfa算法:

算法流程

算法大致流程是用一個隊列來進行維護。初始時將源加入隊列。每次從隊列中取出一個元素,並對所有與他相鄰的點進行松弛,若某個相鄰的點松弛成功,則將其入隊。直到隊列為空時算法結束。

這個算法,簡單的說就是隊列優化的bellman-ford,利用了每個點不會更新次數太多的特點發明的此算法

SPFA——Shortest Path Faster Algorithm,它可以在O(kE)的時間復雜度內求出源點到其他所有點的最短路徑,可以處理負邊。SPFA的實現甚至比Dijkstra或者Bellman_Ford還要簡單:

設Dist代表S到I點的當前最短距離,Fa代表S到I的當前最短路徑中I點之前的一個點的編號。開始時Dist全部為+∞,只有Dist[S]=0,Fa全部為0。

維護一個隊列,里面存放所有需要進行迭代的點。初始時隊列中只有一個點S。用一個布爾數組記錄每個點是否處在隊列中。

每次迭代,取出隊頭的點v,依次枚舉從v出發的邊v->u,設邊的長度為len,判斷Dist[v]+len是否小於 Dist[u],若小於則改進Dist[u],將Fa[u]記為v,並且由於S到u的最短距離變小了,有可能u可以改進其它的點,所以若u不在隊列中,就將它放入隊尾。這樣一直迭代下去直到隊列變空,也就是S到所有的最短距離都確定下來,結束算法。若一個點入隊次數超過n,則有負權環

SPFA 在形式上和寬度優先搜索非常類似,不同的是寬度優先搜索中一個點出了隊列就不可能重新進入隊列,但是SPFA中一個點可能在出隊列之后再次被放入隊列,也就是一個點改進過其它的點之后,過了一段時間可能本身被改進,於是再次用來改進其它的點,這樣反復迭代下去。設一個點用來作為迭代點對其它點進行改進的平均次數為k,有辦法證明對於通常的情況,k在2左右。

代碼模板:

SPFA
void Spfa()
{
    for (int i(0); i<num_town; ++i)//初始化
    {
        dis[i] = MAX;
        visited[i] = false;    
    }
    queue<int> Q;
    dis[start] = 0;
    visited[start] = true;
    Q.push(start);
    while (!Q.empty()){
        int temp = Q.front();
        Q.pop();
        for (int i(0); i<num_town; ++i)
        {
            if (dis[temp] + road[temp][i] < dis[i])//存在負權的話,就需要創建一個COUNT數組,當某點的入隊次數超過V(頂點數)返回。
            {
                dis[i] = dis[temp] + road[temp][i];
                if (!visited[i])
                {
                    Q.push(i);
                    visited[i] = true;    
                }        
            }            
        }
        visited[temp] = false;            
    }    
}

  四種算法總結完了,都是東拼西湊的,自己學的也不好,還是靜下心來好好學吧。也許有一天,你發覺日子特別的艱難,那可能是這次的收獲將特別的巨大。這幾天總是在抱怨生活,患得患失,卻忘了自己為什么留下來暑期集訓,因為你什么都沒有,所以你必須努力!噶嗚!~加油

                                                                                                                                                                         ————Anonymous.PJQ


免責聲明!

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



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