最短路問題 - 模板總結(Dijkstra + Bellman-Ford + SPFA + Floyd)

最短路問題 - 模板總結(Dijkstra + Bellman-ford + SPFA + Floyd)

最短路問題分類圖:
在這裏插入圖片描述

1、Dijkstra算法(正邊權單源最短路問題)

1-1、樸素Dijkstra算法-O(n2)

給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環,所有邊權均爲正值。

請你求出1號點到n號點的最短距離,如果無法從1號點走到n號點,則輸出-1。

輸入格式
第一行包含整數n和m。

接下來m行每行包含三個整數x,y,z,表示存在一條從點x到點y的有向邊,邊長爲z。

輸出格式
輸出一個整數,表示1號點到n號點的最短距離。

如果路徑不存在,則輸出-1。

數據範圍
1≤n≤500,
1≤m≤105,
圖中涉及邊長均不超過10000。

輸入樣例:
3 3
1 2 2
2 3 1
1 3 4
輸出樣例:
3


分析:

m>n2O(n2)本題m>n^2,用O(n^2)算法更優。

稠密圖用鄰接矩陣存儲。注意到有重邊,在讀入的時候取重邊中最小的權重即可。

具體落實:

S1S2dis+dis[S]=0S1S2jdis[j]S2S1ttS2①、整個點集分爲兩部分,一部分是到起點的最短距離已經確定,記爲S_1。另一部分未確定,記爲S_2。\\\qquad首先初始化距離dis數組爲+∞,起點所在位置dis[S]=0,就說將起點加入S_1。\\\qquad再用起點去更新S_2中的點j到起點的最短距離dis[j]。\\\qquad接下來的每次操作,都從S_2中選擇一個到S_1距離最近的點t,再用點t去更新S_2中的點到起點的最短距離。

nnS1dis②、以上操作重複n次,將n個點都加入到S_1中,此時的dis數組存儲的就是到任意點到起點的最短距離。
模板:

#include<cstring>
#include<algorithm>
#include<iostream>
#include<cstdio>

using namespace std;

const int N=510;

int n,m,g[N][N],dis[N];
bool st[N];

int dijkstra(int S)
{
    memset(dis,0x3f,sizeof dis);
    dis[S]=0;
    
    for(int i=0;i<n;i++)
    {
        int t=-1;
        for(int j=1;j<=n;j++)
            if(!st[j] && (t==-1 || dis[t]>dis[j]))
                t=j;
                
        st[t]=true;
        
        for(int j=1;j<=n;j++)
            dis[j]=min(dis[j],dis[t]+g[t][j]);
    }
    
    return dis[n]==0x3f3f3f3f ? -1 : dis[n];
    
}

int main()
{
    memset(g,0x3f,sizeof g);
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        g[a][b]=min(g[a][b],c);
    }
    
    cout<<dijkstra(1)<<endl;
    
    return 0;
}

1-2、堆優化的Dijkstra算法-O(mlogn)

給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環,所有邊權均爲非負值。

請你求出1號點到n號點的最短距離,如果無法從1號點走到n號點,則輸出-1。

輸入格式
第一行包含整數n和m。

接下來m行每行包含三個整數x,y,z,表示存在一條從點x到點y的有向邊,邊長爲z。

輸出格式
輸出一個整數,表示1號點到n號點的最短距離。

如果路徑不存在,則輸出-1。

數據範圍
1≤n,m≤1.5×105,
圖中涉及邊長均不小於0,且不超過10000。

輸入樣例:
3 3
1 2 2
2 3 1
1 3 4
輸出樣例:
3

分析:

n<=105O(n2)O(mlog2n)Dijkstran<=10^5,此時O(n^2)效率明顯低於O(mlog_2n),採用堆優化版的Dijkstra算法。

稀疏圖,採用鄰接表存儲圖。

Dijkstra通過分析樸素的Dijkstra算法,容易發現,\\我們每一趟都需要遍歷點集中的所有點來尋找到當前到原點距離最近的點。\\事實上,這個過程可以通過小根堆來維護。

S2S2這樣我們每次從小根堆中取出的就是S_2中到起點距離最近的點,再用該點更新S_2中與其相鄰的點到起點的距離,\\並加入堆中。再重複操作即可。

代碼:

#include<iostream>
#include<cstring>
#include<algorithm>
#include<queue>
#include<cstdio>

#define P pair<int,int>

using namespace std;

const int N=1e6+10;

int n,m,e[N],w[N],ne[N],h[N],idx;
bool st[N];

void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

int dijkstra()
{
    int dis[N];
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    
    priority_queue<P,vector<P>,greater<P>> heap;
    heap.push({0,1});
    
    while(heap.size())
    {
        P t=heap.top();
        heap.pop();
        
        int id=t.second;
        if(st[id]) continue;
        st[id]=true;
        
        for(int i=h[id];~i;i=ne[i])
        {
            int j=e[i];
            if(dis[j]>dis[id]+w[i])
            {
                dis[j]=dis[id]+w[i];
                heap.push({dis[j],j});
            }
        }
    }
    
    return dis[n]==0x3f3f3f3f ? -1 : dis[n];
    
}


int main()
{
    memset(h,-1,sizeof h);
    scanf("%d%d",&n,&m);
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    
    cout<<dijkstra()<<endl;
    
    return 0;
}

2、Bellman-Ford算法(帶負權且經過的邊的數量有限制)-O(nm)

給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環, 邊權可能爲負數。

請你求出從1號點到n號點的最多經過k條邊的最短距離,如果無法從1號點走到n號點,輸出impossible。

注意:圖中可能 存在負權迴路 。

輸入格式
第一行包含三個整數n,m,k。

接下來m行,每行包含三個整數x,y,z,表示存在一條從點x到點y的有向邊,邊長爲z。

輸出格式
輸出一個整數,表示從1號點到n號點的最多經過k條邊的最短距離。

如果不存在滿足條件的路徑,則輸出“impossible”。

數據範圍
1≤n,k≤500,
1≤m≤10000,
任意邊長的絕對值不超過10000。

輸入樣例:
3 3 1
1 2 1
2 3 1
1 3 3
輸出樣例:
3

分析:

()有負權迴路,最短路未必存在(起點到終點的路徑上未經過負權環,此時仍然是有最短路的)。

k共迭代k次,每次遍歷所有的邊,更新所有點到起點的最短距離。

代碼:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>

using namespace std;

const int N=10010;

int n,m,k,backup[N];
struct Edge
{
    int u,v,w;  
}e[N];

int bellman_ford()
{
    int dis[N];
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    
    for(int i=0;i<k;i++)
    {
        memcpy(backup,dis,sizeof dis);
        for(int j=1;j<=m;j++)
        {
            int a=e[j].u,b=e[j].v,w=e[j].w;
            dis[b]=min(dis[b],backup[a]+w);
        }
    }
    
    return dis[n]>0x3f3f3f3f/2 ? -1 : dis[n];
}

int main()
{
    cin>>n>>m>>k;
    for(int i=1;i<=m;i++) scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
    
    int t=bellman_ford();
    if(t==-1) puts("impossible");
    else cout<<t<<endl;
    
    return 0;
}

3、SPFA算法(帶負權的最短路)-O(m)~O(nm)

3-1、SPFA求最短路

給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環, 邊權可能爲負數。

請你求出1號點到n號點的最短距離,如果無法從1號點走到n號點,則輸出impossible。

數據保證不存在負權迴路。

輸入格式
第一行包含整數n和m。

接下來m行每行包含三個整數x,y,z,表示存在一條從點x到點y的有向邊,邊長爲z。

輸出格式
輸出一個整數,表示1號點到n號點的最短距離。

如果路徑不存在,則輸出”impossible”。

數據範圍
1≤n,m≤105
圖中涉及邊長絕對值均不超過10000。

輸入樣例:
3 3
1 2 5
2 3 -3
1 3 4
輸出樣例:
2

分析:

SPFABellmanFordBFdis[b]=min(dis[b],dis[a]+w)dis[a]dis[w]SPFA算法是Bellman-Ford算法的優化,\\我們發現BF算法每次迭代均要遍歷所有的邊,但事實上,並不是所有邊均會被更新得更小。\\轉移方程:dis[b]=min(dis[b],dis[a]+w),可見,只有當dis[a]減小,dis[w]纔會變小。\\因此,只有當某個點到起點的距離變小,經過該節點的其他點到起點的距離纔可能變小。

BFSBF我們採用BFS的思想來對BF算法進行優化,\\用一個隊列來存儲到起點距離變短的點,每次出隊後用該點更新與其相鄰節點到起點的距離,\\再將更新過的到起點距離變短的點繼續入隊。

++2需要注意,由於存在負權邊,因此我們初始化的+∞可能會略微的減小。\\因此我們認爲若某點到起點的距離大於\frac{+∞}{2},則說明從起點無法到達該點。

代碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>

using namespace std;

const int N=100010;

int n,m,e[N],h[N],w[N],ne[N],idx;
bool st[N];

void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}

int spfa()
{
    int dis[N];
    memset(dis,0x3f,sizeof dis);
    dis[1]=0;
    
    queue<int> Q;
    Q.push(1);
    st[1]=true;
    
    while(Q.size())
    {
        int u=Q.front();
        Q.pop();
        st[u]=false;
        
        for(int i=h[u];~i;i=ne[i])
        {
            int j=e[i];
            if(dis[j]>dis[u]+w[i])
            {
                dis[j]=dis[u]+w[i];
                if(!st[j])
                {
                    st[j]=true;
                    Q.push(j);
                }
            }
        }
    }
    
    return dis[n];
}

int main()
{
    memset(h,-1,sizeof h);
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        add(a,b,c);
    }
    
    int t=spfa();
    if(t==0x3f3f3f3f) puts("impossible");
    else cout<<t<<endl;
    
    return 0;
}

3-2、SPFA判斷負環

給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環, 邊權可能爲負數。

請你判斷圖中是否存在負權迴路。

輸入格式
第一行包含整數n和m。

接下來m行每行包含三個整數x,y,z,表示存在一條從點x到點y的有向邊,邊長爲z。

輸出格式
如果圖中存在負權迴路,則輸出“Yes”,否則輸出“No”。

數據範圍
1≤n≤2000,
1≤m≤10000,
圖中涉及邊長絕對值均不超過10000。

輸入樣例:
3 3
1 2 -1
2 3 4
3 1 -4
輸出樣例:
Yes


分析:

cnt[x]x與求最短路的思路類似,需額外增加數組cnt[x],記錄起點到x點經過的邊數。

cnt[x]>=nxnn+12若cnt[x]>=n,表示從起點到x點經過了n條邊,意味着經過了n+1個點,由抽屜原理,必然有某個點經過了2次。\\這就說明了圖中存在一個負環。

SPFA與最短路不同的是,這次我們要先將所有點入隊。\\因爲負環可能從某個起點出發未必能經過,需要將所有點都當作起點跑SPFA。

代碼:

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

bool spfa()
{
    queue<int> q;

    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }

    while (q.size())
    {
        int t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;

                if (cnt[j] >= n) return true;
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    if (spfa()) puts("Yes");
    else puts("No");

    return 0;
}



4、Floyd(多源最短路)-O(n3)

給定一個n個點m條邊的有向圖,圖中可能存在重邊和自環,邊權可能爲負數。

再給定k個詢問,每個詢問包含兩個整數x和y,表示查詢從點x到點y的最短距離,如果路徑不存在,則輸出“impossible”。

數據保證圖中不存在負權迴路。

輸入格式
第一行包含三個整數n,m,k

接下來m行,每行包含三個整數x,y,z,表示存在一條從點x到點y的有向邊,邊長爲z。

接下來k行,每行包含兩個整數x,y,表示詢問點x到點y的最短距離。

輸出格式
共k行,每行輸出一個整數,表示詢問的結果,若詢問兩點間不存在路徑,則輸出“impossible”。

數據範圍
1≤n≤200,
1≤k≤n2
1≤m≤20000,
圖中涉及邊長絕對值均不超過10000。

輸入樣例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
輸出樣例:
impossible
1

kijk思路十分簡單,枚舉中間點k,更新所有從i到j且經過k的路徑的最短距離。

+2同樣的,由於存在負權邊,若距離仍大於\frac{+∞}{2},則認爲兩點之間不連通。

代碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

#define inf 0x3f3f3f3f

using namespace std;

const int N=210;

int n,m,q,d[N][N];

void floyd()
{
    for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++)
                d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
}

int main()
{
    cin>>n>>m>>q;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            if(i==j) d[i][j]=0;
            else d[i][j]=inf;
            
    for(int i=0;i<m;i++)
    {
        int a,b,c;
        scanf("%d%d%d",&a,&b,&c);
        d[a][b]=min(d[a][b],c);
    }
    
    floyd();
    
    while(q--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        if(d[a][b]>inf/2) puts("impossible");
        else printf("%d\n",d[a][b]);
    }
    
    return 0;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章