最短路算法

最短路算法


簡述

最短路是一種及常見的算法,在OI考試及平常生活中,都很常見,也是圖論學習的初步算法。ios

牢固掌握最短路算法,是極爲重要的。算法

常見的最短路算法有如下幾種:數組

Floyd算法網絡

  • 多源最短路,求出全部點對的最短路長度
  • 時間複雜度:\(O(n³)\)

Dijkstra算法閉包

  • 單源最短路,求出某個點s到全部點的最短路長度
  • 時間複雜度:\(O(n²)/O(m log n)\)
  • 沒法處理負權

SPFA算法,即隊列優化的Bellman-Ford算法學習

  • 單源最短路,求出某個點s到全部點的最短路長度
  • 時間複雜度:聲稱爲\(O(m)\),最壞\(O(nm)\),容易卡到最壞
  • 能夠處理負權邊,能夠判斷負權環

鬆弛操做

鬆弛操做:經過某條路徑更新dis[v]的值優化

  • $if (dis[v] > dis[u] + e.dist) dis[v] = dis[u] + e.dist $
  • 嘗試使用s到u的最短路加上邊(u,v)的長度來更新s到v的最短路

幾乎是全部最短路(單源)算法的核心。spa


Floyd算法

算法原理

Floyd算法是一個經典的動態規劃算法code

用通俗的語言來描述的話,首先咱們的目標是尋找從點i到點j的最短路徑。

從動態規劃的角度看問題,咱們須要爲這個目標從新作一個詮釋。(這個詮釋正是動態規劃最富創造力的精華所在)

實現方式

從任意節點i到任意節點j的最短路徑不外乎2種可能,1是直接從i到j,2是從i通過若干個節點k到j。

因此,咱們假設\(Dis(i,j)\)爲節點u到節點v的最短路徑的距離,

對於每個節點k,咱們檢查\(Dis(i,k) + Dis(k,j) < Dis(i,j)\)是否成立,

若是成立,證實從i到k再到j的路徑比i直接到j的路徑短,咱們便設置\(Dis(i,j) = Dis(i,k) + Dis(k,j)\)

這樣一來,當咱們遍歷完全部節點k,\(Dis(i,j)\)中記錄的即是i到j的最短路徑的距離。

代碼實現

只有5行,簡單易懂,但新手容易寫錯的地方是枚舉順序,必定是先中間節點\(k\),再枚舉\(i,j\)

1 for(k=1;k<=n;k++)
2     for(i=1;i<=n;i++)
3         for(j=1;j<=n;j++)
4             if(e[i][j]>e[i][k]+e[k][j])
5                  e[i][j]=e[i][k]+e[k][j];

傳遞閉包

在交際網絡中,給定若干個元素和若干個二元對關係,且關係具備傳遞性,

「經過傳遞性推導出更多的元素之間的關係」 被稱爲傳遞閉包。

創建鄰接矩陣,d,其中 \(d(i,j)=1\) 表示 \(i\)\(j\) 有關係,\(d(i,j)=0\) 表示 \(i\)\(j\) 沒有關係,特別的,\(d(i,i)=1\)

使用 Floyd 算法能夠解決傳遞閉包問題,代碼以下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#define maxn 330
using namespace std;

int n,m;
bool d[maxn][maxn];

int main(){
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++) d[i][i]=true;
    for(int i=1;i<=m;i++){
        int x,y;
        scanf("%d %d",&x,&y);
        d[x][y]=d[y][x]=true;
    }
    for(int k=1;k<=n;k++){
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
                d[i][j]|=d[i][k]&d[k][j];//核心代碼
            }
        }
    }
    for(int i=1;i<=n;i++){
        for(int j=1;j<=n;j++){
            if(d[i][j]) printf("%d %d\n",i,j);
        }
    }
    return 0;
}

這樣就完美解決了這個問題(沒學以前,有一次老師出了原題,我考試後自閉了)

實際應用

因爲Floyd的時間複雜度並不優秀,它在實際應用中每每只起到思想啓蒙的做用。

咱們能夠用Floyd的思想來計算一些題目。(一般只要發現是Floyd的思想,代碼實現十分簡單)

固然若是是須要模板題的話,能夠看這裏

想要加深理解的話,看這道題


Dijkstra算法

算法原理

本算法基於貪心思想,並不適用於有負權圖中

設G=(V,E)是一個帶權有向圖,把圖中頂點集合V分紅兩組,第一組爲已求出最短路徑的頂點集合。

(用S表示,初始時S中只有一個源點,之後每求得一條最短路徑 , 就將加入到集合S中,直到所有頂點都加入到S中)

第二組爲其他未肯定最短路徑的頂點集合(用U表示),按最短路徑長度的遞增次序依次把第二組的頂點加入S中。

在加入的過程當中,總保持從源點v到S中各頂點的最短路徑長度不大於從源點v到U中任何頂點的最短路徑長度。

此外,每一個頂點對應一個距離,S中的頂點的距離就是從v到此頂點的最短路徑長度。

U中的頂點的距離,是從v到此頂點只包括S中的頂點爲中間頂點的當前最短路徑長度。

以上原理沒看懂沒有關係,主要是如下的實現方式

實現方式

  1. 初始化\(dist[1]=0\),其他節點的 \(dist\) 的值爲正無窮大。
  2. 找出一個未被標記的點、\(dist[x]\) 最小的節點 \(x\) ,而後標記節點 \(x\)
  3. 掃描節點 \(x\) 的全部出邊 \((x,y,z)\) ,若 \(dist[y]>dist[x]+z\) ,則使用 \(dist[x]+z\) 更新 \(dist[y]\)
  4. 重複以上 2,3 兩個步驟,直到全部節點都被標記。

代碼實現

未加優化的算法以下,時間複雜度 \(O(n^2)\)

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#define N 10010
#define M 500010
#define maxd 2147483647
using namespace std;

int n,m,s,dis[N];
bool use[N];
int head[M],cnt=0;
struct node{
    int next,to,val;
}edge[M];

void addedge(int x,int y,int z){
    cnt++;
    edge[cnt].next=head[x];
    edge[cnt].to=y;
    edge[cnt].val=z;
    head[x]=cnt;
    return;
}

void dij(){
    for(int i=1;i<=n;i++) dis[i]=maxd;
    memset(use,false,sizeof(use));
    dis[s]=0;

    for(int i=2;i<=n;i++){
        int minn=maxd,k;
        for(int j=1;j<=n;j++){
            if(!use[j]&&minn>dis[j]){minn=dis[j];k=j;}
        }//尋找全局最小值
        use[k]=true;
        for(int j=head[k];j;j=edge[j].next){
            int go=edge[j].to;
            if(use[go]) continue;
            dis[go]=min(dis[go],dis[k]+edge[j].val);
        }
    }
    return;
}

int main(){
    scanf("%d %d %d",&n,&m,&s);
    int u,v,w;
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&u,&v,&w);
        addedge(u,v,w);
    }
    dij();
    for(int i=1;i<=n;i++) printf("%d ",dis[i]);
    //system("pause");
    return 0;
}

那麼咱們考慮怎麼優化呢?

咱們能夠發現,上面程序的主要瓶頸在於尋找全局最小值的過程(見註釋)

因此咱們能夠用一個小根堆進行維護,用 \(O(log n)\) 的時間獲取最小值,並用 \(O(log n)\) 的時間執行一條邊的擴展更新,

最終在 \(O(m log n)\) 的時間內完成算法,代碼以下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#include<queue>
#define N 100010
#define M 1000010
#define maxd 2147483647
using namespace std;

int n,m,s;
int head[N],dis[N],cnt=0;
bool use[N];
struct node{
    int next,to,val;
}edge[M];
priority_queue<pair<int,int> >q;
//爲了不重載小於號。
//pair的first用於存儲dis[]的相反數(變小根堆),second用存儲編號。

void addedge(int x,int y,int z){
    cnt++;
    edge[cnt].next=head[x];
    edge[cnt].to=y;
    edge[cnt].val=z;
    head[x]=cnt;
    return;
}

void dij(){
    for(int i=1;i<=n;i++) dis[i]=maxd;
    memset(use,false,sizeof(use));
    q.push(make_pair(0,s));
    dis[s]=0;

    while(!q.empty()){
        int now=q.top().second;
        q.pop();
        if(use[now]) continue;
        use[now]=true;
        for(int i=head[now];i;i=edge[i].next){
            int y=edge[i].to;
            int z=edge[i].val;
            if(dis[y]>dis[now]+z){
                dis[y]=dis[now]+z;
                q.push(make_pair(-dis[y],y));
            }
        }
    }
    return;
}

int main(){
    scanf("%d %d %d",&n,&m,&s);
    int u,v,w;
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&u,&v,&w);
        addedge(u,v,w);
    }
    dij();
    for(int i=1;i<=n;i++) printf("%d ",dis[i]);
    //system("pause");
    return 0;
}

實際應用

因爲時間複雜度出衆,Dijkstra算法是單源最短路的經常使用解法之一(前提是沒有負權)

舒適提示:若是你既能夠用Dijkstra,又能夠用\(SPFA\) ,請不要選擇\(SPFA\) ,否則你將承擔十年OI一場空的風險

應用範圍很廣,常常在題目中遇到,必定要緊緊掌握。


SPFA算法

算法原理

\(SPFA\) 特殊之處在於它是一個基於隊列的最短路算法。

它的原理是對圖進行V-1次鬆弛操做,獲得全部可能的最短路徑。

優勢是邊的權值能夠爲負數、實現簡單。缺點是容易被卡。

實現方式

因爲它是隊列優化的 Bellman-Ford 算法,因此咱們先介紹 Bellman-Ford 算法。

Bellman-Ford 算法基於迭代思想。它的流程以下:

  1. 掃描全部的邊 \((x,y,z)\) ,若 \(dist[y]>dist[x]+z\) 則用 \(dist[x]+z\) 更新 \(dist[y]\)
  2. 重複上述步驟,直到沒有更新操做產生。

時間複雜度爲 \(O(nm)\)

\(SPFA\) 算法流程以下:

  1. 創建一個隊列,最初隊列中只含有起點1。
  2. 取出隊頭節點 \(x\) ,掃描它的全部出邊\((x,y,z)\),若 \(dist[y]>dist[x]+z\) 則用 \(dist[x]+z\) 更新 \(dist[y]\)
  3. 同時,若是 \(y\) 不在隊列中,則把 \(y\) 入隊。
  4. 重複 2~3​ 步,直至隊列爲空。

是否是十分簡單易懂?

這個隊列避免了 Bellman-Ford 算法中對不須要擴展的節點的冗餘掃描,在稀疏圖中的效率極高。

時間複雜度很玄學,爲 \(O(km)\) 級別,其中能夠證實,通常 \(k\leq2\)

可是在稠密圖或特殊構造的網絡圖(良心出題人),該算法可能退化成 \(O(nm)\)

因此沒有負權時用Dijkstra算法,有負權時用 \(SPFA\) 算法(這個時候他還卡你 \(SPFA\) 就沒道理了)

代碼實現

普通的 \(SPFA\) 算法實現簡單,代碼以下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cmath>
#include<cstring>
#include<queue>
#define N 100010
#define M 1000010
#define maxd 2147483647 
using namespace std;

int n,m,s;
int head[N],cnt=0,dis[N];
bool use[N];
struct node{
    int next,to,val;
}edge[M];
queue<int>q;

void addedge(int x,int y,int z){
    cnt++;
    edge[cnt].next=head[x];
    edge[cnt].to=y;
    edge[cnt].val=z;
    head[x]=cnt;
    return;
}

void spfa(){
    for(int i=1;i<=n;i++) dis[i]=maxd;
    memset(use,false,sizeof(use));
    use[s]=true;
    dis[s]=0;
    q.push(s);

    while(!q.empty()){
        int now=q.front();q.pop();
        use[now]=false;
        for(int i=head[now];i;i=edge[i].next){
            int y=edge[i].to;
            int z=edge[i].val;
            if(dis[y]>dis[now]+z){
                dis[y]=dis[now]+z;
                if(!use[y]) q.push(y),use[y]=true;
            }
        }
    }
    return;
}

int main(){
    scanf("%d %d %d",&n,&m,&s);
    int u,v,w;
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&u,&v,&w);
        addedge(u,v,w);
    }
    spfa();
    for(int i=1;i<=n;i++) printf("%d ",dis[i]);
    //system("pause");
    return 0;
}

優化固然是有的,可是因爲優化價值不大,甚至更容易被卡掉,因此這裏就不介紹了。

若是要判斷負環,僅須要計算每個點的入隊狀況,若是某個點鬆弛了第n次,說明有負環。

實際應用

這個代碼主要用於負權圖中,稀疏圖表現也還行。

若是你擔憂有負權但出題人又卡 \(SPFA\) 的話,請自行尋找出路(其實應該不會出現這種狀況)


分層圖最短路

前置知識

分層圖最短路是指在能夠進行分層圖的圖上解決最短路問題。(分層圖:能夠理解爲有多個平行的圖)

通常模型是:在一個正常的圖上能夠進行 k 次決策,對於每次決策,不影響圖的結構,隻影響目前的狀態或代價。

通常將決策前的狀態和決策後的狀態之間鏈接一條權值爲決策代價的邊,表示付出該代價後就能夠轉換狀態了。

通常有兩種方法解決分層圖最短路問題:

  1. 建圖流:建圖時直接建成k+1層。
  2. 升維流:多開一維記錄機會信息。

固然具體選擇哪種方法,看數據範圍吧 。

方法一

咱們建k+1層圖。而後有邊的兩個點,多建一條到下一層邊權爲0的單向邊,若是走了這條邊就表示用了一次機會。

有N個點時,1~n表示第一層, (1+n)~(n+n)表明第二層, 以此類推。

由於要建K+1層圖,數組要開到n * ( k + 1),點的個數也爲n * ( k + 1 ) 。

請看代碼:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <queue>
#include <vector>
#define ll long long
#define inf 0x3f3f3f3f
#define pii pair<int, int>
const int mod = 1e9+7;
const int maxn = 5e4 * 42;
using namespace std;
struct node {int to,w,next;} edge[maxn];
int head[maxn], cnt;
int dis[maxn], vis[maxn];
int n, m, s, t, k;
struct Dijkstra
{
    void init()
    {
        memset(head,-1,sizeof(head));
        memset(dis,0x3f,sizeof(dis));
        memset(vis,0,sizeof(vis));
        cnt = 0;
    }
 
    void add(int u,int v,int w)
    {
        edge[cnt].to = v;
        edge[cnt].w = w;
        edge[cnt].next = head[u];
        head[u] = cnt ++;
    }
 
    void dijkstra()
    {
        priority_queue<pii,vector<pii>,greater<pii> > q;
        dis[s] = 0; q.push({dis[s],s});
        while(!q.empty())
        {
            int now = q.top().second;
            q.pop();
            if(vis[now]) continue;
            vis[now] = 1;
            for(int i = head[now]; i != -1; i = edge[i].next)
            {
                int v = edge[i].to;
                if(!vis[v] && dis[v] > dis[now] + edge[i].w)
                {
                    dis[v] = dis[now] + edge[i].w;
                    q.push({dis[v],v});
                }
            }
        }
    }
}dj;
 
int main()
{
    while(~scanf("%d%d%d", &n, &m, &k))
    {
        dj.init(); scanf("%d%d",&s,&t);
        while(m--)
        {
            int u, v, w;
            scanf("%d%d%d",&u, &v, &w);
            for(int i = 0; i <= k; i++)
            {
                dj.add(u + i * n, v + i * n, w);
                dj.add(v + i * n, u + i * n, w);
                if(i != k)
                {
                    dj.add(u + i * n, v + (i + 1) * n, 0);
                    dj.add(v + i * n, u + (i + 1) * n, 0);
                }
            }
        }
        dj.dijkstra(); int ans = inf;
        for(int i = 0; i <= k; i++)
            ans = min(ans, dis[t + i * n]);
 
        printf("%d\n",ans);
    }
}

方法二

咱們把dis數組和vis數組多開一維記錄k次機會的信息。

dis[ i ][ j ] 表明到達 i 用了 j 次免費機會的最小花費.
vis[ i ][ j ] 表明到達 i 用了 j 次免費機會的狀況是否出現過.

更新的時候先更新同層之間(即花費免費機會相同)的最短路,而後更新從該層到下一層(即再花費一次免費機會)的最短路。

不使用機會 dis[v][c] = min(min,dis[now][c] + edge[i].w);
使用機會 dis[v][c+1] = min(dis[v][c+1],dis[now][c]);

寫法相似於 \(DP\)

代碼見下:

#include <iostream>
#include <string.h>
#include <stdio.h>
#include <algorithm>
#include <queue>
#include <vector>
#define ll long long
#define inf 0x3f3f3f3f
#define pii pair<int, int>
const int mod = 1e9+7;
const int maxn = 1e5+7;
using namespace std;
struct node{int to, w, next, cost; } edge[maxn];
int head[maxn], cnt;
int dis[maxn][15], vis[maxn][15];
int n, m, s, t, k;
struct Dijkstra
{
    void init()
    {
        memset(head,-1,sizeof(head));
        memset(dis,127,sizeof(dis));
        memset(vis,0,sizeof(vis));
        cnt = 0;
    }
 
    void add(int u,int v,int w)
    {
        edge[cnt].to = v;
        edge[cnt].w = w;
        edge[cnt].next = head[u];
        head[u] = cnt ++;
    }
 
    void dijkstra()
    {
        priority_queue <pii, vector<pii>, greater<pii> > q;
        dis[s][0] = 0;
        q.push({0, s});
        while(!q.empty())
        {
            int now = q.top().second; q.pop();
            int c = now / n; now %= n;
            if(vis[now][c]) continue;
            vis[now][c] = 1;
            for(int i = head[now]; i != -1; i = edge[i].next)
            {
                int v = edge[i].to;
                if(!vis[v][c] && dis[v][c] > dis[now][c] + edge[i].w)
                {
                    dis[v][c] = dis[now][c] + edge[i].w;
                    q.push({dis[v][c], v + c * n});
                }
            }
            if(c < k)
            {
                for(int i = head[now]; i != -1; i = edge[i].next)
                {
                    int v = edge[i].to;
                    if(!vis[v][c+1] && dis[v][c+1] > dis[now][c])
                    {
                        dis[v][c+1] = dis[now][c];
                        q.push({dis[v][c+1], v + (c + 1) * n});
                    }
                }
            }
        }
    }
}dj;
 
int main()
{
    while(~scanf("%d%d%d", &n, &m, &k))
    {
        dj.init(); scanf("%d%d",&s,&t);
        while(m--)
        {
            int u, v, w;
            scanf("%d%d%d",&u, &v, &w);
            dj.add(u, v, w);
            dj.add(v, u, w);
        }
        dj.dijkstra();
        int ans = inf;
        for(int i = 0; i <= k; i++)
            ans = min(ans, dis[t][i]);
        printf("%d\n", ans);
    }
}

可見本寫法較爲複雜(由於我不會),因此推薦第一種作法。

你會發現講述時常常用到 「機會」這個詞,緣由是這道題

能夠當作模板題了。


有點權的最短路

通常的最短路並無點權,但若是遇到點權怎麼辦呢?

  1. 思路一:先無論點權,走到一個點以後再加,結果發現不可行。
  2. 思路二:上面提到的分層思想,將點權化爲邊權,發現可行。

具體流程以下:

  1. \(n\) 個點的圖分爲兩層,共計 \(2n\) 個點 。
  2. 輸入點權,將點 \(i\) 與點 \(n+i\) 相連,邊權爲點權。
  3. 輸入邊權,假設爲 \((u,v,w)\) ,那麼將點 \(u+n\) 與點 \(v\) 相連,邊權爲 \(w\)
  4. 跑一遍共 \(2n\) 個點的最短路。
  5. 輸出 \(dis(n+1...n+n)\) 即爲答案。

代碼以下:

#include<cstdio>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cmath>
#include<queue>
#include<vector>
#define N 100010
#define M 1000010
#define maxd 2147483647
using namespace std;

int n,m,s,dis[2*N];
bool use[2*N];
struct node{
    int to,val;
};
vector<node>edge[2*N];
priority_queue<pair<int,int> >q;

void dij(){
    for(int i=1;i<=2*n;i++) dis[i]=maxd;
    memset(use,false,sizeof(use));
    q.push(make_pair(0,s));
    dis[s]=0;

    while(!q.empty()){
        int now=q.top().second;
        q.pop();
        if(use[now]) continue;
        use[now]=true;
        for(int i=0;i<edge[now].size();i++){
            int y=edge[now][i].to;
            int z=edge[now][i].val;
            if(dis[y]>dis[now]+z){
                dis[y]=dis[now]+z;
                q.push(make_pair(-dis[y],y));
            }
        }
    }
    return;
}

int main(){
    scanf("%d %d %d",&n,&m,&s);
    int a,b,c;
    node p;
    for(int i=1;i<=n;i++){
        scanf("%d",&a);
        p.to=i+n;
        p.val=a;
        edge[i].push_back(p);
    }
    for(int i=1;i<=m;i++){
        scanf("%d %d %d",&a,&b,&c);
        p.to=b;
        p.val=c;
        edge[a+n].push_back(p);
    }
    dij();
    for(int i=n+1;i<=2*n;i++) printf("%d ",dis[i]);
    system("pause");
    return 0;
}

代碼使用 \(dij\) 實現,其餘實現方法大同小異。

雖然運用並非很廣,但這裏提到一下,之後可能會用的到。


結語

這麼重要的算法怎麼能不學呢?

全文資料:《算法競賽進階指南》以及某谷 \(dalao\) 的博客

熟練掌握三種基礎算法以後,必定要靈活應用。

咕咕咕

相關文章
相關標籤/搜索