[ABC061D]Score Attack /「模板」Bellman-ford
做了这题我发现
- 我不懂单源最短路。
- BF 还是有用的。
给出有向图,求单源最长路,或指出有环。
开始时觉得这不是直接写个 spfa 记录一下每个点更新次数解决?然后就 WA 了。
其实这是一个严重的历史遗留问题,即学习最短路算法时不经 BF 直接学习并记下 SPFA。
所以本博客又名《从 BF 到 SPFA》。
Bellman-Ford
首先,WA 是很显然的,出现环了不代表在到 \(n\) 的路上一定有。那难道要找出每个环上的点并一一判断是否在到 \(n\) 的路径上吗?其实并不是,只需要对代码进行小小的改动就可以了。
先回到 BF,BF 的流程是这样的:
- 迭代 \(n-1\) 次。
- 每一次尝试松弛每条边。
其实感性理解一下十分的简单,就是一个 dp 的思想,因为每个路径不可能有超过 \(n-1\) 条边,而每次进行一次迭代时会让最短路延长上 \(1\),所以迭代 \(n-1\) 次就可以出结果了。
那么如何判环呢?很简单,如果再来一次某个点的最短路又双叒叕被更新了,那么在前往其的路径上肯定有一个环。注意,是前往其的路径上有一个环。
然后还有一个易错点,先来看以下错误判环代码
for (int j = 1; j <= m; j++)
if (l[j].v == n && dis[l[j].v] > dis[l[j].u] + l[j].z)
return std::cout << "inf", 0;
直观的想一想 BF 到底在干什么?它从起始点开始,一步步往后走,遇到岔路就分个身往前走,一直走 \(n-1\) 步,现在的 \(d\) 值就是走了 \(n-1\) 步以内的,而这样子只判了一步。
这个错误非常难以发现(至少我非常难发现),甚至很多地方的数据弱(说的就是你,AtCoder),你只要记一下前面 \(d_n\) 的值再在后面对比一下就可以通过。
那么如何改正?有了那个感性的理解,可以知道,只要让它的某一个分身走过那个环后再次到达 \(n\) 就可以了,环的大小肯定不超过 \(n\),所以只要把这个跑 \(n\) 遍后再判断即可。
代码
#include <iostream>
#include <cstring>
#define int long long
const int N = 1005, M = 2005, INF = 0x3f3f3f3f3f3f3f3fll;
int n, m, dis[N], u[M], v[M], c[M];
signed main() {
std::cin >> n >> m;
for (int i = 1; i <= m; i++)
std::cin >> u[i] >> v[i] >> c[i], c[i] = -c[i];
memset(dis, 0x3f, sizeof dis);
dis[1] = 0;
for (int i = 1; i < n; i++)
for (int j = 1; j <= m; j++)
dis[v[j]] = std::min(dis[v[j]], dis[u[j]] + c[j]);
int p = dis[n];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
dis[v[j]] = std::min(dis[v[j]], dis[u[j]] + c[j]);
if (dis[n] != p) return std::cout << "inf", 0;
std::cout << -dis[n];
}
Shortsest Path Faster Algorithm
没错,以上就是 SPFA 的全称。提出这个名儿的论文在这,但里面的证明据说是错光的。
其实这个算法就是 BF 的队列优化,并没有改变 BF 的最坏复杂度,但在随机的数据下跑得飞快。
SPFA 的思路是这样的:
- 搞个队列,把起点塞进去。
- 取出队首,尝试松弛和它相连的边。
- 重复这两步直到队列空。
优化就在于少了很多不必要的更新,真正践行了我们的“感性理解”,就是后面没用的步数就不要走了,只走从头开始能走到的。
有了前面的铺垫,卡它的方式……似乎还不是那么显而易见。可以看 fstqwq 的回答
显然,普通 SPFA 是非常好卡的,只需要一个随机网格图(在网格图中走错一次路可能导致很高的额外开销),或者一个构造过的链套菊花(使得队列更新菊花的次数非常高)即可。很多奇怪写法的 SPFA 都只能通过两者中的至多一种,因此你只需要将图构造为网格套菊花即可。