全源最短路封装模板(APSP,Floyd求最小环,Floyd求最短路,Johnson算法)

文章目录

介绍

更多算法模板见 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;
    }
};

题目

  1. 最短路:https://www.luogu.com.cn/problem/B3647
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";
    }
}
  1. 最小环 :https://www.luogu.com.cn/problem/P6175
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";
    }
}
相关推荐
梭七y12 小时前
【力扣hot100题】(105)三数之和
数据结构·算法·leetcode
Morwit12 小时前
如何使用CMake构建Qt新项目
开发语言·c++·qt
zmzb010313 小时前
C++课后习题训练记录Day62
开发语言·c++
fpcc13 小时前
C++23中的模块应用说明之二整体说明和导出控制
c++·c++23
我想吃余14 小时前
【C++篇】C++11:线程库
开发语言·c++
CSDN_RTKLIB14 小时前
【静态初始化与动态初始化】术语对比
开发语言·c++
WhereIsMyChair14 小时前
DPO 核心损失函数β调大可以控制不偏离ref模型太远
人工智能·算法·机器学习
智者知已应修善业14 小时前
【组合数】2024-3-16
c语言·c++·经验分享·笔记·算法
天上飞的粉红小猪14 小时前
线程同步与互斥
linux·开发语言·c++