1. 程式人生 > >每一對頂點之間的最短路徑【Floyd】

每一對頂點之間的最短路徑【Floyd】

演算法總結:

①Dijkstra演算法用的是貪心策略,每次都找當前最短路徑的下一個最短距離點。所以不適合帶有負權的情況。至於時間效率通過各種優化可以到達不同的程度。但是樸素的Dijkstra演算法永遠是最穩定的。

②Bellman-Ford演算法是Dijkstra的一種變式,它摒棄了貪心的策略,但是每次都需要鬆弛所有的路徑,所以也適合負權的情況。但是時間效率較低。有資料顯示,Bellman-Ford演算法也可以應用貪心策略,這屬於高階技巧,這裡不予考慮。

③Floyd演算法用的是動態規劃的思想,由於是不定源點,所以它需要對所有的情況進行鬆弛操作,最終獲得所有的最短路徑長度。由於需要遍歷,所以它的時間效率比較低。但是遍歷策略好的話,還是能很快得到想要的結果。

SPFA演算法是用FIFO佇列優化的Bellman-Ford演算法,所以支援負權的情況,由於已經將需要考慮的節點放入佇列中,所以避免了很多不必要的鬆弛,時間效率也相對較高。由於佇列中無法進行修改,所以時間效率相對不穩定。

⑤所有的佇列優化其實都能轉化為hash優化,這裡不再考慮更進一步的優化。

 -----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

一、Dijkstra演算法:單源到所有節點的最短路演算法。

演算法要求:可以是無向圖或有向圖,所有邊權均為正,最短路存在。如果圖是五環圖,則最長路也一定存在,但是如果是有環圖,則最長路不一定存在。

演算法思想:每次比較所有的從源點可以到達的路的長度,然後選出最短的一條放入源點集合,然後更新最短路徑長度。這樣,當所有的點都在源點集合中時,得到的長度就是最短路長度。

1)使用鄰接矩陣的稠密圖普通Dijkstra演算法

         演算法時間複雜度:O(n^2)

         演算法程式碼:

#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
#define INF (1<<31)-1   //int最大值
#define MAXN 100   //最大節點數
int dist[MAXN],c[MAXN][MAXN];
//dist[i]存放從源點到i節點的最短距離,c陣列用來表示圖
int n,line;   //n為節點數,line為邊數

//迪傑斯特拉演算法主演算法模組
void Dijkstra(int v)   //v代表源點
{
    inti,j,temp,u;  //temp用於暫時存放最短路徑長度
    boolvis[MAXN];   //判定重複標記陣列
    for(i=0; i<n; i++) //dist陣列與vis陣列的初始化
    {
        dist[i]=c[v][i];
        vis[i]=0;
    }
    dist[v]=0;  //源點到源點的距離設為0
    vis[v]=1;   //源點預設為已經訪問
    for(i=0; i<n; i++) //總共應該遍歷n次
    {
        temp=INF;  //初始化temp為最大值
        u=v;   //初始化最短節點為當前節點
        for(j=0; j<n; j++)if((!vis[j])&&dist[j]<temp)
            {
                u=j;   //找出v距離最短的節點u
                temp=dist[j];   //更新最短距離
            }
        if(temp==INF)return;   //不存在其它節點了,返回
        vis[u]=1;   //標記該最短節點為已經訪問
        for(j=0; j<n; j++)if((!vis[j])&&dist[u]+c[u][v]<dist[j]) //鬆弛操作
                dist[j]=dist[u]+c[u][v];  //更新最短距離
    }
}
int main()
{
    int p,q,len,i,j,origin;
    while(scanf(“%d%d”,&n,&line))
    {
        if(!n&&!line) break;  //如果圖為空白圖,則結束
        for(i=0; i<n; i++)
            for(j=0; j<n; j++)
                c[i][j]=INF;   //初始化節點間距離,預設為節點間全部不連通
        while(line--)   //輸入這line條邊的內容
        {
            scanf(“%d%d%d”,&p,&q,&len);
//p為邊的起點,q為邊的終點,len為邊的長度
            //下面是無向圖的輸入方式
            if(len<c[p][q])
            {
                c[p][q]=len;
                c[q][p]=len;
            }
            /*下面是有向圖的輸入方式
            if(len<c[p][q])
                   c[p][q]=lem;
            以上是有向圖的輸入方式*/
        }
        for(i=0; i<n; i++)           dist[i]=INF;   //初始化最短距離陣列為最大長度
        scanf(“%d”,&origin);
        Dijkstra(origin);  //最短路演算法呼叫
        printf(“%d\n”,dist[n]);
    }
}

2)使用鄰接表的稀疏圖的普通Dijkstra演算法

演算法時間複雜度:O(m*log(n))

演算法程式碼:(程式碼以無向圖為例,有向圖部分以註釋形式寫在程式碼中)

###標頭檔案以及巨集定義部分省略###
int first[MAXN];  //first[u]儲存節點u的第一條邊的編號
int u[MAXN],v[MAXN],w[MAXN];
//u[i]表示第i條邊的一端端點,v[i]表示第i條邊的另一端端點,w[i]表示第i條邊的長度
int next[2*MAXN];  //next[i]表示第i條邊的下一條邊的編號【無向圖】
//int next[MAXN]; next[i]表示第i條邊的下一條邊的編號。【有向圖】
#########省略部分#########
scanf(“%d%d”,&n,&m);
for(i=0; i<n; i++) first[i]=-1; //初始化連結串列表頭
for(i=0; i<2*line; i++) //輸入2*line條邊,每次將邊首插法插入連結串列表頭(避免遍歷連結串列)。
//如果是有向圖,那麼只有line條邊,即i<line。
{
    scanf(“%d%d%d”,&u[i],&v[i],&w[i]);
    next[i]=first[u[i]];
    first[u[i]]=i;
    //以下只有無向圖才有
    i++;
    u[i]=v[i-1];
    v[i]=u[i-1];
    next[i]=first[u[i]];
    first[u[i]]=i;
    //以上只有無向圖才有
}
//對應的Dijkstra演算法
void Dijkstra(int x)   //v代表源點
{
    inti,j,temp,minu;  //temp用於暫時存放最短路徑長度
    boolvis[MAXN];   //判定重複標記陣列
    for(i=first[x]; i!=-1; i=next[i]) //dist陣列與vis陣列的初始化
    {
        dist[v[i]]=w[i];
        vis[v[i]]=0;
    }
    dist[x]=0;  //源點到源點的距離設為0
    vis[x]=1;   //源點預設為已經訪問
    for(i=0; i<n; i++) //總共應該遍歷n次
    {
        temp=INF;  //初始化temp為最大值
        minu=x;   //初始化最短節點為當前節點
        for(j=first[x]; j!=-1; j=next[j])
        {
            if((!vis[v[j]])&&dist[v[j]]<temp)
            {
                minu=j;   //找出v距離最短的節點uu
                temp=dist[v[j]];   //更新最短距離
            }
        }
        if(temp==INF)return;   //不存在其它節點了,返回
        vis[minu]=1;   //標記該最短節點為已經訪問
        for(j=first[x]; j!=-1; j=next[j])if((!vis[v[j]])&&dist[minu]+w[j]<dist[v[j]]) //鬆弛操作
                dist[v[j]]=dist[minu]+w[j];  //更新最短距離
    }
}


3)使用優先佇列的Dijkstra優化演算法

演算法時間複雜度:O(n*lgn+ m)。一般情況下最快。

演算法程式碼:

#include <queue>   //需要優先佇列
#include <utility>   //需要pair型別
######其他標頭檔案已經巨集定義省略######
int first[MAXN];  //first[u]儲存節點u的第一條邊的編號
int u[MAXN],v[MAXN],w[MAXN];
//u[i]表示第i條邊的一端端點,v[i]表示第i條邊的另一端端點,w[i]表示第i條邊的長度
int next[2*MAXN];  //next[i]表示第i條邊的下一條邊的編號【無向圖】
//int next[MAXN]; next[i]表示第i條邊的下一條邊的編號。【有向圖】
#########省略部分#########
scanf(“%d%d”,&n,&m);
for(i=0;i<n;i++) first[i]=-1;   //初始化連結串列表頭
for(i=0;i<2*line;i++)  //輸入2*line條邊,每次將邊首插法插入連結串列表頭(避免遍歷連結串列)。
//如果是有向圖,那麼只有line條邊,即i<line。
{
       scanf(“%d%d%d”,&u[i],&v[i],&w[i]);
       next[i]=first[u[i]];
       first[u[i]]=i;
       //以下只有無向圖才有
       i++;
       u[i]=v[i-1];
       v[i]=u[i-1];
next[i]=first[u[i]];
       first[u[i]]=i;
       //以上只有無向圖才有
}
typedef pair<int,int> pii;  //定義雙物件型別
priority_queue<pii,vector<pii>,greater<pii>> pq;   //宣告最小出隊的優先佇列
//對應的Dijkstra演算法
int Dijkstra(int x)
{
       inti,j;
       pq.push(make_pair(d[x],x);
       while(!pq.empty())
       {
              piiminu=pq.top();pq.pop();
              intx=minu.second;
              if(minu.first!=dist[x])continue;
              for(inti=first[x];i!=-1;i=next[i]) if(d[v[i]]>dist[x]+w[i])
              {
                     dist[v[i]]=dist[x]+w[i];
                     pq.push(make_pair(d[v[i]],v[i]));
              }
       }
}


二、Bellman-Ford演算法:單源到所有節點的最短最長路演算法。

演算法要求:可以使無向圖或者有向圖。邊權可以存在負值。當然最短路不一定存在,當最短路存在時,可以求出最短路長度。如果圖為有環圖,則最短路不一定存在,最長路也不一定存在。如有負權則輸出錯誤提示。也適合求解約束拆分系統的不等式問題。

演算法思想:如果最短路存在,一定存在一個不含環的最短路。在邊權可正可負的圖中,環有零環、正環、負環3種。如果包含零環或正環,去掉以後路徑不會變長;如果包含負環,則最短路不存在。可以通過n-1輪鬆弛操作得到。

1)樸素的BF演算法。

演算法時間複雜度:O(mn)

演算法程式碼:

const int N =205;
const int M =20005;
const int MAXN = 1000000000
int dist[N];
 
//自定義邊的結構體
struct edge{int u,v,w;}e[M];  //u,v分別是邊的兩端點,w為邊長度
//初始化dist陣列
void init(int vs,int s)
{
       inti;
       for(i=0;i<vs;i++)
              dist[i]=MAXN;
       dist[s]=0;
       return;
}
//鬆弛操作,成功返回true,失敗返回false
bool relax(int u,int v,int w)
{
       if(dist[v]>dist[u]+w)
       {
              dist[v]=dist[u]+w;
              returntrue;
}
return false;
}
//BF主演算法模組,返回false表示演算法失敗,圖中存在負環。
bool bellmanFord(int es,int vs,int s)   //es表示邊數,vs表示點數,s表示起點
{
       inti,j;
       init(vs,s);
       boolflag;
       for(i=0;i<vs-1;i++)   //應進行vs-1次鬆弛操作
       {
              flag=false;
              for(j=0;j<es;j++)
       if(relax(e[j].u,e[j].v,e[j].w))
              flag=true;
return flag;
}
}
 
int main()
{
       intn,m,i;
       while(scanf(“%d%d”,&n,&m)!=EOF)
{
       for(i=0;i<m;i++)
       {
              scanf(“%d%d%d”,&e[i].u,&e[i].v,&e[i].w);
              e[m+i].u=e[i].v;
              e[m+i].v=e[i].u;
              e[m+i].w=e[i].w;
}
if(bellmanFord(m<<1,n,1))
printf(“%d\n”,dist[n]);
else printf(“No\n”);
}
return 0;
}
 


(2)使用FIFO佇列的優化BF演算法(使用鄰接表)

演算法時間複雜度:O(mn)

演算法程式碼:

#include <queue>
#define INF (1<<31)-1
######其他標頭檔案以及巨集定義省略######
int first[MAXN];  //first[u]儲存節點u的第一條邊的編號
int u[MAXN],v[MAXN],w[MAXN];
//u[i]表示第i條邊的一端端點,v[i]表示第i條邊的另一端端點,w[i]表示第i條邊的長度
int next[2*MAXN];  //next[i]表示第i條邊的下一條邊的編號【無向圖】
//int next[MAXN]; next[i]表示第i條邊的下一條邊的編號。【有向圖】
#########省略部分#########
queue<int> q;
bool inq[MAXN];
bool bellmanFord(int x)
{
    int i,j;
    bool ans;
    for(i=0; i<n; i++)d[i]=!i?0:INF;
    memset(inq,0,sizeof(inq));   //在佇列中的標誌
    q.push(x);
    ans=false;
    while(!q.empty())
    {
        int x=q.front();
        q.pop();
        inq[x]=false;
        for(i=first[x]; i!=-1; i=next[i]) if(dist[v[e]]>dist[x]+w[e])
            {
                dist[v[e]]=dist[x]+w[e];
                ans=true;
                if(!inq[v[e]])
                {
                    inq[v[e]]=true;
                    q.push(v[e]);
                }
            }
    }
    return ans;
}


三、Floyd演算法:任意兩點之間的最短路

演算法思想:動態規劃。

時間複雜度:O(n^3)

演算法程式碼:

#define MAXN 100

#define INF (1<<31)-1

int n,m,p,q;

int f[MAXN+10][MAXN+10];

void Floyd()

{

       inti,j,k;

       for(k=0;k<n;k++)

{

       for(i=0;i<n;i++)

       {

              for(j=0;j<n;j++)

{

       if(f[i][k]+f[k][j]<f[i][j])

              f[i][j]=f[i][k]+f[k][j];

}

}

}

if(f[0][n-1]==INF) printf(“0\n”);

else printf(“%d\n”,f[0][n-1]);

}

int main()

{

       inta,b,c,i;

       while(~scanf(“%d%d”,&n,&m))

{

       if(!n&&!m) break;

       for(p=0;p<n;p++)

              for(q=0;q<n;q++)

                     f[p][q]=INF;

       for(i=0;i<m;i++)

{

       scanf(“%d%d%d”,&a,&b,&c);

       f[a][b]=c;

       f[b][a]=c;

}

floyd();

}

return 0;

}

四、SPFA演算法:單源點最短路的高效實用演算法

演算法要求:無特殊要求

演算法思想:用FIFO佇列優化的BF演算法。

演算法時間複雜度:O(k|E|),k為常數,一般k<=2

演算法程式碼:

#include <queue>

#######省略部分######

#define INF (1<<31)-1

#define N 1010

int dist[N],n,m;

int edge[N][N];

bool vis[N];

void spfa(int s)

{

       inti,u;

       memset(vis,false,sizeof(vis));

       for(i=0;i<n;i++)dist[i]=INF;

       queue<int>q;

       q.push(s);

vis[s]=true;

dist[s]=0;

while(!q.empty())

{

       u=q.front();

       q.pop();

       vis[u]=false;

       for(i=0;i<n;i++)

{

       if(dist[i]>dist[u]+edge[u][i])

              dist[i]=dist[u]+edge[u][i];

       if(!vis[i])

{

       vis[i]=true;

       q.push(i);

}

}

}

}

int main()

{

       inti,j,a,b,c,origin;

while(scanf(“%d%d”,&n,&m)!=EOF&&(n||m))

{

       for(i=0;i<N;i++)

              for(j=0;j<i;j++)

{

       edge[i][j]=INF;

       edge[j][i]=INF;

}

},

for(i=0;i<m;i++)

{

       scanf(“%d%d%d”,&a,&b,&c);

       if(edge[a][b]>c)

       {

              edge[a][b]=c;

              edge[b][a]=c;

}

scanf(“%d”,&origin);

spfa(origin);

printf(“%d\n”,dist[n]);

}

return 0;

}