最短路演算法

BSS梅者如西發表於2024-10-10

本篇文章將介紹一些關於最短路的基本演算法。

Floyd演算法

處理全源最短路問題,時間複雜度較高,容易實現,時間複雜度O(N^3)

點選檢視程式碼
//n表示節點總數,f[i][j]表示i到j的最短路徑
for(int k=1;k<=n;k++){//表示僅允許經過節點1-k
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            f[i][j]=min(f[i][j],f[i][k]+f[k][j]);//比較經過k點與不經過k點的最短路
        }
    }
}

Bellman_ford演算法

基於鬆弛原理的演算法,可以帶負邊權。
鬆弛原理即對於邊(u,v),有
dis[v]=min(dis[v],dis[u]+w[u,v])即我們判斷將經過v與不經過v的路徑進行比較,取到更優的路徑,更新為最優的路徑
設這個圖有m條邊與n個節點,因為每次鬆弛操作要對每一條邊都進行鬆弛操作,進行m次,最短路最多有n-1條邊,最多進行n-1次鬆弛,所以此演算法的時間複雜度為o(nm)
因為此演算法時間複雜度與佇列最佳化後的SPFA演算法的最壞時間複雜度相同,所以可以優先使用SPFA演算法的程式碼,Bellman_ford演算法程式碼實現略。
此演算法可以判斷負環,如果第n次鬆弛仍能找到可以鬆弛的邊,則此圖有負環。(此做法只能判斷由源點出發能否到達出發,更為嚴謹的做法是建立一個超級源點,將此點與所有點連線一條權值為0的邊)

SPFA演算法

Bellman_ford演算法的佇列最佳化,我們發現不是每個點都需要鬆弛,而是被鬆弛後的點所連線的邊才可能引起下一次鬆弛,我們用佇列記錄可能被鬆弛的點。
SPFA演算法同樣也可以判斷負環,需要記錄最短路經過多少邊,如果最短路經過n條邊,則說明此圖有負環。
此演算法的最壞時間複雜度為O(nm),可以製作hack資料來卡掉此演算法,所以在沒有負邊權時儘量使用Dijkstra演算法。但若題目中有負邊權且無特殊條件下,SPFA演算法作為正解不應被hack。

點選檢視程式碼
struct edge{
    int v,w;
}g[N];
int dis[N],vis[N],cnt[N];
queue<int>q;
bool spfa(int s,int n){
    memset(vis,0,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    dis[s]=0;
    q.push(s);
    vis[s]=1;
    while(!q.empty()){
        int u=q.front();q.pop();
        vis[u]=0;
        for(auto x:g[u]){
            int v=x.v;w=x.w;
            if(dis[v]>dis[u]+w){//鬆弛操作
                dis[v]=dis[u]+w;
                cnt[v]=cnt[u]+1;//記錄最短路邊數
                if(cnt[v]>=n) return false;//判斷負環
                if(!vis[v]) q.push(v),vis[v]=1;
            }
        }
    }
    return true;
}

Dijkstra演算法

同樣要用到鬆弛原理,只能在非負邊權圖中使用。
考慮一種貪心,我們定義集合p為已確定最短路的點集,集合q為未確定最短路的點集,一點dis值為起點到此點的距離,我們先將起點的dis值設為0,其他都是正無窮,我們在集合q中找到dis值最短的點,將此點加入集合p,對此點進行鬆弛操作,直到所有點都被加入集合p。
我們可以證明當前貪心策略正確,因為全域性的邊權都為非負數,全域性最小值無法被其他節點更新,所以當我們找到dis值最小的點時,他已經是起點到此點的最短路徑,我們不斷用全域性最小值進行擴充,最終可以得到所有點的最短路徑。
在稠密圖時可直接運用暴力做法做最優,時間複雜度為O(n^2),稀疏圖是則需要使用優先佇列最佳化,時間複雜度為O(m logm)
下面僅給出實現難度略大的優先佇列最佳化做法

點選檢視程式碼
struct node{
    int u,v,w;
};
vector<node> g[N];
int vis[N],dis[N];
struct T{
    int d,u;
    friend bool operator<(T a,T b){
        //重構運算子<(重構>會編譯錯誤),因為優先佇列預設從大到小排序,相反重構可以讓他從小到大排序
        return a.d>b.d;
    }
};
priority_queue< T > q;
void dijkstra(int start){
    memset(vis,0,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    dis[start]=0;//初始化起點的dis值為0
    q.push((T){0,start});//
    while(!q.empty()){
        T t=q.top();
        q.pop();//取出當前路徑最短的點
        int u=t.u;
        if(vis[u]) continue;//如果該點已經找到最短路徑則無須在繼續
        vis[u]=1;//標記此點已經找到最短路徑
        for(auto e:g[u]){
            int v=e.v,u=e.u;
            if(dis[v]>e.w+dis[u]){//鬆弛操作
                dis[v]=e.w+dis[u];
                q.push((T){dis[v],v});//將得到路徑長度的點放入優先佇列
            }
        }
    }
}

相關文章