1. 程式人生 > >最短路的四種演算法總結

最短路的四種演算法總結

標題 ##最短路的四種演算法

1、floyd
核心程式碼只有五行
for(int k=1; k<=n; k++)
for(int i=1; i<=n; i++)
for(int j=1; j<=n; j++)
{
if(map1[i][j]>map1[i][k]+map1[k][j])
map1[i][j]=map1[i][k]+map1[k][j];
}
這種演算法可以找多源最短路,想知道a點到b點最短路,只能加入中間點來縮短路徑,比如a到b 加入中間點k a到k到b
那麼我們可以這樣判斷,要知道i到j的最短路,我們只要判斷e[i][j]是否大於e[i][1]+e[1][j]即可,而中間值1則要用for迴圈從1到n遍歷一個遍,就是查詢所有中間值
以下為完整程式碼

//floyd
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
int main()
{
    int n,m,a,b,c;
    int map1[100][100];
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++)
        for(int j=1;j<=n;j++)
        if(i==j) map1[i][j]=0
; else map1[i][j]=inf; for(int i=1; i<=m; i++) { scanf("%d%d%d",&a,&b,&c); map1[a][b]=c; } for(int k=1; k<=n; k++) for(int i=1; i<=n; i++) for(int j=1; j<=n; j++) { if(map1[i][j]>map1[i][k]+map1[k][j]) map1[i][j]=map1[i][k]+map1[k][j]; } for
(int i=1; i<=n; i++) { for(int j=1;j<=n;j++) printf("%d ",map1[i][j]); printf("\n"); } return 0; }

2、dijkstra
這個演算法只能計算單元最短路,而且不能計算負權值,這個演算法是貪心的思想,
dis陣列用來儲存起始點到其他點的最短路,但開始時卻是存的起始點到其他點的初始路程。通過n-1遍的遍歷找最短。
比如1到3的最短路就是比較dis[3]與dis[2]+e[2][3],如果大於的話就更新dis[3]位dis[2]+e[2][3],這個專業術語叫鬆弛,這種演算法的核心思想就是通過邊來鬆弛一號頂點到其他定點的路程,這也就能解釋為什麼要遍歷n-1遍了。
book陣列用來標記,被標記的是已經找過的最短路,沒被標記的沒有被找過的最短路,當全部找過以後演算法結束,也就是說dis陣列中的數是起始點到其他所有點的最短路
以下為完整程式碼

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
int e[100][100],dis[100],book[100];//e用來記錄陣列,dis用來記錄初始點到各個點的位置,book用來標記,被標記的表示已經連線成最短路
int main()
{
    int n,m,t1,t2,t3,u,v,min1;
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++)
        for(int j=1; j<=n; j++)
            if(i==j) e[i][j]=0;
            else e[i][j]=inf;
    for(int i=1; i<=m; i++)
    {
        scanf("%d%d%d",&t1,&t2,&t3);
        e[t1][t2]=t3;
    }
    for(int i=1; i<=n; i++)
        dis[i]=e[1][i];
    for(int i=1; i<=n; i++)
        book[i]=0;
    book[1]=1;
    for(int i=1; i<=n-1; i++) //更新dis陣列
    {
        min1=inf;
        for(int j=1; j<=n; j++)
        {
            if(book[j]==0&&dis[j]<min1)//每次找最短的邊
            {
                min1=dis[j];
                u=j;
            }
        } 
            book[u]=1;
            for(int v=1; v<=n; v++)
            {
                if(e[u][v]<inf)
                    if(dis[v]>dis[u]+e[u][v])//如果從起始點到j的距離大於起始點到u的距離加上u到j的距離就更新,專業術語鬆弛操作
                        dis[v]=dis[u]+e[u][v];
            }
    }
    for(int i=1; i<=n; i++)
        printf("%d ",dis[i]);
    return 0;
}
下邊仍為dijkstra演算法,不過存地圖的方式從鄰接矩陣換成了鄰接表,鄰接表的時間複雜度更低,而且可以存兩點之間的多組資料,比如1點到2點之間可能存在2條路,每條路的權值不同
先介紹鄰接表,這裡給出啊哈磊的部落格,簡單易懂,連結
http://ahalei.blog.51cto.com/4767671/1391988
個人理解first陣列用來存以u[i]為頂點的最後一條邊的編號,next陣列存編號為i的邊的上一條邊,如果這個邊是以u[i]為頂點的第一條邊,則next[i]為-1。
創立
for(i=1;i<=n;i++)
    first[i]=-1;
for(i=1;i<=m;i++)
{
    scanf("%d %d %d",&u[i],&v[i],&w[i]);
    next[i]=first[u[i]];//next等於編號i的上一條邊
    first[u[i]]=i;
}
遍歷
for(i=1;i<=n;i++)
{
    k=first[i];
    while(k!=-1)
    {
        printf("%d %d %d\n",u[k],v[k],w[k]);
        k=next[k];
    }
}
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<queue>
using namespace std;
#define inf 0x3f3f3f3f
const int maxn=10005;
int num,first[maxn];
struct node
{
    int id;
    int len;
    int val;
}node1;
struct edge
{
    int id;
    int len;
    int val;
    int next;//作用跟next陣列一樣
}e[maxn];
priority_queue<node>q;
bool operator <(node a,node b)
{
    return a.len>b.len;
}
void add(int u,int v,int len,int val)
{
    e[num].id=v;//存下一個點,以邊找點
    e[num].val=val;
    e[num].len=len;
    e[num].next=first[u];
    first[u]=num;
    num++;
}
int main()
{
    node cur;
    int ans=inf;
    int k,n,r,s,d,l,t;
    scanf("%d%d%d",&k,&n,&r);
    memset(first,-1,sizeof(first));
    while(!q.empty()) q.pop();
    num=0;
    for(int i=0;i<r;i++)
    {
        scanf("%d%d%d%d",&s,&d,&l,&t);
        add(s,d,l,t);
    }
    node1.id=1;//起始點
    node1.len=0;
    node1.val=0;
    q.push(node1);
    while(!q.empty())
    {
        cur=q.top();
        q.pop();
        if(cur.id==n)
        {
            ans=cur.len;
            break;
        }
        for(int i=first[cur.id];i!=-1;i=e[i].next)
        {
            if(k>=cur.val+e[i].val)//只有符合要求的點才會進入佇列
            {
                node1.id=e[i].id;
                node1.len=e[i].len+cur.len;
                node1.val=e[i].val+cur.val;
                q.push(node1);
            }
        }
    }
    if(ans==inf)
        printf("-1\n");
    else
    printf("%d\n",ans);
}

4、bellman(可以計算負權值)
那麼先介紹一下負權迴路,如果是單向圖,那麼所有權值之和為負數,這是負權迴路。如果是無向圖只要有一個負權值,就不會存在最短路,跟負權迴路一個意思。
這個演算法一般用於有負權值的最短路,但時間複雜度也不高。
有負權值的最短路
有了負權值dijs這種演算法就不能用了,為什麼呢?
因為這種演算法是貪心的思想,每次鬆弛的的前提是用來鬆弛這條邊的最短路肯定是最短的。然而有負權值的時候這個前提不能得到保證,所以dijs這種演算法不能成立。
這裡給出一個部落格,看這個很清晰。
連結:http://blog.csdn.net/baidu_31818237/article/details/50611592
bellman的核心程式碼只有4行
for(int k=1;k<=n-1;k++)//進行n-1次鬆弛
for(int i=1;i<=m;i++)//列舉每一條邊
if(dis[v[i]]>dis[u[i]]+w[i])//嘗試鬆弛每一條邊
dis[v[i]]=dis[u[i]]+w[i];
這個演算法也是遍歷n-1遍找過所有的點,至於為什麼是n-1呢。dijs演算法n-1次遍歷是因為有n-1個點需要遍歷,這個也是因為最短路是一個不包含迴路的路徑,無論正負權迴路都不能有,那麼去掉迴路,n個點任意兩點之間就最多有n-1條邊。但是程式可能在不到n-1次迴圈就已經找到了所有最短路,說明這個是最壞情況下是n-1次遍歷。
dis同樣是存在起始點到各個頂點的最短路,這個與dijs不同的是,dijs每次找到最近的點進行鬆弛操作,而這個bellman則是隻要路程更短我就鬆弛。也是因為這樣才能用來解決負權值問題。
那麼怎麼來看有負權值迴路呢,如果有負權值迴路,那最短路就不會存在,因為最短路會越來與小。那麼在n-1輪鬆弛後,要是還能鬆弛就代表有負權值迴路。
下邊是完整程式碼

#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
#define inf 0x3f3f3f3f
int dis[100],u[100],v[100],w[100];
int main()
{
    int n,m,flag1,flag2;
    while(~scanf("%d%d",&n,&m))
    {
        for(int i=1; i<=m; i++)
            scanf("%d%d%d",&u[i],&v[i],&w[i]);
        for(int i=1; i<=n; i++)
            dis[i]=inf;
        dis[1]=0;
        for(int k=1; k<=n-1; k++)
        {
            flag1=0;//用於標記本輪鬆弛是否發生
            for(int i=1; i<=m; i++)
            {
                if(dis[v[i]]>dis[u[i]]+w[i])
                    dis[v[i]]=dis[u[i]]+w[i];
                flag1=1;//有更新
            }
            if(flag1==0)
                break;//dis陣列沒有更新,即沒有鬆弛,結束演算法
        }
        flag2=0;
        for(int i=1; i<=m; i++)
            if(dis[v[i]]>dis[u[i]]+w[i])
                flag2=1;
        if(flag2==1)
            printf("有負權迴路\n");
        else
        {
            for(int i=1; i<=n; i++)
                printf("%d ",dis[i]);
        }
        printf("\n");
    }
    return 0;
}

其實這個演算法可以在優化,因為有可能在n-1輪鬆弛還沒結束就已經是最短路,那麼用佇列進行優化。
4、spfa(佇列優化的bellman)
我沒找到兩者有什麼區別,本來spfa就是佇列優化而來的bellman, 我用了鄰接表,反正是要優化就優化到底,下邊還會給出優先佇列的spfa
程式碼
陣列模擬鄰接表

#include<stdio.h>
#include<string.h>
#include<queue>
#include<algorithm>
using namespace std;
int dis[100],u[100],v[100],w[100],vis[100],first[100],next[100],c[100];
int flag;
int spfa(int s,int n)
{
    queue<int>q;
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    memset(vis,0,sizeof(vis));
    memset(c,0,sizeof(c));
    q.push(s);
    vis[s]=1;
    flag=0;
    while(!q.empty())
    {
        int x;
        x=q.front();
        q.pop();
        vis[x]=0;
        //隊頭元素出隊,並且消除標記
        for(int k=first[x]; k!=0; k=next[k]) //遍歷頂點x的鄰接表
        {
            int y=v[k];
            if(dis[x]+w[k]<dis[y])
            {
                dis[y]=dis[x]+w[k];  //鬆弛
                if(!vis[y])  //頂點y不在隊內
                {
                    vis[y]=1;    //標記
                    c[y]++;      //統計次數
                    q.push(y);   //入隊
                    if(c[y]>n)  //超過入隊次數上限,說明有負環
                        return flag=0;
                }
            }
        }

    }
}
int main()
{
    int n,m;
    while(~scanf("%d%d",&n,&m))
    {
        memset(first,-1,sizeof(first));
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d%d",&u[i],&v[i],&w[i]);
            next[i]=first[u[i]];
            first[u[i]]=i;
        }
        spfa(1,n);
        if(flag)
            printf("有負權迴路\n");
        else
        {
            for(int i=1;i<=n;i++)
            printf("%d ",dis[i]);
        }
    }
}

這個是vector模擬鄰接表

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<map>
#include<queue>
#include<math.h>
#include<vector>
#include<iostream>
#define inf 0x3f3f3f3f
#define ll long long
#define N 100000+10
using namespace std;
int n,m;
int x,y,z;
bool v[100];
int d[100];
struct node
{
    int y,z;
};
vector<node>e[100];
void spfa(int x)
{
    memset(v,0,sizeof(v));
    d[x]=0;
    queue<int>q;
    q.push(x);
    v[x]=1;
    while(!q.empty())
    {
        int st=q.front();
        q.pop();
        v[st]=0;
        for(int i=0;i<e[st].size();i++)
        {
            if(d[st]+e[st][i].z<d[e[st][i].y])
            {
                d[e[st][i].y]=d[st]+e[st][i].z;
                if(!v[e[st][i].y])
                {
                    q.push(e[st][i].y);
                    v[e[st][i].y]=1;
                }
            }
        }
    }
}
int main()
{
    scanf("%d%d",&n,&m);
   memset(d,inf,sizeof(d));
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&x,&y,&z);
       e[x].push_back((node){y,z});
       e[y].push_back((node){x,z});
    //存入無向圖
    }
    spfa(1);
    for(int i=1;i<=n;i++)
    {
        printf("%d ",d[i]);
    }
}

最後著重說一下鄰接表可以用陣列也可以用vector
實現
e[]這個陣列,e[i][j]i為起點,j為終止點的標號,e[i][j].y就是終止點
例如
j:0 1 2 j只是標號第幾個
i:1 (2,3 3,2 4,4) 括號內的東西就是終止點和權值,e[1][0].y=2(終止點),e[1][0].z=3(權值)
i:2 (1,2 3,5 4,6)
i:3 (1,2 2,4 4,5)
這樣也可以實現連結串列,並且可以存無向圖,陣列實現的鄰接表存無向圖很麻煩