幾個最短路徑的演算法

ZHUO_SIR發表於2018-06-08

一、floyd

1.介紹 
  floyd演算法只有五行程式碼,程式碼簡單,三個for迴圈就可以解決問題,所以它的時間複雜度為O(n^3),可以求多源最短路問題。 
2.思想: 
  Floyd演算法的基本思想如下:從任意節點A到任意節點B的最短路徑不外乎2種可能,1是直接從A到B,2是從A經過若干個節點X到B。所以,我們假設Dis(AB)為節點A到節點B的最短路徑的距離,對於每一個節點X,我們檢查Dis(AX) + Dis(XB) < Dis(AB)是否成立,如果成立,證明從A到X再到B的路徑比A直接到B的路徑短,我們便設定Dis(AB) = Dis(AX) + Dis(XB),這樣一來,當我們遍歷完所有節點X,Dis(AB)中記錄的便是A到B的最短路徑的距離。 
舉個例子:已知下圖, 
這裡寫圖片描述 
 如現在只允許經過1號頂點,求任意兩點之間的最短路程,只需判斷e[i][1]+e[1][j]是否比e[i][j]要小即可。e[i][j]表示的是從i號頂點到j號頂點之間的路程。e[i][1]+e[1][j]表示的是從i號頂點先到1號頂點,再從1號頂點到j號頂點的路程之和。其中i是1~n迴圈,j也是1~n迴圈,程式碼實現如下。 
 

for(i=1; i<=n; i++)
{
    for(j=1; j<=n; j++)
    {
        if ( e[i][j] > e[i][1]+e[1][j] )
            e[i][j] = e[i][1]+e[1][j];
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

 接下來繼續求在只允許經過1和2號兩個頂點的情況下任意兩點之間的最短路程。在只允許經過1號頂點時任意兩點的最短路程的結果下,再判斷如果經過2號頂點是否可以使得i號頂點到j號頂點之間的路程變得更短。即判斷e[i][2]+e[2][j]是否比e[i][j]要小,程式碼實現為如下。 
 

//經過1號頂點
for(i=1; i<=n; i++)
    for(j=1; j<=n; j++)
        if (e[i][j] > e[i][1]+e[1][j]) 
            e[i][j]=e[i][1]+e[1][j];
//經過2號頂點
for(i=1; i<=n; i++)
    for(j=1; j<=n; j++)
        if (e[i][j] > e[i][2]+e[2][j])  
            e[i][j]=e[i][2]+e[2][j];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

 最後允許通過所有頂點作為中轉,程式碼如下: 
 

for(k=1; k<=n; k++)
    for(i=1; i<=n; i++)
        for(j=1; j<=n; j++)
            if(e[i][j]>e[i][k]+e[k][j])
                e[i][j]=e[i][k]+e[k][j];
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

這段程式碼的基本思想就是:最開始只允許經過1號頂點進行中轉,接下來只允許經過1和2號頂點進行中轉……允許經過1~n號所有頂點進行中轉,求任意兩點之間的最短路程。與上面相同 
3.程式碼模板:

#include <stdio.h>
#define inf 0x3f3f3f3f
int map[1000][1000];
int main()
{
    int k,i,j,n,m;
    //讀入n和m,n表示頂點個數,m表示邊的條數
    scanf("%d %d",&n,&m);

    //初始化
    for(i=1; i<=n; i++)
        for(j=1; j<=n; j++)
            if(i==j)
                map[i][j]=0;
            else
                map[i][j]=inf;
    int a,b,c;
    //讀入邊
    for(i=1; i<=m; i++)
    {
        scanf("%d %d %d",&a,&b,&c);
        map[a][b]=c;//這是一個有向圖
    }

    //Floyd-Warshall演算法核心語句
    for(k=1; k<=n; k++)
        for(i=1; i<=n; i++)
            for(j=1; j<=n; j++)
                if(map[i][j]>map[i][k]+map[k][j] )
                    map[i][j]=map[i][k]+map[k][j];

    //輸出最終的結果,最終二維陣列中存的即使兩點之間的最短距離
    for(i=1; i<=n; i++)
    {
        for(j=1; j<=n; j++)
        {
            printf("%10d",map[i][j]);
        }
        printf("\n");
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

二、dijkstra

五.Dijkstra(迪傑斯特拉) 
1.演算法介紹: 
Dijkstra(迪傑斯特拉)演算法是典型的單源最短路徑演算法,Dijkstra 演算法,用於對有權圖進行搜尋,找出圖中兩點的最短距離,既不是DFS搜尋,也不是BFS搜尋。 
把Dijkstra 演算法應用於無權圖,或者所有邊的權都相等的圖,Dijkstra 演算法等同於BFS搜尋。

2.演算法描述 
1)演算法思想:設G=(V,E)是一個帶權有向圖,把圖中頂點集合V分成兩組,第一組為已求出最短路徑的頂點集合(用S表示,初始時S中只有一個源點,以後每求得一條最短路徑 , 就將加入到集合S中,直到全部頂點都加入到S中,演算法就結束了),第二組為其餘未確定最短路徑的頂點集合(用U表示),按最短路徑長度的遞增次序依次把第二組的頂點加入S中。在加入的過程中,總保持從源點v到S中各頂點的最短路徑長度不大於從源點v到U中任何頂點的最短路徑長度。此外,每個頂點對應一個距離,S中的頂點的距離就是從v到此頂點的最短路徑長度,U中的頂點的距離,是從v到此頂點只包括S中的頂點為中間頂點的當前最短路徑長度。 
例子 
這裡寫圖片描述 
這裡寫圖片描述

重點需要理解這句拗口的”按最短路徑長度的遞增次序依次把第二組的頂點加入S中。在加入的過程中,總保持從源點v到S中各頂點的最短路徑長度不大於從源點v到U中任何頂點的最短路徑長度” 
實際上,Dijkstra 演算法是一個排序過程,就上面的例子來說,是根據A到圖中其餘點的最短路徑長度進行排序,路徑越短越先被找到,路徑越長越靠後才能被找到,要找A到F的最短路徑,我們依次找到了 
A –> C 的最短路徑 3 
A –> C –> B 的最短路徑 5 
A –> C –> D 的最短路徑 6 
A –> C –> E 的最短路徑 7 
A –> C –> D –> F 的最短路徑 9 
Dijkstra 演算法執行的附加效果是得到了另一個資訊,A到C的路徑最短,其次是A到B, A到D, A到E, A到F 
為什麼Dijkstra 演算法不適用於帶負權的圖? 
就上個例子來說,當把一個點選入集合S時,就意味著已經找到了從A到這個點的最短路徑,比如第二步,把C點選入集合S,這時已經找到A到C的最短路徑了,但是如果圖中存在負權邊,就不能再這樣說了。舉個例子,假設有一個點Z,Z只與A和C有連線,從A到Z的權為50,從Z到C的權為-49,現在A到C的最短路徑顯然是A –> Z –> C 
對帶負權的圖,應該用Floyd演算法 
3.程式碼: 
鄰接矩陣

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int MAX=0x3f3f3f3f;
int map[110][110];
int dis[110];
int visit[110];
/*
關於三個陣列:map陣列存的為點邊的資訊,比如map[1][2]=3,表示1號點和2號點的距離為3
dis陣列存的為起始點與每個點的最短距離,比如dis[3]=5,表示起始點與3號點最短距離為5
visit陣列存的為0或者1,1表示已經走過這個點。
*/
int n,m;
int dijkstra()
{
    int i,j,pos=1,min,sum=0;
    memset(visit,0,sizeof(visit));//初始化為0,表示開始都沒走過
    for(i=1; i<=n; i++)
    {
        dis[i]=map[1][i];
    }
    visit[1]=1;
    dis[1]=0;
    int T=n-1;
    while(T--)
    {
        min=MAX;
        for(j=1; j<=n; j++)
        {
            if(visit[j]==0&&min>dis[j])
            {
                min=dis[j];
                pos=j;
            }
        }
        visit[pos]=1;//表示這個點已經走過
        for(j=1; j<=n; j++)
        {
            if(visit[j]==0&&dis[j]>min+map[pos][j])//更新dis的值
                dis[j]=map[pos][j]+min;
        }
    }
    return dis[n];
}
int main()
{
    int i,j;
    while(cin>>n>>m)//n表示n個點,m表示m條邊
    {
        memset(map,MAX,sizeof(map));
        int a,b,c;
        for(i=1; i<=m; i++)
        {
            cin>>a>>b>>c;
            if(c<map[a][b])//防止有重邊
                map[a][b]=map[b][a]=c;
        }
        int sum=dijkstra();
        cout<<sum<<endl;
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64

鄰接表實現

#include <stdio.h>
#include <string.h>
#include <string>
#include <vector>
#include <algorithm>
#define INF 0x3f3f3f3f

using namespace std;

struct node
{
    int end;//終點
    int power;//權值
} t;

int n;//n為邊數
vector<node>q[500001];//鄰接表儲存圖的資訊(相當於一個儲存著結構體的二維陣列)
int dis[500001];//距離陣列
bool vis[500001];//標記陣列

void Dijkstra(int start, int end)
{
    memset(vis, false, sizeof(vis));
    for(int i=0; i<=n; i++)
    {
        dis[i] = INF;
    }
    int len=q[start].size();
    for(int i=0; i<len; i++)
    {
        if(q[start][i].power < dis[q[start][i].end] )
            dis[q[start][i].end]=q[start][i].power; //從起點開始的dis陣列更新
    }

    vis[start]=true;//起點標記為1

    for(int k=0; k<n-1; k++)
    {
        int pos, min=INF;
        for(int i=1; i<=n; i++)
        {
            if( !vis[i] && dis[i]<min )
            {
                //當前節點未被訪問過且權值較小
                min=dis[i];
                pos=i;
            }
        }

        vis[pos]=true;

        //再次更新dis陣列
        len=q[pos].size();
        for(int j=0; j<len; j++)
        {
            if( !vis[q[pos][j].end] && dis[ q[pos][j].end ]>q[pos][j].power+dis[pos] )
                dis[q[pos][j].end ] = q[pos][j].power + dis[pos];
        }
    }
    printf("%d\n", dis[end] );
}


int main()
{
    int m;
    while(scanf("%d %d", &n, &m)&&n&&m)//輸入點和邊
    {
        for(int i=0; i<=n; i++)
            q[i].clear();//將vector陣列清空
        for(int i=0; i<m; i++)
        {
            int begin,end, power;
            scanf("%d %d %d", &begin, &end, &power);//輸入
            /*t作為node型臨時變數,為了方便壓入,以下程式碼為無向圖的輸入邊*/
            t.end=end;
            t.power=power;
            q[begin].push_back(t);  //輸入的資料依次存到q[begin][0]、q[begin][1]裡

            t.end=begin;
            t.power=power;
            q[end].push_back(t);
        }
        //Dijkstra(1, n);
        int start, end;//自己確定起始點和終止點
        scanf("%d %d", &start, &end);//輸入起始點和終止點
        Dijkstra(start, end);
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90

三、Bellman-Ford(貝爾曼-福特)

Dijkstra演算法是處理單源最短路徑的有效演算法,但它侷限於邊的權值非負的情況,若圖中出現權值為負的邊,Dijkstra演算法就會失效,求出的最短路徑就可能是錯的。這時候,就需要使用其他的演算法來求解最短路徑,Bellman-Ford演算法就是其中最常用的一個。該演算法由美國數學家理查德•貝爾曼(Richard Bellman, 動態規劃的提出者)和小萊斯特•福特(Lester Ford)發明。Bellman-Ford演算法的流程如下:

給定圖G(V, E)(其中V、E分別為圖G的頂點集與邊集),源點s,

1.陣列Distant[i]記錄從源點s到頂點i的路徑長度,初始化陣列Distant[n]為, Distant[s]為0;

2.以下操作迴圈執行至多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進行更新,說明最短路徑已經查詢完畢,或者部分點不可達,跳出迴圈。否則執行下次迴圈;

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

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

首先介紹一下鬆弛計算。如下圖: 
這裡寫圖片描述

鬆弛計算之前,點B的值是8,但是點A的值加上邊上的權重2,得到5,比點B的值(8)小,所以,點B的值減小為5。這個過程的意義是,找到了一條通向B點更短的路線,且該路線是先經過點A,然後通過權重為2的邊,到達點B。 
當然,如果出現一下情況: 
這裡寫圖片描述

則不會修改點B的值,因為3+4>6。

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

經過第一次遍歷後,點B的值變為5,點C的值變為8,這時,注意權重為-10的邊,這條邊的存在,導致點A的值變為-2。(8+ -10=-2) 
這裡寫圖片描述

第二次遍歷後,點B的值變為3,點C變為6,點A變為-4。正是因為有一條負邊在迴路中,導致每次遍歷後,各個點的值不斷變小。

在回過來看一下bellman-ford演算法的第三部分,遍歷所有邊,檢查是否存在d(v) > d (u) + w(u,v)。因為第二部分迴圈的次數是定長的,所以如果存在無法收斂的情況,則肯定能夠在第三部分中檢查出來。比如 
這裡寫圖片描述

此時,點A的值為-2,點B的值為5,邊AB的權重為5,5 > -2 + 5. 檢查出來這條邊沒有收斂。

所以,Bellman-Ford演算法可以解決圖中有權為負數的邊的單源最短路徑問。

程式碼:

int N, M;
typedef struct node
{
    int u, v;
    int cost;
} E;
node E[N];
int dis[N], pre[N];
bool Bellman()
{
    int ok;
    for(int i = 1; i <= N; ++i)
        dis[i] = (i == 1 ? 0 : MAX);
    for(int i = 1; i <= N - 1; ++i)
    {
        ok=1;
        for(int j = 1; j <= M; ++j)
            if(dis[E[j].v] > dis[E[j].u] + E[j].cost)
            {
                dis[E[j].v] = dis[E[j].u] + E[j].cost;
                ok=0;
            }
        if(ok==1)
            break;
    }
    bool flag = 1;
    for(int i = 1; i <= M; ++i)
        if(dis[E[i].v] > dis[E[i].u] + E[i].cost)
        {
            flag = 0;
            break;
        }
    return flag;
}
int main()
{
    cin>>N>>M;
    for(int i = 1; i <= M; ++i)
        cin>>E[i].u>>E[i].v>>E[i].cost;
    if(Bellman())
        cout<<dis[M];
    else
        cout<<"存在負";
    return 0;
}

相關文章