最短路問題
最短路問題
在帶權圖中,每條邊都有一個權值,就是邊的長度。路徑的長度等於經過所有邊權之和,求最小值。
如上圖,從 \(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\),接着執行如下操作:
- 從 \(V-U\) 中找出一個距離源點最近的頂點 \(v\),將 \(v\) 加入集合 \(U\)。
- 並用 \(d_v\) 和頂點 \(v\) 連出來的邊來更新和 \(v\) 相鄰的、不在集合 \(U\) 中的頂點 \(d\),這一步成為松弛操作。
- 重復 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\) 是否在隊列中。
- 初始隊列僅有源點,且 \(d_s=0\)。
- 取出隊頂元素 \(u\),掃描從 \(u\) 出發的每一條邊,設每條邊的另一端為 \(v\),邊 \(u,v\) 的權值為 \(w\),若 \(d_u+w<d_v\),則
- 將 \(d_v\) 修改為 \(d_u+w\)。
- 若 \(v\) 不在隊列中,則將 \(v\) 入隊。
- 重復 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;
}