问题引入:(【模板】Johnson 全源最短路 - 洛谷)
ps:(如果不想 情景带入 请直接转跳到Johnson算法详解)
目录
题目描述
给定一个包含 n 个结点和 m 条带权边的有向图,求所有点对间的最短路径长度,一条路径的长度定义为这条路径上所有边的权值和。
注意:
边权可能为负,且图中可能存在重边和自环;
部分数据卡 n 轮 SPFA 算法。
输入格式
第 1 行:222 个整数 n,m,表示给定有向图的结点数量和有向边数量。
接下来 m 行:每行 333 个整数 u,v,w,表示有一条权值为 w 的有向边从编号为 u 的结点连向编号为 v 的结点。
输出格式
![]()
输入输出样例
输入 #1
5 7 1 2 4 1 4 10 2 3 7 4 5 3 4 2 -2 3 4 -3 5 3 4输出 #1
128 1000000072 999999978 1000000026 1000000014输入 #2
5 5 1 2 4 3 4 9 3 4 -3 4 5 3 5 3 -2输出 #2
-1说明/提示
【样例解释】
左图为样例 111 给出的有向图,最短路构成的答案矩阵为:
0 4 11 8 11 1000000000 0 7 4 7 1000000000 -5 0 -3 0 1000000000 -2 5 0 3 1000000000 -1 4 1 0
右图为样例 222 给出的有向图,红色标注的边构成了负环,注意给出的图不一定连通。
【数据范围】
![]()
通过看题我们可以得到什么有帮助的信息:
1.SPFA已死(题明确表示SPFA将会被卡 --> T)
2.给的边权可能为 负
3.这是一道全源最短路问题
常用最短路时间复杂度:
朴素dijkstra算法 --> 时间复杂是 O(n2+m), n 表示点数,m 表示边数
堆优化版dijkstra --> 时间复杂度 O(mlogn), n 表示点数,m 表示边数
Bellman-Ford算法 --> 时间复杂度 O(nm), n 表示点数,m 表示边数
spfa 算法(队列优化的Bellman-Ford算法)--> 时间复杂度 平均情况下 O(m),最坏情况下 O(nm), n 表示点数,m 表示边数
floyd算法 --> 时间复杂度是 O(n3), n 表示点数
请再次看一下本题数据范围:
我们可能会很头疼 ):
因为无论是单跑 floyd算法 还是
跑n遍 朴素dijkstra算法(不能处理负权问题)or Bellman-Ford算法 or spfa 算法
都难逃T的命运 ):
但!但是,我们可以发现
如果我们可以跑 n遍 堆优化版dijkstra 就可以控制在时间允许的范围去解决这个多源最短路问题。
但!但是,堆优化版dijkstra 不能处理负权问题
这就抛出了一个思路:
我们是否可以将所有边权 想办法 变成正(是可以为0的,因为0并不会影响跑 堆优化版dijkstra)的,然后跑 n遍 堆优化版dijkstra
好滴!
这就引进了一种新的算法:Johnson 全源最短路
----------------------------------------------------------------------我不是分割线 (: (: (:
Johnson 全源最短路 详解
解决问题 最暴力的方式 就是层层攻破(滑稽,我就喜欢搞“暴力”)
第一个问题:如何可以把负权都转变成正权
为了更好的帮助理解,我借用b站Johnson 全源最短路讲解视频
[Rocky]Johnson全源最短路-C++竞赛级入门第三十一节
[Rocky]Johnson全源最短路-C++竞赛级入门第三十一节_哔哩哔哩_bilibili
中引入的 势 这一概念
假设有一个"nb点"(下面简称nb),nb可以到所有点 & nb到所有点的距离都相同(为了更好的描述,将距离 设为 k)
下面是图示(便于理解)
如果加上这个nb点跑最短路会得到什么呢?
为了更舒服与方便的去写&理解,我们不防将k == 0
示意图如下:
跑最短路(nb到其他点的最短距离)
示意图如下:
ps:绿色数值 代表nb到该点的最短距离
这时候又出现了一个问题:这!啊这?到底有什么用呢?
约定:u点到v点 记作 u --> v,权值为 w
nb点到x点的最短距离为 head(x)
我们可以先拿一个负权边看一下:
3 --> 4 :
-3 + 0 - (-3) == 0;
也就是:w + head(u) - head(v)
要不拿一条正权边看看?
2 --> 3
7 + (-5) - 0 == 2
可以惊奇的发现:nb(值nb点) NB 啊!!!
现在已经解决将负权边 转换为 正权边的方法了!!! (:
如果很纠结 这样操作为啥是可行的 ???
不用纠结下面就给你简单的证明:
将负权边 转 正权的可行性证明:
因为我们 预处理操作(得到head[某点]值的操作)是跑的最短路
u -- v w
一定有 h(v) <= h(u) + w (原理)
那么也一定有:
w + h(u) - h(v) >= 0
只是简单的证明,应该可以帮助您理解 (:
呃呃呃... ...
似乎又抛出了一个问题:
现在全部都是正权边,但最后最短路跑出来的结果要怎么 推回去呢?
跑完最短路答案的回推:
约定:dist[j] 是初始点(可以自己设置,根据题意设置初始点) 到 j 点的最短距离
j*dist[j]+head[j]-head[i] (每次就是简单倒推回去)
为什么要乘 j 呢?
因为中间经过了j个点,我们需要把每一个点都倒推回去哦!!!
如果有点难理解,希望您可以先耐心往下看,结合代码可以更好的帮助您理解 (:
就这道题分析(上代码):
先看一下整体代码:
#include <bits/stdc++.h>
#define buff \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0)
#define int long long
#define endl "\n"
#define PII pair<int, int>
using namespace std;
const int N = 1e4;
int n, m;
int e[N], w[N], ne[N], h[N], idx;
bool st[N];
int dist[N];
int head[N];
int cnt[N];
void add(int a, int b, int c)
{
ne[++idx] = h[a];
e[idx] = b;
w[idx] = c;
h[a] = idx;
}
bool spfa()
{
queue<int> q;
for (int i = 1; i <= n; i++)
{
q.push(i);
st[i] = 1;
}
while (q.size())
{
int t = q.front();
q.pop();
st[t] = 0;
for (int i = h[t]; i; i = ne[i])
{
int j = e[i];
if (head[t] + w[i] < head[j])
{
head[j] = head[t] + w[i];
cnt[j]++;
if (cnt[j] >= n + 1)
return 1;
if (!st[j])
{
st[j] = 1;
q.push(j);
}
}
}
}
return 0;
}
void dij(int s)
{
for (int i = 1; i <= n; i++)
{
st[i] = 0;
dist[i] = 1e9;
}
priority_queue<PII> q;
dist[s] = 0;
q.push(make_pair(-dist[s], s));
while (q.size())
{
int u = q.top().second;
q.pop();
if (st[u] == 1)
continue;
st[u] = 1;
for (int i = h[u]; i; i = ne[i])
{
int v = e[i];
if (dist[u] + w[i] < dist[v])
{
dist[v] = dist[u] + w[i];
q.push(make_pair(-dist[v], v));
}
}
}
}
signed main()
{
buff;
cin >> n >> m;
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
for (int i = 1; i <= n; i++)
{
add(0, i, 0);
}
if (spfa())
{
cout << -1 << endl;
return 0;
}
for (int i = 1; i <= n; i++)
{
for (int j = h[i]; j; j = ne[j])
{
w[j] += head[i] - head[e[j]];
}
}
//测试使用
// for(int i=1;i<=n;i++)
// cout<<head[i]<<endl;
for (int i = 1; i <= n; i++)
{
dij(i);
int ans = 0;
for (int j = 1; j <= n; j++)
{
if (dist[j] == 1e9)
ans += j * 1e9;
else
ans += j * (dist[j] + head[j] - head[i]);
}
cout << ans << endl;
}
}
局部代码解析or功能(目的):
bool spfa()
{
queue<int> q;
for (int i = 1; i <= n; i++)
{
q.push(i);
st[i] = 1;
}
while (q.size())
{
int t = q.front();
q.pop();
st[t] = 0;
for (int i = h[t]; i; i = ne[i])
{
int j = e[i];
if (head[t] + w[i] < head[j])
{
head[j] = head[t] + w[i];
cnt[j]++;
if (cnt[j] >= n + 1)
return 1;
if (!st[j])
{
st[j] = 1;
q.push(j);
}
}
}
}
return 0;
}
跑这个spfa是为了:
1.判断负环
2.得到head(x)的值 (也就是上午提到的nb点到其他点的最短距离)
for (int i = 1; i <= n; i++)
{
for (int j = h[i]; j; j = ne[j])
{
w[j] += head[i] - head[e[j]];
}
}
为了使负权边 变成 正权边
void dij(int s)
{
for (int i = 1; i <= n; i++)
{
st[i] = 0;
dist[i] = 1e9;
}
priority_queue<PII> q;
dist[s] = 0;
q.push(make_pair(-dist[s], s));
while (q.size())
{
int u = q.top().second;
q.pop();
if (st[u] == 1)
continue;
st[u] = 1;
for (int i = h[u]; i; i = ne[i])
{
int v = e[i];
if (dist[u] + w[i] < dist[v])
{
dist[v] = dist[u] + w[i];
q.push(make_pair(-dist[v], v));
}
}
}
}
常规堆优化版dijkstra
for (int i = 1; i <= n; i++)
{
dij(i);
int ans = 0;
for (int j = 1; j <= n; j++)
{
if (dist[j] == 1e9)
ans += j * 1e9;
else
ans += j * (dist[j] + head[j] - head[i]);
}
cout << ans << endl;
}
这段代码的目的:
1.倒推回去得到答案
2.输出答案
最后感谢您的阅读!!!
(: (: (:
生活这条狗啊,追的我连从容撒泡尿的时间都没有。——《英雄时代》