文章目录
介绍
更多算法模板见 github :https://github.com/surtr1/Algorithm/tree/main/Graphs/APSP
部分内容搬自oiwiki
持续更新中。。。
Floyd
算法原理
Floyd 可以是用来求任意两个结点之间的最短路的。
复杂度比较高,但是常数小,容易实现。
适用于任何图,可以是有向图或者无向图,边权可以为正也可以为负,但是最短路必须存在。(图中不能有负环)
核心思想就是通过考虑图中所有可能的中转点,逐步更新两点间的最短路径长度和路径信息,直至找到最终的最短路径。也称之为插点法。
我们定义数组 d i s [ k ] [ x ] [ y ] dis[k][x][y] dis[k][x][y],表示路径中间只允许经过结点 1 1 1 到 k k k,x 到结点 y 的最短路径长度。
d i s [ n ] [ x ] [ y ] dis[n][x][y] dis[n][x][y] 就是结点 x x x 到 y y y 的最短路长度。
更新公式就是: d i s [ k ] [ x ] [ y ] = m i n ( d i s [ k − 1 ] [ x ] [ y ] , d i s [ k − 1 ] [ x ] [ k ] + d i s [ k − 1 ] [ k ] [ y ] ) dis[k][x][y] = min(dis[k-1][x][y], dis[k-1][x][k]+dis[k-1][k][y]) dis[k][x][y]=min(dis[k−1][x][y],dis[k−1][x][k]+dis[k−1][k][y])
这样做的空间复杂度是 O ( N 3 ) O(N^3) O(N3)
cpp
for (k = 1; k <= n; k++) {
for (x = 1; x <= n; x++) {
for (y = 1; y <= n; y++) {
dis[k][x][y] = min(dis[k - 1][x][y], dis[k - 1][x][k] + dis[k - 1][k][y]);
}
}
}
我们发现第一层的状态 k k k 只会由 k − 1 k - 1 k−1转移过来,所以我们枚举的时候把 k k k这层放到最外面,然后从小到大枚举,这样就可以少一层状态,空间复杂度优化为 O ( N 2 ) O(N^2) O(N2)
cpp
for (k = 1; k <= n; k++) {
for (x = 1; x <= n; x++) {
for (y = 1; y <= n; y++) {
dis[x][y] = min(dis[x][y], dis[x][k] + dis[k][y]);
}
}
}
注意 d i s [ i ] [ i ] = 0 dis[i][i] = 0 dis[i][i]=0 ,如果 i → j i \to j i→j 有边,且边权为 w w w ,则 d i s [ i ] [ j ] = w dis[i][j] = w dis[i][j]=w,否则无边置为 d i s [ i ] [ j ] = ∞ dis[i][j] = ∞ dis[i][j]=∞
Floyd时间复杂度: O ( N 2 ) O(N^2) O(N2),空间复杂度: O ( N 2 ) O(N^2) O(N2)。
传递闭包
已知一个有向图中任意两点之间是否有连边,要求判断任意两点是否连通。
我们只需要按照 Floyd 的过程,逐个加入点判断一下。
只是此时的边的边权变为 1 / 0 1/0 1/0,而取 min 变成了 或 运算。
cpp
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= n; ++j)
dis[i][j] |= dis[i][k] && dis[k][j];
再进一步用 bitset 优化,复杂度可以到 O ( n 3 w ) O(\frac{n^3}{w}) O(wn3)。
cpp
// std::bitset<SIZE> f[SIZE];
for (k = 1; k <= n; k++)
for (i = 1; i <= n; i++)
if (dis[i][k]) dis[i] = dis[i] | dis[k];
Floyd求最小环
给一个正权无向图 ,找一个最小权值和的环(节点数 ≥ 3 \ge 3 ≥3)。
记原图中 u , v u,v u,v 之间边的边权为 v a l ( u , v ) val\left(u,v\right) val(u,v)。
注意到 Floyd 算法有一个性质:在最外层循环到点 k k k 时(尚未开始第 k 次),此时的 d i s u , v dis_{u,v} disu,v 表示为从 u u u 到 v v v 且仅经过编号在 [ 1 , k ) \left[1, k\right) [1,k) 区间中的点的最短路。
最小环至少有三个顶点,设其中编号最大的顶点为 w w w,环上与 w w w 相邻两侧的两个点为 u , v u,v u,v,则在最外层循环枚举到 k = w k=w k=w 时,该环的长度即为 d i s u , v + v a l ( v , w ) + v a l ( w , u ) dis_{u,v}+val\left(v,w\right)+val\left(w,u\right) disu,v+val(v,w)+val(w,u)。
故在循环时对于每个 k k k 枚举满足 i < k , j < k i<k,j<k i<k,j<k 的 ( i , j ) (i,j) (i,j),更新答案即可。
任何一个环,一定存在一个编号最大的节点,故所有可能是最小环的环一定会被枚举到。
记录路径
现在已经知道了环的形式为 u → k → v u\to k\to v u→k→v,然后再从 v v v 回到 u u u(经过的点编号均 < k <k <k)。
问题转化为求 v ⇝ u v\leadsto u v⇝u 的路径。由三角不等式 d i s u , v ≤ d i s u , i + d i s i , v dis_{u,v}\le dis_{u,i}+dis_{i,v} disu,v≤disu,i+disi,v,考虑记录 p o s u , v = j pos_{u,v}=j posu,v=j 表示使得 d i s u , v = d i s u , j + d i s j , v dis_{u,v}=dis_{u,j}+dis_{j,v} disu,v=disu,j+disj,v 的点。显然 j j j 就在 v ⇝ u v\leadsto u v⇝u 的路径上。
于是可以将路径转化为 v ⇝ j v\leadsto j v⇝j 和 j ⇝ u j\leadsto u j⇝u 两段,分别递归处理即可。
上述是解决无向图问题,但Floyd可拓展到有向图(节点数 ≥ 2 \ge 2 ≥2)且更简单些,具体代码看模板:
模板
cpp
#include <vector>
#include <algorithm>
#include <limits>
#include <iostream>
using namespace std;
template<class T>
struct Floyd {
int n;
vector<vector<T>> dis; // 最短路矩阵
vector<vector<T>> val; // 原图邻接矩阵 (用于记录边权和重置)
vector<vector<int>> pos;// 路由表:记录 pos[i][j] = k,即 i->j 中间经过了 k
vector<int> path_res; // 存储结果路径
T min_cycle; // 存储最小环长度
const T INF = numeric_limits<T>::max() / 3;
Floyd(int _n) {
init(_n);
}
void init(int _n) {
n = _n;
dis.assign(n + 1, vector<T>(n + 1, INF));
val.assign(n + 1, vector<T>(n + 1, INF));
pos.assign(n + 1, vector<int>(n + 1, 0));
// 只有在标准 Floyd 寻路时,对自己距离才初始化为 0
// 在具体求解最小环函数中,会根据是有向还是无向重新处理对角线
for (int i = 1; i <= n; ++i) {
val[i][i] = 0;
}
}
void addEdge(int u, int v, T w) {
val[u][v] = min(val[u][v], w);
}
void MinDis() {
dis = val;
for (int k = 1; k <= n; k++) {
for (int x = 1; x <= n; x++) {
for (int y = 1; y <= n; y++) {
dis[x][y] = min(dis[x][y], dis[x][k] + dis[k][y]);
}
}
}
}
// 递归还原路径 i -> ... -> j (不含 i 和 j)
void get_path(int i, int j) {
int k = pos[i][j];
if (k == 0) return; // 直连,无中间点
get_path(i, k);
path_res.push_back(k);
get_path(k, j);
}
// 有向图最小环
void DirMinCycle() {
min_cycle = INF;
path_res.clear();
pos.assign(n + 1, vector<int>(n + 1, 0));
// 对角线 dis[i][i] 必须设为 INF
// 这样才能算出 i -> ... -> i 的回路
dis = val;
for(int i = 1; i <= n; ++i) dis[i][i] = INF;
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis[i][k] != INF && dis[k][j] != INF) {
if (dis[i][k] + dis[k][j] < dis[i][j]) {
dis[i][j] = dis[i][k] + dis[k][j];
pos[i][j] = k; // 记录中间点
}
}
}
}
}
int start = -1;
for (int i = 1; i <= n; i++) {
if (dis[i][i] < min_cycle) {
min_cycle = dis[i][i];
start = i;
}
}
if (start != -1) {
path_res.push_back(start); // 起点
get_path(start, start); // 递归找中间点
// path_res 此时包含:起点 -> 中间点...
// 这是一个闭环序列,如 1 -> 2 -> 3 (意味着 1->2, 2->3, 3->1)
}
}
// 无向图最小环
void UndirMinCycle() {
min_cycle = INF;
path_res.clear();
pos.assign(n + 1, vector<int>(n + 1, 0));
//无向图对角线设为 0,因为要算两点间真实最短路
dis = val;
for(int i = 1; i <= n; ++i) dis[i][i] = 0;
for (int k = 1; k <= n; k++) {
// (1) 先判断环 (必须在更新 dis 之前)
// 环由 i->j (最短路, 不经过k) + i->k (直接) + k->j (直接) 组成
for (int i = 1; i < k; i++) {
for (int j = 1; j < i; j++) {
if (dis[i][j] != INF && val[i][k] != INF && val[k][j] != INF) {
T cur_len = dis[i][j] + val[i][k] + val[k][j];
if (cur_len < min_cycle) {
min_cycle = cur_len;
// 记录路径
path_res.clear();
path_res.push_back(i); // 环的一部分 i
get_path(i, j); // i 到 j 之间的路
path_res.push_back(j); // 环的一部分 j
path_res.push_back(k); // 环的最大节点 k
}
}
}
}
// (2) 后更新最短路
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (dis[i][k] != INF && dis[k][j] != INF) {
if (dis[i][k] + dis[k][j] < dis[i][j]) {
dis[i][j] = dis[i][k] + dis[k][j];
pos[i][j] = k;
}
}
}
}
}
}
vector<int> getPath() {
return path_res;
}
};
题目
cpp
/*
https://www.luogu.com.cn/problem/B3647
*/
void solve1() {
int n, m;
cin >> n >> m;
Floyd<long long> work(n);
for (int i = 1; i <= m; i++){
int u, v, w;
cin >> u >> v >> w;
work.addEdge(u, v, w);
work.addEdge(v, u, w);
}
work.MinDis();
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++)
cout << work.dis[i][j] << " ";
cout << "\n";
}
}
cpp
/*
最小环:
https://www.luogu.com.cn/problem/P6175
*/
void solve2() {
int n, m;
cin >> n >> m;
Floyd<long long> work(n);
for (int i = 1; i <= m; i++){
int u, v, w;
cin >> u >> v >> w;
work.addEdge(u, v, w);
work.addEdge(v, u, w);
}
work.UndirMinCycle();
long long w = work.min_cycle;
if (w == work.INF) cout << "No solution.\n";
else cout << w << "\n";
}
Johnson 算法
算法原理
任意两点间的最短路可以通过枚举起点,跑 n n n 次 B e l l m a n -- F o r d Bellman--Ford Bellman--Ford 算法解决,时间复杂度是 O ( n 2 m ) O(n^2m) O(n2m) 的,也可以直接用 Floyd 算法解决,时间复杂度为 O ( n 3 ) O(n^3) O(n3)。
我们发现跑 n n n次 D i j k s t r a Dijkstra Dijkstra 要比上述跑 n n n 次 B e l l m a n -- F o r d Bellman--Ford Bellman--Ford 算法的时间复杂度更优秀,在稀疏图上也比 F l o y d Floyd Floyd 算法的时间复杂度更加优秀。
那我们可以使用 D i j k s t r a Dijkstra Dijkstra,但是 D i j k s t r a Dijkstra Dijkstra 算法不能正确求解带负权边的最短路,因此我们需要对原图上的边进行预处理,确保所有边的边权均非负。
我们新建一个虚拟节点(在这里我们就设它的编号为 0 0 0)。从这个点向其他所有点连一条边权为 0 的边。
接下来用 B e l l m a n -- F o r d Bellman--Ford Bellman--Ford 算法求出从 0 0 0 号点到其他所有点的最短路,记为 h i h_i hi。
假如存在一条从 u u u 点到 v v v 点,边权为 w w w 的边,则我们将该边的边权重新设置为 w + h u − h v w+h_u-h_v w+hu−hv。
接下来以每个点为起点,跑 n n n 轮 D i j k s t r a Dijkstra Dijkstra 算法即可求出任意两点间的最短路了。
使用 p r i o r i t y q u e u e priority_queue priorityqueue 实现 D i j k s t r a Dijkstra Dijkstra 算法,该算法的时间复杂度是 O ( n m log m ) O(nm\log m) O(nmlogm)。
证明
为什么能这样做?
我们先讨论一个物理概念------势能。
诸如重力势能,电势,势能的变化量只和起点和终点的相对位置有关,而与起点到终点所走的路径无关。
势能还有一个特点,势能的绝对值往往取决于设置的零势能点,但无论将零势能点设置在哪里,两点间势能的差值是一定的。
设原图中边 ( u , v ) (u, v) (u,v) 的权重为 w ( u , v ) w(u, v) w(u,v)。
我们引入势能函数 h ( x ) h(x) h(x)(表示从新建源点到其它点的最短路),定义新边权 w ^ ( u , v ) \hat{w}(u, v) w^(u,v) 为: w ( u , v ) + h ( u ) − h ( v ) w(u,v)+h(u)−h(v) w(u,v)+h(u)−h(v)
证明1:路径等价性 (为什么最短路没变?)
对于一条从起点 s s s 到终点 t t t 的路径 p : s = v 0 → v 1 → ⋯ → v k = t p: s = v_0 \to v_1 \to \dots \to v_k = t p:s=v0→v1→⋯→vk=t,其在新图中的总长度为:
L e n g t h n e w ( p ) = ∑ i = 0 k − 1 w ^ ( v i , v i + 1 ) = ∑ i = 0 k − 1 ( w ( v i , v i + 1 ) + h ( v i ) − h ( v i + 1 ) ) Length_{new}(p)=\sum_{i=0}^{k-1} \hat{w}(v_i, v_{i+1}) = \sum_{i=0}^{k-1} (w(v_i, v_{i+1}) + h(v_i) - h(v_{i+1})) Lengthnew(p)=∑i=0k−1w^(vi,vi+1)=∑i=0k−1(w(vi,vi+1)+h(vi)−h(vi+1))
相消后: L e n g t h n e w ( p ) = L e n g t h o l d ( p ) + h ( s ) − h ( t ) Length_{new}(p)=Length_{old}(p)+h(s)−h(t) Lengthnew(p)=Lengthold(p)+h(s)−h(t)
对于固定的起点 s s s 和终点 t t t, h ( s ) − h ( t ) h(s) - h(t) h(s)−h(t) 是一个常数。
因此,无论选哪条路径,新长度都只比原长度多了一个固定值。使原长度最小的路径,必然也使新长度最小。
结论:新图中的最短路径即为原图中的最短路径。
证明2:非负性 (为什么可以用 Dijkstra?)
根据三角不等式 ,对于图中任意一条边 ( u , v ) (u, v) (u,v),其权重为 w ( u , v ) w(u, v) w(u,v)。根据最短路径的性质,从 S S S 到 v v v 的最短距离一定小于等于从 S S S 先到 u u u 再经过边 ( u , v ) (u,v) (u,v) 到达 v v v 的距离。
即必须满足: h ( v ) ≤ h ( u ) + w ( u , v ) h(v) \le h(u) + w(u,v) h(v)≤h(u)+w(u,v)
移项可得: 0 ≤ w ( u , v ) + h ( u ) − h ( v ) 0 \le w(u,v) + h(u) - h(v) 0≤w(u,v)+h(u)−h(v)
故而新边权 w ^ = w ( u , v ) + h ( u ) − h ( v ) ≥ 0 \hat{w} = w(u,v) + h(u) - h(v) \ge 0 w^=w(u,v)+h(u)−h(v)≥0
所有新边权都非负,我们后续才能使用 Dijkstra 算法。
模板
cpp
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
#include <limits>
using namespace std;
template <class T>
struct Johnson {
struct Edge {
int to;
T w;
};
int n;
vector<vector<Edge>> adj;
vector<T> h;
vector<vector<T>> dis; // dis[i][j] 表示 i 到 j 的最短路
const T INF = numeric_limits<T>::max() / 2;
Johnson(int _n) : n(_n) {
adj.resize(n + 1);
h.assign(n + 1, 0);
dis.assign(n + 1, vector<T>(n + 1, INF));
}
void addEdge(int u, int v, T w) {
adj[u].push_back({v, w});
}
// SPFA: 计算势能 h[]
// 返回 false 表示存在负环
bool spfa() {
vector<int> cnt(n + 1, 0);
vector<bool> in_queue(n + 1, false);
queue<int> q;
// 隐式构建虚拟源点:将所有点入队,初始势能 h=0
for (int i = 1; i <= n; i++) {
h[i] = 0;
in_queue[i] = true;
q.push(i);
}
while (!q.empty()) {
int u = q.front();
q.pop();
in_queue[u] = false;
for (const auto& e : adj[u]) {
int v = e.to;
T w = e.w;
if (h[v] > h[u] + w) {
h[v] = h[u] + w;
if (!in_queue[v]) {
q.push(v);
in_queue[v] = true;
cnt[v]++;
if (cnt[v] > n) return false; // 负环检测
}
}
}
}
return true;
}
// Dijkstra: 计算 s 到其他点的最短路
void dijkstra(int s) {
priority_queue<pair<T, int>, vector<pair<T, int>>, greater<pair<T, int>>> pq;
//d是引入势能后的新最短路
vector<T> d(n + 1, INF);
vector<bool> vis(n + 1, false);
d[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (vis[u]) continue;
vis[u] = true;
for (const auto& e : adj[u]) {
int v = e.to;
// w_new = w + h[u] - h[v]
T new_w = e.w + h[u] - h[v];
if (d[v] > d[u] + new_w) {
d[v] = d[u] + new_w;
pq.push({d[v], v});
}
}
}
// 还原真实距离:RealDist = NewDist + h[v] - h[s]
for (int i = 1; i <= n; i++) {
if (d[i] == INF) {
dis[s][i] = INF;
} else {
dis[s][i] = d[i] + h[i] - h[s];
}
}
}
bool work() {
// 1. 计算势能
if (!spfa()) return false;
// 2. N 次 Dijkstra
for (int i = 1; i <= n; i++) {
dijkstra(i);
}
return true;
}
};
题目练习
https://www.luogu.com.cn/problem/P5905
cpp
/*
https://www.luogu.com.cn/problem/P5905
*/
void solve() {
int n, m;
cin >> n >> m;
Johnson<long long> john(n);
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
john.addEdge(u, v, w);
}
if(!john.work()) {
cout << "-1\n";
return;
}
for (int i = 1; i <= n; i++) {
long long ans = 0;
for (int j = 1; j <= n; j++) {
long long d = john.dis[i][j];
if(d == john.INF) ans += 1e9 * j;
else ans += j*d;
}
cout << ans << "\n";
}
}