1. 程式人生 > 其它 >最短路徑演算法

最短路徑演算法

Floyd、樸素 dij、堆優化 dij、SPFA、SPFA判負環。

最短路問題

最短路問題

在帶權圖中,每條邊都有一個權值,就是邊的長度。路徑的長度等於經過所有邊權之和,求最小值。

如上圖,從 \(1\)\(4\) 的最短路徑為 1->2->3->4,長度為 5。

對於無權圖或者邊權相同的圖,我們顯然可以使用 bfs 求解。

但是對於帶權圖,就不能通過 bfs 求得了。

Floyd 多源最短路演算法

概述

所謂多源則是它可以求出以每個點為起點到其它每個點的最短路。

有一種特殊情況是求不出最短路的,就是存在負環。每次經過這段路之後最短路長度就會減少,演算法便會得到錯誤的答案,一些演算法甚至會有死迴圈。但是 Floyd 無法判斷是否出現這種情況,所以就只能在沒有負環的情況下使用。

Floyd 演算法是一種利用動態規劃的思想、計算給定的帶權圖中任意兩個頂點之間最短路徑的演算法。無權圖可以直接把邊權看作 \(1\)

Floyd 寫法最為簡單。但是一定要切記中間點 \(k\) 的列舉一定要放在最外層。

int g[maxn][maxn];
for (int k = 1;k <= n;k++) {
    for (int i = 1;i <= n;i++) {
        for (int j = 1;j <= n;j++) {
            g[i][j] = min(g[i][k] + g[k][j], g[i][j]);
        }
    }
}

時間複雜度為 \(\mathcal{O}(n^3)\)

程式碼實現

#include <iostream>
#include <cmath>
#include <cstring>
using namespace std;

const int N = 101;

int g[N][N];

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

int main() {
    memset(g, 0x3f, sizeof(g));
    for (int i = 0; i < N; i++) {
        g[i][i] = 0;
    }
    int n, m;
    int u, v, w;
    cin >> n >> m;
    for (int i = 0; i < m; i++) {
        cin >> u >> v >> w;
        g[u][v] = g[v][u] = w;
    }
    Floyd(n);
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cout << g[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

例題:城市

題目描述

一個國家中有 \(n\) 個城市,\(m\) 條道路,每條道路都是單向的。國王想知道以每座城市為起點可以到達那些城市?

輸入格式

第一行輸入 \(n(1\le n\le 100),m(1\le m\le 1000)\),表示城市和道路數量。

接下來 \(m\) 行,每行兩個整數 \(u,v(1\le u,v\le n)\) 表示有一條從 \(u\) 城市到 \(v\) 城市的一條道路。

輸出格式

輸出 \(n\) 行,每行 \(n\) 個數,用空格隔開。

\(i\) 行第 \(j\) 個數表示城市 \(i\) 是否可以到達城市 \(j\)。如果可以輸出 1,否則輸出 0。

樣例輸入

3 2
1 2
1 3

樣例輸出

1 1 1
0 1 0
0 0 1

解析

可以直接套用 floyd 求解。

程式碼

#include <iostream>
using namespace std;
int g[105][105];
int main() {
    int n, m, u, v;
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        g[i][i] = 1;
    }
    for (int i = 0; i < m; i++) {
        cin >> u >> v;
        g[u][v] = 1;
    }
    
    for (int k = 1;k <= n;k++) {
        for (int i = 1;i <= n;i++) {
            for (int j = 1;j <= n;j++) {
                if (!g[i][j]) {
                    if (g[i][k] && g[k][j]) {
                        g[i][j] = true;
                    }
                }
            }
        }
    }
    
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            cout << g[i][j] << " ";
        }
        cout << endl;
    }
    return 0;
}

樸素迪傑斯特拉最短路演算法

單源最短路問題是指:從源點 \(s\) 到圖中其餘各頂點的最短路徑。

演算法流程

我們定義帶權圖 \(G\) 所有頂點集合為 \(V\),接著我們再定義已確定從源點出發的最短路徑的頂點集合為 \(U\)。初始集合 \(U\) 為空,記從源點 \(s\) 出發到每個頂點 \(v\) 的距離為 \(d_v\),初始 \(d_s=0\),接著執行如下操作:

  1. \(V-U\) 中找出一個距離源點最近的頂點 \(v\),將 \(v\) 加入集合 \(U\)
  2. 並用 \(d_v\) 和頂點 \(v\) 連出來的邊來更新和 \(v\) 相鄰的、不在集合 \(U\) 中的頂點 \(d\),這一步成為鬆弛操作。
  3. 重複 1 和 2,直到 \(V=U\) 或找不到一個從 \(s\) 出發有路徑到達的頂點,演算法結束。

迪傑斯特拉演算法的時間複雜度為 \(\mathcal{O}(V^2)\),其中 \(V\) 表示頂點的個數。

圖片演示

我們用一個例子來說明這個演算法。

初始每個頂點的 \(d\) 設定為無窮大 \(\inf\),源點 \(M\)\(d_M\) 設定為 \(0\)。當前 \(U=\emptyset\)\(V-U\) 中的 \(d\) 最小的頂點為 \(M\)。從頂點 \(M\) 出發,更新相鄰點的 \(d\)

更新完畢,此時 \(U=\{M\}\)\(V-U\)\(d\) 最小的頂點是 \(W\)。從 \(W\) 出發,更新相鄰點的 \(d\)

更新完畢,此時 \(U=\{M,W\}\)\(V-U\)\(d\) 最小的頂點是 \(E\)。從 \(E\) 出發,更新相鄰頂點的 \(d\)

更新完畢,此時 \(U=\{M,W,E\}\)\(V-U\)\(d\) 最小的頂點是 \(X\)。從 \(X\) 出發,更新相鄰頂點的 \(d\)

更新完畢,此時 \(U=\{M,W,E,X\}\)\(V-U\)\(d\) 最小的頂點是 \(D\)。從 \(D\) 出發,沒有其他不在集合 \(U\) 中的頂點。

此時 \(U=V\),演算法結束。

示例程式碼

for (int i = 1;i <= n;i++) {
    int mind = inf;
    int v = 0;
    for (int j = 1;j <= n;j++) {
        if (d[j] < mind && !vis[j]) {
            mind = d[j];
            v = j;
        }
    }
    if (mind == inf) {
        break;
    }
    vis[v] = true;
    for (int j = 0;j < g[v].size();j++) {
       	d[g[v][j].v] = min(d[g[v][j].v], d[v] + g[v][j].w);
    }
}

程式碼實現

#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 1001;
const int inf = 0x3f3f3f3f;
struct node {
    int v, w;
    node() {
      
    }
    node(int vv, int ww) {
        v = vv;
        w = ww;
    }
};
vector<node> g[N];
int n, m, s;
int d[N];
bool vis[N];
int main() {
    cin >> n >> m >> s;
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back(node(v, w));
        g[v].push_back(node(u, w));
    }
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    
    for (int i = 1;i <= n;i++) {
        int mind = inf;
        int v = 0;
        for (int j = 1;j <= n;j++) {
            if (!vis[j] && d[j] < mind) {
                mind = d[j];
                v = j;
            }
        }
        if (mind == inf) {
            break;
        }
        vis[v] = true;
        for (int j = 0;j < g[v].size();j++) {
            d[g[v][j].v] = min(d[g[v][j].v], d[v] + g[v][j].w);
        }
    }
    
    for (int i = 1; i <= n; i++) {
        cout << d[i] << " ";
    }
    return 0;
}

堆優化迪傑斯特拉演算法

在樸素演算法中,每次使用 \(\mathcal{O}(n)\) 查詢距離當前點最近的點過於消耗時間,我們可以使用堆優化來直接獲得最近的點。

堆優化

如果暴力列舉的話,時間複雜度為 \(\mathcal{O}(V^2)\)

如果考慮使用一個 set 來維護點的集合,這樣時間複雜度就優化到了\(\mathcal{O}((V+E)\log V)\)

示例程式碼

set<pair<int, int> > min_heap;
min_heap.insert(make_pair(0, s));
while (min_heap.size()) {
    int mind = min_heap.begin()->first;
    int v = min_heap.begin()->second;
    min_heap.erase(min_heap.begin());
    for (int i = 0;i < g[v].size();i++) {
        if (d[g[v][i].v] > d[v] + g[v][i].w) {
            min_heap.erase(make_pair(d[g[v][i].v], g[v][i].v));
            d[g[v][i].v] = d[v] + g[v][i].w;
            min_heap.insert(make_pair(d[g[v][i].v], g[v][i].v));
        }
    }
}

程式碼實現

#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
#include <set>
using namespace std;
const int N = 100001;
const int inf = 0x3f3f3f3f;
struct node {
    int v, w;
    node() {
      
    }
    node(int vv, int ww) {
        v = vv;
        w = ww;
    }
};
vector<node> g[N];
int n, m, s;
int d[N];
set<pair<int, int> > min_heap;
int main() {
    cin >> n >> m >> s;
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back(node(v, w));
        g[v].push_back(node(u, w));
    }
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    
    min_heap.insert(make_pair(0, s));
    while (min_heap.size()) {
        int mind = min_heap.begin() -> first;
        int v = min_heap.begin() -> second;
        min_heap.erase(min_heap.begin());
        for (int i = 0;i < g[v].size();i++) {
            if (d[g[v][i].v] > d[v] + g[v][i].w) {
                min_heap.erase(make_pair(d[g[v][i].v], g[v][i].v));
                d[g[v][i].v] = d[v] + g[v][i].w;
                min_heap.insert(make_pair(d[g[v][i].v], g[v][i].v));
            }
        }
    }
    
    for (int i = 1; i <= n; i++) {
        cout << d[i] << " ";
    }
    return 0;
}

例題:旅行

題目描述

一個國家中有 \(n\) 個城市,\(m\) 個道路。小明位於第 \(s\) 個城市,他想知道其他城市距離自己所在城市的最短距離是多少。

輸入格式

第一行輸入一個整數 \(n(1\le n\le 1\times 10^5),m(0\le m\le 1\times 10^6),s(1\le s\le n)\),分別表示城市個數、道路數量以及起點城市。

接下來 \(m\) 行,每行三個不同的正整數 \(u,v,w(1\le u,v\le n,1\le w\le 1000)\),表示 \(u\) 城市到 \(v\) 城市之間有一條長度為 \(w\) 的道路。

輸出格式

輸出一行,包含 \(n\) 個數字,表示以 \(s\) 為起點到第 \(i\) 個城市的最短距離。如果不可到達輸出 \(-1\)

解析

可以直接使用堆優化迪傑斯特拉來解決。

參考程式碼

#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
#include <set>
using namespace std;
const int N = 100001;
const int inf = 0x3f3f3f3f;
struct node {
    int v, w;
    node() {
    }
    node(int vv, int ww) {
        v = vv;
        w = ww;
    }
};
vector<node> g[N];
int n, m, s;
int d[N];
set<pair<int, int> > min_heap;
bool in_queue[N];
int main() {
    cin >> n >> m >> s;
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back(node(v, w));
    }
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    min_heap.insert(make_pair(0, s));
    while (!min_heap.empty()) {
        int mind = min_heap.begin() -> first;
        int v = min_heap.begin() -> second;
        min_heap.erase(min_heap.begin());
        for (int i = 0;i < g[v].size();i++) {
            if (d[g[v][i].v] > d[v] + g[v][i].w) {
                min_heap.erase(make_pair(d[g[v][i].v], g[v][i].v));
                d[g[v][i].v] = d[v] + g[v][i].w;
                min_heap.insert(make_pair(d[g[v][i].v], g[v][i].v));
            }
        }
    }
    for (int i = 1;i <= n;i++) {
        if (d[i] < inf) cout << d[i] << ' ';
        else cout << -1 << ' ';
    }
    cout << endl;
    return 0;
}

SPFA 演算法

演算法內容

在該演算法中,需要使用一個佇列來儲存即將拓展的頂點列表,並使用 \(\text{in\_queue}_i\) 來表示頂點 \(i\) 是否在佇列中。

  1. 初始佇列僅有源點,且 \(d_s=0\)
  2. 取出隊頂元素 \(u\),掃描從 \(u\) 出發的每一條邊,設每條邊的另一端為 \(v\),邊 \(u,v\) 的權值為 \(w\),若 \(d_u+w<d_v\),則
    • \(d_v\) 修改為 \(d_u+w\)
    • \(v\) 不在佇列中,則將 \(v\) 入隊。
  3. 重複 2 直到佇列為空。

演算法效率

空間複雜度很顯然為 \(\mathcal{O}(V)\)。如果平均入隊次數為 \(k\),則 SPFA 的時間複雜度為 \(\mathcal{O}(kE)\)

對於一般隨機稀疏圖,\(k\) 不超過 \(4\)

示例程式碼

bool in_queue[maxn];
int d[maxn];
queue<int> q;
void spfa(int s) {
    memset(in_queue, 0, sizeof(in_queue));
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    in_queue[s] = true;
    q.push(s);
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        in_queue[v] = false;
        for (int i = 0;i < g[v].size();i++) {
            if (d[g[v][i].v] > d[v] + g[v][i].w) {
                d[g[v][i].v] = d[v] + g[v][i].w;
                if (!in_queue[g[v][i].v]) {
                    q.push(g[v][i].v);
                    in_queue[g[v][i].v] = true;
                }
            }
        }
    }
}

程式碼實現

#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100001;
const int inf = 0x3f3f3f3f;
struct node {
    int v, w;
    node() {
      
    }
    node(int vv, int ww) {
        v = vv;
        w = ww;
    }
};
vector<node> g[N];
int n, m, s;
int d[N];
bool in_queue[N];
queue<int> q;
int main() {
    cin >> n >> m >> s;
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back(node(v, w));
        g[v].push_back(node(u, w));
    }
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    
    in_queue[s] = true;
    q.push(s);
    
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        in_queue[v] = false;
        for (int i = 0;i < g[v].size();i++) {
            int x = g[v][i].v;
            if (d[x] > d[v] + g[v][i].w) {
                d[x] = d[v] + g[v][i].w;
                if (!in_queue[x]) {
                    q.push(x);
                    in_queue[x] = true;
                }
            }
        }
    }
    
    for (int i = 1; i <= n; i++) {
        cout << d[i] << " ";
    }
    return 0;
}

SPFA 判負環

我們可以在入隊時,記錄每個頂點入隊次數 \(\text{cnt}_i\)。如果一個頂點入隊次數大於 \(n\),那麼就出現了負環。

程式碼實現

#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100001;
const int inf = 0x3f3f3f3f;
struct node {
    int v, w;
    node() {
      
    }
    node(int vv, int ww) {
        v = vv;
        w = ww;
    }
};
vector<node> g[N];
int n, m, s;
int d[N], cnt[N];
bool in_queue[N];
queue<int> q;
bool spfa(int s) {
    memset(d, 0x3f, sizeof(d));
    d[s] = 0;
    in_queue[s] = true;
    q.push(s);
    
    cnt[s] ++;
    
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        in_queue[v] = false;
        for (int i = 0; i < g[v].size(); i++) {
            int x = g[v][i].v;
            if (d[x] > d[v] + g[v][i].w) {
                d[x] = d[v] + g[v][i].w;
                if (!in_queue[x]) {
                    q.push(x);
                    in_queue[x] = true;
                    cnt[x] ++;
                    if (cnt[x] > n) {
                        return true;
                    }
                }
            }
        }
    }
    return false;
}
int main() {
    cin >> n >> m >> s;
    for (int i = 0; i < m; i++) {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back(node(v, w));
    }
    if (spfa(s)) {
        cout << "YES" << endl;
    } else {
        cout << "NO" << endl;
    }
    return 0;
}