[ABC061D]Score Attack /「模板」Bellman-ford

做了这题我发现

  1. 我不懂单源最短路。
  2. BF 还是有用的。

给出有向图,求单源最长路,或指出有环。

开始时觉得这不是直接写个 spfa 记录一下每个点更新次数解决?然后就 WA 了。
其实这是一个严重的历史遗留问题,即学习最短路算法时不经 BF 直接学习并记下 SPFA

所以本博客又名《从 BF 到 SPFA》。

Bellman-Ford

首先,WA 是很显然的,出现环了不代表在到 \(n\) 的路上一定有。那难道要找出每个环上的点并一一判断是否在到 \(n\) 的路径上吗?其实并不是,只需要对代码进行小小的改动就可以了。
先回到 BF,BF 的流程是这样的:

  1. 迭代 \(n-1\) 次。
  2. 每一次尝试松弛每条边。

\[\boxed{ \begin{aligned} \text{Be}&\text{llman-Ford 伪代码}\\ \hline 1.\ &\text{for } i = 1 \to n-1 \\ 2.\ & \kern{2em}\text{for } j = 1 \to m \\ 3.\ & \kern{4em} d_{v_j} = \min\{d_{v_j}, d_{u_j} + l_j\}\\ \end{aligned} } \]

其实感性理解一下十分的简单,就是一个 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 的思路是这样的:

  1. 搞个队列,把起点塞进去。
  2. 取出队首,尝试松弛和它相连的边。
  3. 重复这两步直到队列空。

优化就在于少了很多不必要的更新,真正践行了我们的“感性理解”,就是后面没用的步数就不要走了,只走从头开始能走到的。
有了前面的铺垫,卡它的方式……似乎还不是那么显而易见。可以看 fstqwq 的回答

显然,普通 SPFA 是非常好卡的,只需要一个随机网格图(在网格图中走错一次路可能导致很高的额外开销),或者一个构造过的链套菊花(使得队列更新菊花的次数非常高)即可。很多奇怪写法的 SPFA 都只能通过两者中的至多一种,因此你只需要将图构造为网格套菊花即可。

posted @ 2021-08-11 16:33  Acfboy  阅读(83)  评论(0编辑  收藏  举报