1. 程式人生 > >紫書第十一章-----圖論模型與演算法(最短路徑Dijkstra演算法Bellman-Ford演算法Floyd演算法)

紫書第十一章-----圖論模型與演算法(最短路徑Dijkstra演算法Bellman-Ford演算法Floyd演算法)

最短路徑演算法一之Dijkstra演算法

演算法描述:在無向圖 G=(V,E) 中,假設每條邊 E[i] 的長度為 w[i],找到由頂點 V0 到其餘各點的最短路徑。
使用條件:單源最短路徑,適用於邊權非負的情況

結合上圖具體搜尋過程,我繪出下表,方便理解該過程!下表是按照上圖的搜尋過程來繪製的,當然,儲存圖的時候,節點儲存順序的不同也會導致搜尋的順序不同,但是可以保證的是每個定點和每條邊都搜尋僅僅一次哦!

這裡寫圖片描述

經過以上的學習,我們能夠感覺到Dijkstra演算法和bfs演算法非常類似,做過下面的兩道題目之後就能很好地感悟到這一點啦~~~
下面將是第一道最短路徑例題,幫我理解Dijkstra演算法,有了前面的講解和圖解,圖表示範,下面的程式碼就比較容易理解了!

【例題 本題程式碼中的演算法既給出了普通版本的程式碼,同時給出了優先佇列優化版本的程式碼】

輸入n和m,代表n個節點,m條邊,然後是m行輸入,每行有x,y,z,代表x到y的路距離為z。問題:從1出發到各點的最短路徑。

參考程式碼如下:

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

using namespace std;

const int maxn=105;
const int INF=1<<30;
int mp[maxn][maxn];
bool vis[maxn];
int
dis[maxn]; int path[maxn]; int n,m; //注意該搜尋演算法中,可以認為所有的節點之間都有邊, //其實就是把那些沒有連線起來的節點的邊的權重看成了 //正無窮 void init() { for(int i=0;i<=n;i++) for(int j=0;j<=n;j++) mp[i][j]=INF; } void Dijkstra(int st) { memset(vis,0,sizeof(vis)); for(int i=0;i<=n;i++) path[i]=-1; for
(int i=0;i<=n;i++) dis[i]=INF; dis[st]=0; for(int i=0;i<n;i++) { int pos=0; int mmin=INF; for(int j=1;j<=n;j++) // 找到一個新的向外擴充套件搜尋的起點 { if(!vis[j] && dis[j]<mmin) { pos=j; mmin=dis[j]; } } if(mmin==INF)break; vis[pos]=1; for(int j=1;j<=n;j++) { if(dis[j]>dis[pos]+mp[pos][j] && mp[pos][j]!=INF) //相當於動態規劃裡面的狀態轉移, { //dis[i]表示的狀態是起點st到i的最短路徑的距離 dis[j]=dis[pos]+mp[pos][j]; path[j]=pos; //用來記錄路徑,這個可以對照上面講過的東東看一下應該可以看懂滴 } } } } void print(int en) { if(en==-1)return; print(path[en]); cout<<en<<"->"; } int main() { int x,y,z; int en; cin>>n>>m; init(); //對mp初始化,一定要記住此語句在輸入n之後,我被這一點坑慘了,找了好久才發現 for(int i=0;i<m;i++) { cin>>x>>y>>z; mp[x][y]=mp[y][x]=z; } Dijkstra(1); cin>>en; cout<<dis[en]<<endl; //列印路徑資訊 print(path[en]); cout<<en<<endl;//打印出最後一個與前面的->配對 return 0; }

不妨根據知識點講解部分的圖,給出測試資料樣例如下:
input

6 9
1 2 7
1 3 9
1 6 14
2 3 10
2 4 15
3 4 11
3 6 2
4 5 6
5 6 9
6

output

結果顯示

【在使用優先佇列優化程式碼之前,先補充memset和fill的區別】

  1. memset函式
    按照位元組填充某字元
    在標頭檔案”cstring”裡面
  2. fill函式
    按照單元賦值,將一個區間的元素都賦同一個值
    在標頭檔案”algorithm”裡面

    因為memset函式按照位元組填充,所以一般memset只能用來填充char、bool型陣列,(因為只有char、bool型佔一個位元組)如果填充int型陣列,除了0和-1,其他的不能。因為只有00000000 = 0,-1同理,如果我們把每一位都填充“1”,會導致變成填充入“11111111”
    而fill函式可以賦值任何,而且使用方法特別簡便:
    fill(arr, arr + n, 要填入的內容);

例如:

#include<iostream>
#include<algorithm>

using namespace std;

int main()
{
    int a[100];
    fill(a,a+20,100);
    for(int i=0;i<20;i++)
        cout<<a[i]<<endl;
    return 0;
}

本例題優先佇列優化程式碼如下所示:

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>

using namespace std;

const int INF=1<<30;
const int maxn=300;

int n,m,en;
int mp[maxn][maxn];
int dis[maxn];
int path[maxn];
bool vis[maxn];
typedef pair<int ,int>pr;//first是到點的邊的長度,second是點

void init()
{
    fill(mp[0],mp[0]+maxn*maxn,INF);
    fill(dis,dis+maxn,INF);
    memset(vis,0,sizeof(vis));
    memset(path,-1,sizeof(path));
}

void Dijkstra(int st)
{
    dis[st]=0;
    priority_queue<pr,vector<pr>,greater<pr> >que;
    que.push(pr(0,st));
    while(!que.empty())
    {
        pr p=que.top();
        que.pop();
        int vi=p.second;
        if(vis[vi])
            continue;
        vis[vi]=1;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i] && dis[i]>dis[vi]+mp[vi][i])
            {
                dis[i]=dis[vi]+mp[vi][i];
                que.push(pr(dis[i],i));
                path[i]=vi;
            }
        }
    }
}

void print(int st)
{
    if(st==-1)return;
    print(path[st]);
    cout<<st<<"->";
}

int main()
{
    while(cin>>n>>m)
    {
        init();
        for(int i=1;i<=m;i++)
        {
            int from,to,len;
            cin>>from>>to>>len;
            mp[from][to]=mp[to][from]=len;
        }
        Dijkstra(1);
        cin>>en;
        cout<<dis[en]<<endl;
        print(path[en]);
        cout<<en<<endl;
    }
    return 0;
}

【類似習題 Choose the best route HDU - 2680 】

One day , Kiki wants to visit one of her friends. As she is liable to carsickness , she wants to arrive at her friend’s home as soon as possible . Now give you a map of the city’s traffic route, and the stations which are near Kiki’s home so that she can take. You may suppose Kiki can change the bus at any station. Please find out the least time Kiki needs to spend. To make it easy, if the city have n bus stations ,the stations will been expressed as an integer 1,2,3…n.

Input

There are several test cases.  Each case begins with three integers n, m and s,(n<1000,m<20000,1=<s<=n) n stands for the number of bus stations in this city and m stands for the number of directed ways between bus stations .(Maybe there are several ways between two bus stations .) s stands for the bus station that near Kiki’s friend’s home. 
Then follow m lines ,each line contains three integers p , q , t (0<t<=1000). means from station p to station q there is a way and it will costs t minutes . 
Then a line with an integer w(0<w<n), means the number of stations Kiki can take at the beginning. Then follows w integers stands for these stations. 
Output
The output contains one line for each data set : the least time Kiki needs to spend ,if it’s impossible to find such a route ,just output “-1”.
Sample Input
5 8 5
1 2 2
1 5 3
1 3 4
2 4 7
2 5 6
2 3 5
3 5 1
4 5 1
2
2 3
4 3 4
1 2 3
1 3 4
2 3 2
1
1
Sample Output
1
-1
【分析   套用Dijkstra演算法模板+引入超級源點或把圖反向(逆向思維)】   
自己多加一個超級源點,把起點集合連線到超級源點上,然後將起點與超級源點的集合的路徑長度設為0,這樣就稱為一個n+1個點的單源最短路演算法。。。。。

AC程式碼(一):

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

using namespace std;

const int maxn=1005;
const int INF=1<<30;

int mp[maxn][maxn];
int dis[maxn];
bool vis[maxn];

int n,m,s;

void init()
{
    fill(mp[0],mp[0]+maxn*maxn,INF);
    fill(dis,dis+maxn,INF);;
    memset(vis,0,sizeof(vis));
}

void Dijkstra(int st)
{
    dis[st]=0;
    for(int i=0;i<=n;i++)
    {
        int mmin=INF;
        int pos=0;
        for(int j=0;j<=n;j++)
        {
            if(!vis[j] && dis[j]<mmin)
            {
                mmin=dis[j];
                pos=j;
            }
        }
        if(mmin==INF)break;//已經完成搜尋,結束
        vis[pos]=1;
        for(int j=0;j<=n;j++)
        {
            if(dis[j]>dis[pos]+mp[pos][j] && mp[pos][j]!=INF)
                dis[j]=dis[pos]+mp[pos][j];
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m>>s)
    {
        init();
        int p,q,t,w,ss;
        int temp[maxn];
        for(int i=0;i<m;i++)
        {
            cin>>p>>q>>t;
            if(t<mp[p][q])//兩個車站之間可能花費不同時間,取最小值,坑點
                mp[p][q]=t;//還要注意本圖是有向圖
        }

        cin>>w;
        while(w--)
        {
            cin>>ss;
            mp[0][ss]=0;
            dis[ss]=0;//該句程式碼可有可無,這一點與優先佇列優化的程式碼不同,在優化的程式碼中再說原因
        }
        Dijkstra(0);
        if(dis[s]==INF)cout<<"-1"<<endl;
        else cout<<dis[s]<<endl;
    }
    return 0;
}

AC程式碼(二):

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

using namespace std;

const int maxn=1005;
const int INF=1<<30;

int mp[maxn][maxn];
int dis[maxn];
bool vis[maxn];

int n,m,s;

void init()
{
    for(int i=0;i<=n;i++)
        for(int j=0;j<=n;j++)
        {
            mp[i][j]=INF;
            //下面這種初始化方式,其實沒有必要,因為從a車站到a車站雖說距離是0
            //但根本不會這麼走,因為這麼走始終沒有前進,沒有意義
            //if(i==j)mp[i][j]=0;
            //else mp[i][j]=INF;
        }
}

void Dijkstra(int st)
{
    memset(vis,0,sizeof(vis));
    for(int i=0;i<=n;i++)
        dis[i]=mp[0][i];
    dis[st]=0;
    for(int i=0;i<n;i++)
    {
        int mmin=INF;
        int pos=0;
        for(int j=1;j<=n;j++)
        {
            if(!vis[j] && dis[j]<mmin)
            {
                mmin=dis[j];
                pos=j;
            }
        }
        if(mmin==INF)break;//已經完成搜尋,結束
        vis[pos]=1;
        for(int j=1;j<=n;j++)
        {
            if(dis[j]>dis[pos]+mp[pos][j] && mp[pos][j]!=INF)
                dis[j]=dis[pos]+mp[pos][j];
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m>>s)
    {
        init();
        int p,q,t,w,ss;
        int temp[maxn];
        for(int i=0;i<m;i++)
        {
            cin>>p>>q>>t;
            if(t<mp[q][p])  //把圖反向,並且兩個車站之間可能花費不同時間,取最小值,坑點
                mp[q][p]=t;
        }
        Dijkstra(s);
        int ans=INF;
        cin>>w;
        while(w--)
        {
            cin>>ss;
            ans=min(ans,dis[ss]);
        }

        if(ans==INF)cout<<"-1"<<endl;
        else cout<<ans<<endl;
    }
    return 0;
}

AC程式碼(三)(優先佇列優化版本,根據反向圖編寫):

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>

using namespace std;

const int INF=1<<30;
const int maxn=1005;

int mp[maxn][maxn];
int dis[maxn];
bool vis[maxn];
int n,m,s;
int w;
int from,to,weight;

typedef pair<int,int>pr;

void init()
{
    memset(vis,0,sizeof(vis));
    fill(mp[0],mp[0]+maxn*maxn,INF);
    fill(dis,dis+maxn,INF);
}

void Dijkstra(int st)
{
    dis[st]=0;
    priority_queue<pr,vector<pr>,greater<pr> >que;
    que.push(make_pair(0,st));
    while(!que.empty())
    {
        pr p=que.top();
        que.pop();
        int vi=p.second;
        if(vis[vi])continue;
        vis[vi]=1;
        for(int i=1;i<=n;i++)
        {
            if(!vis[i] && dis[i]>dis[vi]+mp[vi][i])
            {
                dis[i]=dis[vi]+mp[vi][i];
                que.push(make_pair(dis[i],i));
            }
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m>>s)
    {
        init();
        for(int i=1;i<=m;i++)
        {
            cin>>from>>to>>weight;
            if(weight<mp[to][from])
                mp[to][from]=weight;
        }
        Dijkstra(s);
        cin>>w;
        int tmp;
        int ans=INF;
        while(w--)
        {
            cin>>tmp;
            ans=min(ans,dis[tmp]);
        }
        if(ans==INF)cout<<"-1"<<endl;
        else cout<<ans<<endl;
    }
    return 0;
}

AC程式碼(四)(用優先佇列優化,根據新增超級源點編寫):

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>

using namespace std;

const int INF=1<<30;
const int maxn=1005;

int mp[maxn][maxn];
int dis[maxn];
bool vis[maxn];
int n,m,s;
int w;
int from,to,weight;

typedef pair<int,int>pr;

void init()
{
    memset(vis,0,sizeof(vis));
    fill(mp[0],mp[0]+maxn*maxn,INF);
    fill(dis,dis+maxn,INF);
}

void Dijkstra(int st)
{
    dis[st]=0;
    priority_queue<pr,vector<pr>,greater<pr> >que;
    que.push(make_pair(0,st));
    while(!que.empty())
    {
        pr p=que.top();
        que.pop();
        int vi=p.second;
        if(vis[vi])continue;
        vis[vi]=1;
        for(int i=0;i<=n;i++)
        {
            if(!vis[i] && dis[i]>dis[vi]+mp[vi][i])
            {
                dis[i]=dis[vi]+mp[vi][i];
                que.push(make_pair(dis[i],i));
            }
        }
    }
}

int main()
{
    ios::sync_with_stdio(false);
    while(cin>>n>>m>>s)
    {
        init();
        for(int i=1;i<=m;i++)
        {
            cin>>from>>to>>weight;
            if(weight<mp[from][to])
                mp[from][to]=weight;
        }
        cin>>w;
        int tmp;
        while(w--)
        {
            cin>>tmp;
            mp[0][tmp]=0;
            //dis[tmp]=0;//注意不要寫這句,否則錯誤,因為如果添加了這句程式碼,距離提前初始化,導致
                         //佇列中只進入了一個元素,即超級源點,然後出隊,演算法結束,這一點和非優化版
                        //的程式碼則不一樣,非優化版的程式碼中,即使剛開始在鬆弛部分程式碼if(!vis[i]                                                                           
                        //&& dis[i]>dis[vi]+mp[vi][i])一次也不執行也沒關係,後續會繼續執行
                    //Dijkstra演算法,但是優化版本的則會結束掉Dijkstra演算法。                                                                                      
        }
        Dijkstra(0);
        if(dis[s]==INF)cout<<"-1"<<endl;
        else cout<<dis[s]<<endl;
    }
    return 0;
}

最短路徑演算法二之Bellman-Ford演算法

回憶前面的Dijkstra演算法,只能解決邊權非負的情況,如果邊權存在負值則無法適用(若有負邊權的話,可以反覆走這條邊,這樣的話,路徑長度變成了無窮小,不存在最短路徑了),怎麼辦呢?Bellman-Ford演算法來解決!
主要思想:對所有的邊進行n-1輪鬆弛操作,因為在一個含有n個頂點的圖中,任意兩點之間的最短路徑最多包含n-1邊。換句話說,第1輪在對所有的邊進行鬆弛後,得到的是從1號頂點只能經過一條邊到達其餘各定點的最短路徑長度。第2輪在對所有的邊進行鬆弛後,得到的是從1號頂點只能經過兩條邊到達其餘各定點的最短路徑長度,……
[Bellman-Ford演算法概述(百度百科)](https://baike.baidu.com/item/Bellman-Ford%E7%AE%97%E6%B3%95/1089090?fr=aladdin)
Bellman - ford演算法是求含負權圖的單源最短路徑的一種演算法,效率較低,程式碼難度較小。其原理為連續進行鬆弛,在每次鬆弛時把每條邊都更新一下,若在n-1次鬆弛後還能更新,則說明圖中有負環,因此無法得出結果,否則就完成。

注意此演算法和Dijkstra演算法都只是適用於單源路徑,Bellman-Ford演算法可以含負權邊,而Dijkstra演算法則不能含有負權邊。

  • 參看劉汝佳的《演算法競賽入門經典(第二版)》363頁如下,幫助理解這個演算法~

首先確定一個事實:如果最短路存在,一定存在一個不含環的最短路。理由:在邊權可正可負的圖中,環有零環、正環和負環三種。如果包含零環或正環,去掉後路徑不會變長;如果包含負環,則意味著最短路不存在(負無窮)。既然不存在環,最短路最多隻經過(不算起點)n-1的節點,可以通過n-1輪的鬆弛操作得到。

  • Bellman-Ford演算法可以大致分為三個部分

第一,初始化所有點。每一個點儲存一個值,表示從原點到達這個點的距離,將原點的值設為0,其它的點的值設為無窮大(表示不可達)。
第二,進行迴圈,迴圈下標為從1到n-1(n等於圖中點的個數)。在迴圈內部,遍歷所有的邊,進行鬆弛計算。
第三,遍歷途中所有的邊(edge(u,v)),判斷是否存在這樣情況:
d(v) > d (u) + w(u,v)
則返回false,表示途中存在從源點可達的權為負的迴路。

【例題 最短路 HDU - 2544 】
在每年的校賽裡,所有進入決賽的同學都會獲得一件很漂亮的t-shirt。但是每當我們的工作人員把上百件的衣服從商店運回到賽場的時候,卻是非常累的!所以現在他們想要尋找最短的從商店到賽場的路線,你可以幫助他們嗎?

Input
輸入包括多組資料。每組資料第一行是兩個整數N、M(N<=100,M<=10000),N表示成都的大街上有幾個路口,標號為1的路口是商店所在地,標號為N的路口是賽場所在地,M則表示在成都有幾條路。N=M=0表示輸入結束。接下來M行,每行包括3個整數A,B,C(1<=A,B<=N,1<=C<=1000),表示在路口A與路口B之間有一條路,我們的工作人員需要C分鐘的時間走過這條路。
輸入保證至少存在1條商店到賽場的路線。
Output
對於每組輸入,輸出一行,表示工作人員從商店走到賽場的最短時間
Sample Input
2 1
1 2 3
3 3
1 2 5
2 3 5
3 1 2
0 0
Sample Output
3
2

【分析】
本題可以使用Dijkstra演算法程式設計,直接仿照Dijkstra講解部分及模板程式碼程式設計即可解決。這裡重點依據此水題來講解Bellman-Ford演算法的程式碼實現及其優化。
點此連結直擊本題程式碼參考博文

AC程式碼(非優化版本):

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;
const int INF=1<<30;
const int maxn=1005;
const int edge_maxn=20005;

int n,m;
int from,to,weight;
bool loop;
int dis[maxn];

typedef struct
{
    int s,e,w;
}Edge;

Edge edge[edge_maxn*2];//無向圖看成有向圖處理

void bellman_ford(int st)
{
    fill(dis,dis+maxn,INF);
    dis[st]=0;
    //step1:對邊進行鬆弛更新操作 
    for(int i=1;i<=n-1;i++)
    {
        bool ok=0;
        for(int j=1;j<=m;j++)
        {
            if(dis[edge[j].e]>dis[edge[j].s]+edge[j].w)
            {
                dis[edge[j].e]=dis[edge[j].s]+edge[j].w;
                ok=1;
            }
        }
        if(!ok)break;
    }
    //step2:判斷圖中是否有負權環
    loop=0;
    for(int i=1;i<=m;i++)
    {
        if(dis[edge[i].e]>dis[edge[i].s]+edge[i].w)
        {
            loop=1;
            break;
        }
    }
}

int main()
{
    while(cin>>n>>m)
    {
        if(n==0 && m==0)break;
        int cnt=1;
        for(int i=1;i<=m;i++)
        {
            cin>>from>>to>>weight;
            edge[cnt].s=edge[cnt+1].e=from;
            edge[cnt].e=edge[cnt+1].s=to;
            edge[cnt++].w=weight;
            edge[cnt++].w=weight;
        }
        m*=2;
        bellman_ford(1);
        cout<<dis[n]<<endl;
    }
    return 0;
}

AC程式碼(佇列優化版本):

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>

using namespace std;

const int INF=1<<30;
const int maxn=1005;

int n,m;
int from,to,weight;
int mp[maxn][maxn];
bool vis[maxn];
int dis[maxn];

void init()
{
    memset(vis,0,sizeof(vis));
    fill(mp[0],mp[0]+maxn*maxn,INF);
    fill(dis,dis+maxn,INF);
}

void bellman_ford(int st)
{
    dis[st]=0;
    queue<int>que;
    que.push(st);
    vis[st]=1;
    //取出隊首元素點,對其相鄰點進行鬆弛操作,如果在佇列外,則加入佇列,
    //和Dijkstra演算法的優化類似,也和bfs思想類似
    while(!que.empty())
    {
        int from=que.front();
        que.pop();
        vis[from]=0;
        for(int i=1;i<=n;i++)
        {
            if(dis[from]+mp[from][i]<dis[i])
            {
                dis[i]=dis[from]+mp[from][i];
                que.push(i);
                vis[i]=1;
            }
        }
    }
}

int main()
{
    while(cin>>n>>m)
    {
        init();
        if(n==0 && m==0)break;
        for(int i=1;i<=m;i++)
        {
            cin>>from>>to>>weight;
            mp[from][to]=mp[to][from]=weight;
        }
        bellman_ford(1);
        cout<<dis[n]<<endl;
    }
    return 0;
}

最短路徑演算法三之Floyd演算法

Dijkstra演算法和Bellman-Ford演算法都只適用於求單源路徑最短路問題,如果要求出任一點到其他所有點的最短路徑,不必呼叫n次Dijkstra演算法或者Bellman-Ford演算法,可以直接用Floyd演算法即可,程式碼非常簡短容易記憶,因為這個程式碼和矩陣乘法長的太像了啊233333333333333

【例題 依舊是講解Bellman-Ford演算法中用到的例題hdu2544】

AC程式碼:

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int inf=1e6+1;//floyd演算法中要注意inf的值,設定為
                    //比最短路徑長度上限恰好大一點點,
                    //因為該演算法中牽扯到mp陣列元素的加法
                    //運算,如果inf比較大,可能會溢位
const int maxn=1005;

int n,m;
int mp[maxn][maxn];//mp[i][j]表示i點到j點的最短距離

void init()//本演算法其實用了動態規劃的思想,對狀態初始化
{
    fill(mp[0],mp[0]+maxn*maxn,inf);
    for(int i=0;i<=n;i++)
        mp[i][i]=0;
}
//該演算法的實現程式碼形式上和矩陣乘法非常相似,便於記憶
void floyd()
{
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                if(mp[i][k]+mp[k][j]<mp[i][j])
                    mp[i][j]=mp[i][k]+mp[k][j];
}

int main()
{
    while(cin>>n>>m && n && m)
    {
        init();
        int from,to,weight;
        for(int i=1;i<=m;i++)
        {
            cin>>from>>to>>weight;
            mp[from][to]=mp[to][from]=weight;
        }
        floyd();
        cout<<mp[1][n]<<endl;
    }
    return 0;
}