题解:洛谷 P5905 全源最短路(Johnson)
【题目来源】
【题目描述】
给定一个包含 \(n\) 个结点和 \(m\) 条带权边的有向图,求所有点对间的最短路径长度,一条路径的长度定义为这条路径上所有边的权值和。
注意:
-
边权可能为负,且图中可能存在重边和自环;
-
部分数据卡 \(n\) 轮 SPFA 算法。
【输入】
第 \(1\) 行:\(2\) 个整数 \(n,m\),表示给定有向图的结点数量和有向边数量。
接下来 \(m\) 行:每行 \(3\) 个整数 \(u,v,w\),表示有一条权值为 \(w\) 的有向边从编号为 \(u\) 的结点连向编号为 \(v\) 的结点。
【输出】
若图中存在负环,输出仅一行 \(-1\)。
若图中不存在负环:
输出 \(n\) 行:令 \(dis_{i,j}\) 为从 \(i\) 到 \(j\) 的最短路,在第 \(i\) 行输出 \(\sum\limits_{j=1}^n j\times dis_{i,j}\),注意这个结果可能超过 int 存储范围。
如果不存在从 \(i\) 到 \(j\) 的路径,则 \(dis_{i,j}=10^9\);如果 \(i=j\),则 \(dis_{i,j}=0\)。
【输入样例】
5 7
1 2 4
1 4 10
2 3 7
4 5 3
4 2 -2
3 4 -3
5 3 4
【输出样例】
128
1000000072
999999978
1000000026
1000000014
【核心思想】
-
问题分析:给定一个包含 \(n\) 个结点和 \(m\) 条带权边的有向图,边权可能为负,且可能存在重边和自环。要求计算所有点对之间的最短路径长度,若图中存在负环则输出 \(-1\),否则输出每行 \(\sum_{j=1}^n j \times dist_{i,j}\)(不可达时 \(dist_{i,j}=10^9\),\(dist_{i,i}=0\))。由于边权可负且需要全源最短路,直接使用 Floyd 复杂度 \(O(n^3)\) 可能无法承受(\(n\) 可达 \(3000\)),而多源 SPFA 也容易超时(题目特别说明卡 \(n\) 轮 SPFA)。因此需要一种既能处理负权边,又能高效求解全源最短路的算法。
-
算法选择:
- Johnson 算法:通过引入“势函数”(potential)对边权进行重新赋值,使得所有边权变为非负,然后利用 \(n\) 次 Dijkstra 求出全源最短路。该算法适合稀疏图,时间复杂度 \(O(n m \log n)\),并能处理负权边(前提是无负环)。
- SPFA:用于计算势函数,同时检测负环。从超级源点 \(0\) 向所有点连权值为 \(0\) 的边,运行 SPFA 得到每个点的最短距离 \(h[v]\)(即势函数),若存在负环则输出 \(-1\)。
- Dijkstra(堆优化):重赋权后边权非负,对每个点作为源点运行一次 Dijkstra,得到重赋权后的最短路 \(d'[v]\),再还原真实距离 \(dist[u][v] = d'[v] - h[u] + h[v]\)。
-
关键步骤:
- 预处理与建图:读入 \(n, m\) 及所有有向边 \((u, v, w)\),存入邻接表。添加超级源点 \(0\),向每个点 \(i\) 连一条权值为 \(0\) 的边,用于 SPFA 求势函数。
- SPFA 求势函数并检测负环:从超级源点 \(0\) 出发运行 SPFA,记录每个点的最短距离 \(h[v]\) 和入队次数 \(cnt[v]\)。若某点入队次数超过 \(n\),则存在负环,输出 \(-1\) 并结束。
- 重赋权(reweighting):对每条原边 \((u, v, w)\),将其权重修改为 \(w' = w + h[u] - h[v]\)。根据三角不等式,\(h[v] \le h[u] + w\),故 \(w' \ge 0\),保证非负。
- \(n\) 次 Dijkstra:对每个节点 \(i\)(\(1 \le i \le n\)),以 \(i\) 为源点运行堆优化 Dijkstra,得到重赋权后的最短距离 \(d'[j]\)(\(j=1..n\))。
- 还原真实距离并输出:原图最短距离 \(dist[i][j] = d'[j] - h[i] + h[j]\)。若 \(d'[j]\) 为无穷大(不可达),则 \(dist[i][j] = 10^9\)。计算每行 \(\sum_{j=1}^n j \times dist_{i,j}\) 并输出。
-
时间/空间复杂度:
- 时间复杂度:\(O(n m)\)(SPFA)+ \(O(n m \log n)\)(\(n\) 次 Dijkstra),即 \(O(n m \log n)\),其中 \(n\) 为点数,\(m\) 为边数。
- 空间复杂度:\(O(n + m)\),存储邻接表和势函数数组。
-
Johnson 算法的核心思想:
- 势函数:利用 SPFA 从超级源点求得 \(h[v]\),满足 \(h[v] \le h[u] + w(u, v)\),从而保证重赋权后边权非负,使得 Dijkstra 可用于负权图。
- 重赋权技巧:将边权修改为 \(w' = w + h[u] - h[v]\),这不会改变最短路径的拓扑结构,因为对于任意路径 \(p\),其重赋权长度与原长度之差为常数 \(h[起点] - h[终点]\),因此最短路径保持不变。
- 负环检测:SPFA 入队次数超过 \(n\) 即表示存在负环,此时全源最短路无定义,直接输出 \(-1\)。
- 适用场景:适用于边权可负、但无负环的稀疏图全源最短路问题,是处理负权边且避免多次 SPFA 的高效方案。
【算法标签】
提高 #Johnson
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 使用 long long 防止中间结果溢出
typedef pair<int, int> PII; // 用于优先队列存储 {距离, 节点编号}
const int N = 3005; // 最大节点数
const int INF = 1e9; // 定义不可达距离(10^9)
int n, m; // n: 节点数, m: 边数
int ans; // 存储每次 Dijkstra 后计算的答案(点对最短路径加权和)
vector<PII> g[N]; // 邻接表存储原图(有向边)
int dis[N]; // 势函数(从超级源点到各点的最短距离)
int d[N]; // 当前源点 Dijkstra 求得的距离(重赋权后)
int cnt[N]; // 记录 SPFA 入队次数,用于检测负环
// 使用 SPFA 从超级源点 0 出发,检测负环并计算势函数
void spfa()
{
memset(dis, 0x3f, sizeof(dis)); // 初始化为极大值
dis[0] = 0; // 超级源点到自身距离为 0
queue<int> q;
q.push(0);
while (!q.empty())
{
int u = q.front();
q.pop();
for (auto [v, w] : g[u]) // 遍历所有出边
{
if (dis[v] > dis[u] + w) // 可以松弛
{
dis[v] = dis[u] + w;
q.push(v);
cnt[v] = cnt[u] + 1; // 记录入队次数(相对于 u 的路径长度)
if (cnt[v] > n) // 入队次数超过节点数,存在负环
{
cout << -1 << endl;
exit(0); // 直接终止程序
}
}
}
}
}
// 重赋权后的 Dijkstra 算法,计算从源点 s 到所有节点的最短路
void dij(int s)
{
memset(d, 0x3f, sizeof(d)); // 初始化距离数组
priority_queue<PII, vector<PII>, greater<PII>> q;
d[s] = 0;
q.push({0, s});
while (!q.empty())
{
auto [D, u] = q.top();
q.pop();
if (D > d[u]) // 跳过过时记录
continue;
for (auto [v, w] : g[u]) // 注意:此时图上的边权已经被重赋为 w'
{
if (d[v] > d[u] + w)
{
d[v] = d[u] + w;
q.push({d[v], v});
}
}
}
}
signed main()
{
cin >> n >> m; // 输入节点数和边数
// 读入所有有向边
while (m--)
{
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
}
// 添加超级源点 0,向每个节点连一条权值为 0 的边,用于求势函数
for (int i = 1; i <= n; i++)
g[0].push_back({i, 0});
// 运行 SPFA,得到势函数 dis[],同时检测负环
spfa();
// 重赋权:对每条边 (u, v, w),新权重 w' = w + dis[u] - dis[v] (保证非负)
for (int u = 1; u <= n; u++)
for (auto &[v, w] : g[u])
w += dis[u] - dis[v];
// 对每个节点作为源点,运行 Dijkstra 求最短路径
for (int i = 1; i <= n; i++)
{
dij(i); // 计算从 i 出发到所有节点的最短距离(重赋权后)
ans = 0;
for (int j = 1; j <= n; j++)
{
// 如果原图中 i 到 j 不可达(重赋权后仍为 INF),则原距离为 INF
if (d[j] >= INF)
ans += j * INF;
else
// 还原真实距离:原距离 = d[j] + dis[j] - dis[i]
ans += j * (d[j] + dis[j] - dis[i]);
}
cout << ans << endl; // 输出第 i 行的结果
}
return 0;
}
【运行结果】
5 7
1 2 4
1 4 10
2 3 7
4 5 3
4 2 -2
3 4 -3
5 3 4
128
1000000072
999999978
1000000026
1000000014
浙公网安备 33010602011771号