最短路演算法的總結

徒手裝機甲發表於2020-12-08

因為各種原因鴿了一個月的部落格。今天就把三種最短路都簡單歸納一下記錄一下。

首先介紹最短路的背景

最短路問題是圖論理論的一個經典問題。尋找最短路徑就是在指定網路中兩結點間找一條距離最小的路。最短路不僅僅指一般地理意義上的距離最短,還可以引申到其它的度量,如時間、費用、線路容量等。
最短路徑演算法的選擇與實現是通道路線設計的基礎,最短路徑演算法是電腦科學與地理資訊科學等領域的研究熱點,很多網路相關問題均可納入最短路徑問題的範疇之中。經典的圖論與不斷髮展完善的計算機資料結構及演算法的有效結合使得新的最短路徑演算法不斷湧現。對最短路問題的研究早在上個世紀60年代以前就卓有成效了,其中對賦權圖的有效演算法是由荷蘭著名計算機專家E.W.Dijkstra在1959年首次提出的,該演算法能夠解決兩指定點間的最短路,也可以求解圖G中一特定點到其它各頂點的最短路。後來海斯在Dijkstra演算法的基礎之上提出了海斯演算法。但這兩種演算法都不能解決含有負權的圖的最短路問題。因此由Ford提出了Ford演算法,它能有效地解決含有負權的最短路問題。

1.Floyd演算法
floyd可以在O(n3)的時間複雜度,O(n2)的空間複雜度下求解正權圖中任意兩點間的最短路長度.它的本質是動態規劃.
定義f[k][i][j]表示從i出發,途中只允許經過編號小於等於k的點時的最短路.(i,j可以大於k但i到j的路徑上的其他點必須編號小於等於k).
轉移時從第k層的DP陣列f[k][][]求解第k+1層的DP陣列f[k+1][i][j].我們可以將f[k+1][][]全部初始化為inf
一條路徑如果保證中轉的點編號小於等於k,那麼一定也滿足經過的點的編號小於等於k+1.於是可以先將上一層的dp陣列直接複製到第k+1層,f[k+1][i][j]=f[k][i][j].
接下來考慮經過了第k+1個點作為中轉點的最短路.我們列舉(i,j),i!=k+1,j!=k+1,然後令f[k+1][i][j]=min(f[k+1][i][j],f[k][i][k+1]+f[k][k+1][j]).
直接這麼寫的空間複雜度是O(n3),接下來我們把空間壓到O(n2).i,j這兩維都是壓不掉的,所以我們把k這一維壓掉.f[i][j]現在存的是f[k][i][j].接下來我們把f[i][j]進行更新使得它裡面的數值變為f[k+1][i][j]
程式碼實現也很簡單

#include<bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 100;
int map1[maxn][maxn];
int u,v,w;
int main(){
    int n,m,i,j,k;
    memset(map1,inf,sizeof(map1));
    for(i = 1;i<=n;i++){
        map1[i][i] = 0;
    } 
    while(m--){
        scanf("%d %d %d",&u,&v,&w);
        map1[u][v] = map1[v][u] = w;
    }
    for(k = 1;k<=n;k++){
        for(i = 1;i<=n;i++){
            for(k = 1;k<=n;k++){
                if(map1[i][j]>map1[i][k]+map1[k][j]){
                    map1[i][j]>map1[i][k]+map1[k][j];
                }
            }
        }
    }
    printf("%d\n",map1[1][n]);
} 

這裡選擇了使用鄰接矩陣存圖,原因是Floyd演算法複雜度較高,一般用於解決規模較小的問題,所以不需要更高效率的儲存工具,鄰接矩陣可以很好的完成這個任務。

2.dijstra演算法

Dijkstra演算法採用的是一種貪心的策略,宣告一個陣列dis來儲存源點到各個頂點的最短距離和一個儲存已經找到了最短路徑的頂點的集合:T,初始時,原點 s 的路徑權重被賦為 0 (dis[s] = 0)。若對於頂點 s 存在能直接到達的邊(s,m),則把dis[m]設為w(s, m),同時把所有其他(s不能直接到達的)頂點的路徑長度設為無窮大。初始時,集合T只有頂點s。
然後,從dis陣列選擇最小值,則該值就是源點s到該值對應的頂點的最短路徑,並且把該點加入到T中,OK,此時完成一個頂點,
然後,我們需要看看新加入的頂點是否可以到達其他頂點並且看看通過該頂點到達其他點的路徑長度是否比源點直接到達短,如果是,那麼就替換這些頂點在dis中的值。
然後,又從dis中找出最小值,重複上述動作,直到T中包含了圖的所有頂點。

在這裡插入圖片描述

演算法的思路也很清晰,我們先嚐試用鄰接矩陣來實現這個演算法

#include<bits/stdc++.h>
using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 1000;
int map1[maxn][maxn],dis[maxn];
bool vis[maxn]={false};
int u,v,w,n;
void Dijkstra(int start)
{    
    vis[start]=true;
    for(int i=0;i<n;i++)
        dis[i]=map1[start][i];
        dis[start]=0;
    for(int i=0;i<n;i++)//進行n次操作,i無實際意義 
    {
        int min1=inf;//最小值記錄 
        int now=-1;//當前最短的點
        for(int j=0;j<n;j++)//找到當前最短的dis[j] 
        {
            if(!vis[j]&&dis[j]<min1)
            {
                min1=dis[j];
                now=j;        
            }    
        } 
        if(now==-1)
            return ;
        vis[now]=true;
        for(int j=0;j<n;j++)
        {
            if(!vis[j]&&map1[now][j])
            {
                if(dis[j]>map1[now][j]+dis[now])
                    dis[j]=map1[now][j]+dis[now];
            }    
        } 
    }
}
int main()
{
    int m,s;
    cin>>n>>m>>s;//n個點,m條變,從s開始 
    for(int i=0;i<maxn;i++)
    for(int j=0;j<maxn;j++)
    map1[i][j]=inf;
    for(int i=0;i<m;i++)
    {
        cin>>u>>v>>w;
        map1[u][v]=w;
    }
    Dijkstra(s);//從s開始 
    for(int i=0;i<n;i++)
    {
        cout<<dis[i]<<endl;
    } 
    return 0;
} 

可以發現這個演算法的複雜度是O(n²)的雖然已經很優秀了,但是在面對一些更復雜問題的時候,這個複雜度還不夠理想,於是我們想優化這個演算法首先可以從空間上優化,我們可以用vector模擬連結串列儲存從每個點出發的所有邊的終點和權值。然後用優先佇列維護每次選中的最短邊。用vis或if (d[v] < p.first) continue;來防止多次重複,按bfs的思想結合優先佇列,可以讓這個演算法的複雜度降低到O(nlogn)基本可以滿足所有問題,就算求多源最短路,我們只用跑n次就能做到比Floyd優秀。

#include<bits/stdc++.h>
using namespace std;
int s, t, n, m, q;
const int MAXV = 100005;
struct edge
{
    int to, cost;
};
typedef pair<int, int> P; //first是最短距離,second是頂點的編號
int V;//頂點個數
vector<edge> G[MAXV];
int d[MAXV];
int vis[MAXV];
void Dijkstra(int s)
{
    priority_queue<P, vector<P>, greater<P> > que;
    for(int i = 1; i <= 100000; i++)
        d[i] = 2000000000;
    d[s] = 0;
    que.push(P(0, s)); //把起點推入佇列
    while(!que.empty())
    {
        P p = que.top();
        que.pop();
        int v = p.second; //頂點的編號
        /*if(vis[v])continue;//防止節點的重複擴充套件
        vis[v]=1;*/
        if (d[v] < p.first) continue;
        for(int i = 0; i < G[v].size(); i++)
        {
            edge e = G[v][i];
            if (d[e.to] > d[v] + e.cost)
            {
                d[e.to] = d[v] + e.cost;
                que.push(P(d[e.to], e.to));
            }
        }
    }
}
int main()
{
    cin >> n >> m >> s;
    for(int i = 1; i <= n; i++)
        G[i].clear();
    edge temp;
    for(int i = 1; i <= m; i++)
    {
        cin >> q >> temp.to >> temp.cost;
        G[q].push_back(temp);
    }
    Dijkstra(s);
    for(int i = 1; i <= n; i++)
    {
        if(d[i] != 2000000000)cout << d[i] << " ";
        else cout << 2147483647 << " ";
    }
    cout << endl;
    return 0;
}

如果追求更優秀的效率,那麼用鏈式前向星存圖大約可以優化五分之一左右的常數,程式碼也附在樓下

#include<bits/stdc++.h>

const int MaxN = 100010, MaxM = 500010;

struct edge
{
    int to, dis, next;
};

edge e[MaxM];
int head[MaxN], dis[MaxN], cnt;
bool vis[MaxN];
int n, m, s;

inline void add_edge( int u, int v, int d )
{
    cnt++;
    e[cnt].dis = d;
    e[cnt].to = v;
    e[cnt].next = head[u];
    head[u] = cnt;
}

struct node
{
    int dis;
    int pos;
    bool operator <( const node &x )const
    {
        return x.dis < dis;
    }
};

std::priority_queue<node> q;

inline void dijkstra()
{
    dis[s] = 0;
    q.push( ( node )
    {
        0, s
    } );
    while( !q.empty() )
    {
        node tmp = q.top();
        q.pop();
        int x = tmp.pos, d = tmp.dis;
        if( vis[x] )
            continue;
        vis[x] = 1;
        for( int i = head[x]; i; i = e[i].next )
        {
            int y = e[i].to;
            if( dis[y] > dis[x] + e[i].dis )
            {
                dis[y] = dis[x] + e[i].dis;
                if( !vis[y] )
                {
                    q.push( ( node )
                    {
                        dis[y], y
                    } );
                }
            }
        }
    }
}


int main()
{
    scanf( "%d%d%d", &n, &m, &s );
    for(int i = 1; i <= n; ++i)dis[i] = 0x7fffffff;
    for( register int i = 0; i < m; ++i )
    {
        register int u, v, d;
        scanf( "%d%d%d", &u, &v, &d );
        add_edge( u, v, d );
    }
    dijkstra();
    for( int i = 1; i <= n; i++ )
        printf( "%d ", dis[i] );
    return 0;
}

3.spfa演算法

演算法思路
我們用陣列d記錄每個結點的最短路徑估計值,用鄰接表來儲存圖G。我們採取的方法是動態逼近法:設立一個先進先出的佇列用來儲存待優化的結點,優化時每次取出隊首結點u,並且用u點當前的最短路徑估計值對離開u點所指向的結點v進行鬆弛操作,如果v點的最短路徑估計值有所調整,且v點不在當前的佇列中,就將v點放入隊尾。這樣不斷從佇列中取出結點來進行鬆弛操作,直至佇列空為止。

很多時候我們並不需要那麼多無用的鬆弛操作。這時候就不再看邊了,而是看點,如果dis[i]重新整理了,那麼說明這個i點可以為我們接下來的鬆弛提供幫助,我們應該將它記錄起來。

那麼我們用佇列來維護“可能會引起鬆弛操作的點”,就可以只訪問必要的邊了,從而省去沒必要的操作。

還要注意一點,這個佇列裡的元素是可以重複入隊的,因為一個點的最短路徑可以不斷更新。另外,當一個點已經在佇列裡的時候,我們是不用將它入隊的,因為我們記錄這個點的目的是等會要使用它,所以我們記的是它的編號。 這裡一定要和Dij的堆優化分清楚,Dij堆優化的佇列裡面存的是路徑長度,我們這裡不需要,所以就算這個點的最短路徑中間可能會變,但是不影響我們的結果,我們只需要記住這個點可能可以作為鬆弛的中轉點,也就是我們可以用它進行鬆弛,就夠了。

但是這個演算法的問題在於,面對菊花圖的資料,它的複雜度會降低到O(n²),所以如果沒有負邊權的情況下,最好不要使用

程式碼如下

#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
struct node
{
    int v, w, next;
}edge[5000005];
int n, tot;
int head[1000005], dis[1000005];
bool vis[1000005];
void add_edge(int u,int v,int w)
{
    tot++;
    edge[tot].v = v;
    edge[tot].w = w;
    edge[tot].next = head[u];
    head[u]=tot;
}
void spfa(int x)
{
    queue <int> q;
    for(int i=1;i<=n;i++)
    {
        dis[i] = INF;
        vis[i] = 0;
    }
    dis[x] = 0;
    vis[x] = 1;
    q.push(x);
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        vis[u] = 0;
        for(int i=head[u]; i; i=edge[i].next)
        {
            int v = edge[i].v;
            if(dis[v] > dis[u]+edge[i].w)
            {
                dis[v] = dis[u]+edge[i].w;
                if(!vis[v])
                {
                    vis[v] = 1;
                    q.push(v);
                }
            }
        }
    }
}
int main()
{
    int m;
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    cin >> n >> m;
    tot = 0;
    while(m--)
    {
        int u, v;
        cin >> u >> v;
        add_edge(u, v, 1);
        add_edge(v, u, 1);
    }
    spfa(1);
    for(int i=1; i<=n; i++)
        cout << i << " " << dis[i] << endl;
    return 0;
}

總結,實際上關於什麼時候選取那個最短路路演算法,很容易就能看出來,如果資料規模小且是多源最短路,那麼無論如何都可以選取Floyd演算法。如果有負邊權而且資料範圍較大,用spfa演算法,其他情況都可以用dijkstar演算法解決,不管是多源最短路還是單源最短路。他都是穩定且優秀的最佳演算法。

相關文章