最短路問題(詳解)


前言

最短路問題分為兩個模塊,一個是單源最短路,一個是多源匯最短路。而其中有4個算法。所以可以分別總結一下。

Dijkstra 算法

這里介紹 Dijkstra 算法,它是一個應用最為廣泛的、名氣也是最大的單源最短路徑算法Dijkstra 算法有一定的局限性:它所處理的圖中不能有負權邊

「前提:圖中不能有負權邊」

換句話說,如果一張圖中,但凡有一條邊的權值是負值,那么使用 Dijkstra 算法就可能得到錯誤的結果不過,在實際生活中所解決的問題,大部分的圖是不存在負權邊的

如:有一個路線圖,那么從一點到另外一點的距離肯定是一個正數,所以,雖然 Dijkstra 算法有局限性,但是並不影響在實際問題的解決中非常普遍的來使用它

看如下實例:

(1)初始

左邊是一張連通帶權有向圖,右邊是起始頂點 0 到各個頂點的當前最短距離的列表,起始頂點 0 到自身的距離是 0

(2)將頂點 0 進行標識,並作為當前頂點

對當前頂點 0 的所有相鄰頂點依次進行松弛操作,同時更新列表從列表的未標識頂點中找到當前最短距離最小的頂點,即 頂點 2,就可以說,起始頂點 0 到頂點 2 的最短路徑即 0 -> 2

因為:圖中沒有負權邊,即便存在從頂點 1 到頂點 2 的邊,也不可能通過松弛操作使得從起始頂點 0 到頂點 2 的距離更小

圖中沒有負權邊保證了:對當前頂點的所有相鄰頂點依次進行松弛操作后,只要能從列表的未標識頂點中找到當前最短距離最小的頂點,就能確定起始頂點到該頂點的最短路徑

(3)將頂點 2 進行標識,並作為當前頂點

(4)對當前頂點 2 的相鄰頂點 1 進行松弛操作,同時更新列表

(5)對當前頂點 2 的相鄰頂點 4 進行松弛操作,同時更新列表

(6)對當前頂點 2 的相鄰頂點 3 進行松弛操作,同時更新列表

從列表的未標識頂點中找到當前最短距離最小的頂點,即 頂點 1,

就可以說,起始頂點 0 到頂點 1 的最短路徑即 0 -> 2 -> 1

(7)將頂點 1 進行標識,並作為當前頂點

(8)對當前頂點 1 的相鄰頂點 4 進行松弛操作,同時更新列表

從列表的未標識頂點中找到當前最短距離最小的頂點,即 頂點 4,就可以說,起始頂點 0 到頂點 4 的最短路徑即 0 -> 2 -> 1 -> 4

(9)將頂點 4 進行標識,並作為當前頂點

當前頂點 4 沒有相鄰頂點,不必進行松弛操作

從列表的未標識頂點中找到當前最短距離最小的頂點,即 頂點 3,就可以說,起始頂點 0 到頂點 3 的最短路徑即 0 -> 2 -> 3

(10)將頂點 3 進行標識,並作為當前頂點

對當前頂點 3 的相鄰頂點 4 進行松弛操作,發現不能通過松弛操作使得從起始頂點 0 到頂點 4 的路徑更短,所以保持原有最短路徑不變至此,列表中不存在未標識頂點,Dijkstra 算法結束,找到了一棵以頂點 0 為根的最短路徑樹

Dijkstra 算法的過程總結:

第一步:從起始頂點開始

第二步:對當前頂點進行標識

第三步:對當前頂點的所有相鄰頂點依次進行松弛操作

第四步:更新列表

第五步:從列表的未標識頂點中找到當前最短距離最小

   的頂點,作為新的當前頂點

第六步:重復第二步至第五步,直到列表中不存在未標識頂點

Dijkstra 算法主要做兩件事情:

(1)從列表中找最值

(2)更新列表

顯然,借助最小索引堆作為輔助數據結構,就可以非常容易地實現這兩件事情

最后,Dijkstra 算法的時間復雜度:O(E*logV)

來2代碼:(最短路問題背代碼基本就行了)

給定一個 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
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n, m;
int g[N][N];
int dist[N];
bool st[N];
int dijkstra(){
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    for (int i = 0; i < n - 1; i ++ ){
        int t = -1;
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);
        st[t] = true;
    }
    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}
int main(){
    scanf("%d%d", &n, &m);
    memset(g, 0x3f, sizeof g);
    while (m -- ){
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        g[a][b] = min(g[a][b], c);
    }
    printf("%d\n", dijkstra());
    return 0;
}

給定一個 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。
數據保證:如果最短路存在,則最短路的長度不超過 109。

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


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

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[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 ++ ;
}

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i])
            {
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}

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);
    }

    cout << dijkstra() << endl;

    return 0;
}


Bellman-Ford算法

貝爾曼-福特算法(Bellman-Ford)是由理查德·貝爾曼和萊斯特·福特創立的,求解單源最短路徑問題的一種算法。它的原理是對圖進行V-1次松弛操作,得到所有可能的最短路徑。其優於Dijkstra算法的方面是邊的權值可以為負數、實現簡單,缺點是時間復雜度過高。

Bellman-Ford算法是一種處理存在負權邊的單元最短路問題的算法。解決了Dijkstra無法求的存在負權邊的問題。 雖然其算法效率不高,但是也有其特別的用處。其實現方式是通過m次迭代求出從源點到終點不超過m條邊構成的最短路的路徑。一般情況下要求途中不存在負環。但是在邊數有限制的情況下允許存在負環。因此Bellman-Ford算法是可以用來判斷負環的。

給定一個 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
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, M = 10010;

struct Edge
{
    int a, b, c;
}edges[M];

int n, m, k;
int dist[N];
int last[N];

void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);

    dist[1] = 0;
    for (int i = 0; i < k; i ++ )
    {
        memcpy(last, dist, sizeof dist);
        for (int j = 0; j < m; j ++ )
        {
            auto e = edges[j];
            dist[e.b] = min(dist[e.b], last[e.a] + e.c);
        }
    }
}

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

    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edges[i] = {a, b, c};
    }

    bellman_ford();

    if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d\n", dist[n]);

    return 0;
}

SPFA算法

解決存在負環的圖的單源最短路徑,bellman-ford算法是比較經典的一個,但是大家都知道,這個算法的效率並不咋的,因為它只知道要求單源最短路,至多做|v|(j圖的結點數)次松弛操作,感覺有點盲目吧,這里介紹一個有西南交通大學段凡丁1994年發明的一個算法即SPFA,很大程度上優化了bellman-ford算法(建議沒有學過的,先去了解一下這個算法),算法的時間效率我就不說了,因為我覺得當我們熟悉某個算法之后,分析時間復雜度就沒什么問題了,如果盲目的記憶,意義不大。

SPFA算法的精妙之處在於不是盲目的做松弛操作,而是用一個隊列保存當前做了松弛操作的結點。只要隊列不空,就可以繼續從隊列里面取點,做松弛操作,想想bellman-ford算法吧,它只知道做|v|次循環就對了。下面講講SPFA為什么這樣做呢?還是舉個例子:

當前源點1在隊列里面,於是我們取了1結點來做對圖進行松弛操作,顯然這個時候2,3結點的距離更新了,入了隊列,我們假設他們沒入隊列,即現在隊列已經空了,那么還有沒有必要繼續做松弛操作呢?顯然沒必要了啊,因為源點1要到其他結點必須經過2或3結點啊。現在懂了吧。

先講一下SPFA的大致思想

算法大致流程是用一個隊列來進行維護。 初始時將源加入隊列。 每次從隊列中取出一個元素,並對所有與他相鄰的點進行松弛,若某個相鄰的點松弛成功,如果該點沒有在隊列中,則將其入隊。 直到隊列為空時算法結束。

判斷有無負環:如果某個點進入隊列的次數超過V次則存在負環(SPFA無法處理帶負環的圖)

SPFA算法有兩個優化算法 SLF 和 LLL: SLF:Small Label First 策略,設要加入的節點是j,隊首元素為i,若dist(j)<dist(i),則將j插入隊首,否則插入隊尾。 LLL:Large Label Last 策略,設隊首元素為i,隊列中所有dist值的平均值為x,若dist(i)>x則將i插入到隊尾,查找下一元素,直到找到某一i使得dist(i)<=x,則將i出對進行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高約 50%。 在實際的應用中SPFA的算法時間效率不是很穩定,為了避免最壞情況的出現,通常使用效率更加穩定的Dijkstra算法。

給定一個 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
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010;

int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[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 ++ ;
}

int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    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];
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return dist[n];
}

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);
    }

    int t = spfa();

    if (t == 0x3f3f3f3f) puts("impossible");
    else printf("%d\n", t);

    return 0;
}


SPFA判負環

bool spfa()
{
    memset(dist, 0, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(st, 0, sizeof st);

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

    while(q.size())
    {
        int t = q.front();
        q.pop();
        st[t] = false;

        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j]  ____   dist[t] ......)
            {
                dist[j] = dist[t] ......;
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

Floyd算法

1.算法原理

Floyd算法是一個經典的動態規划算法,它又被稱為插點法。該算法名稱以創始人之一、1978年圖靈獎獲得者、斯坦福大學計算機科學系教授羅伯特·弗洛伊德命名。Floyd算法是一種利用動態規划的思想尋找給定的加權圖中多源點之間最短路徑的算法,算法目標是尋找從點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的最短路徑的距離。

2.算法內容

3.算法步驟

4.代碼

給定一個 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
#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 210, INF = 1e9;

int n, m, Q;
int 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()
{
    scanf("%d%d%d", &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;

    while (m -- )
    {
        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);

        int t = d[a][b];
        if (t > INF / 2) puts("impossible");
        else printf("%d\n", t);
    }

    return 0;
}



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM