题解:洛谷 P2850 Wormholes

【题目来源】

洛谷:P2850 [USACO06DEC] Wormholes G - 洛谷

【题目描述】

Farmer John 在探索他的农场时发现了许多神奇的虫洞。虫洞的特性非常特殊——它是一个单向通道,能将你传送到它的目的地,而且时间还会回溯到过去!FJ 的每个农场包含 \(N (1 \le N \le 500)\) 块编号为 \(1 \sim N\) 的田地、\(M (1 \le M \le 2500)\) 条双向路径和 \(W (1 \le W \le 200)\) 个虫洞。

作为狂热的时间旅行爱好者,FJ 希望实现:从某块田地出发,经过若干路径和虫洞后,在初始离开时间之前回到起点。这样或许他能遇见自己:)

为了判断可行性,FJ 将提供 \(F (1 \le F \le 5)\) 个农场的完整地图。所有路径通行耗时不超过 \(10,000\) 秒,虫洞最多能将 FJ 带回 \(10,000\) 秒前。

【输入】

\(1\) 行:一个整数 \(F\),表示农场数。后续为 \(F\) 个农场的数据。

每个农场:

  • \(1\) 行:三个空格分隔的整数 \(N\)(田地数), \(M\)(双向路径数), \(W\)(虫洞数)。

  • \(2 \sim M+1\) 行:每行三个空格分隔的整数 \((S, E, T)\),表示 \(S\)\(E\) 间有一条耗时 \(T\) 秒的双向路径。两块田地间可能存在多条路径。

  • \(M+2 \sim M+W+1\) 行:每行三个空格分隔的整数 \((S, E, T)\),表示一条从 \(S\)\(E\) 的单向虫洞,可将 FJ 带回 \(T\) 秒前。

【输出】

输出 \(F\) 行:对每个农场,若 FJ 能达成目标输出YES,否则输出NO

【输入样例】

2
3 3 1
1 2 2
1 3 4
2 3 1
3 1 3
3 2 1
1 2 3
2 3 4
3 1 8

【输出样例】

NO
YES

【核心思想】

  1. 问题分析:给定 \(F\) 个农场,每个农场有 \(N\) 块田地、\(M\) 条双向正权路径、\(W\) 条单向负权虫洞。判断是否存在从某点出发经过若干路径和虫洞后,在初始时间之前回到起点的路径。这等价于判断图中是否存在负权回路。这是一个SPFA 负环检测问题。

  2. 算法选择

    • SPFA(Shortest Path Faster Algorithm):用于检测图中是否存在负权回路
    • 全源初始化:将所有节点同时加入队列,检测任意位置的负环(不只从节点1出发)
    • 入队次数判负环:若某节点入队次数 \(\geq N\),则存在负环
  3. 关键步骤

    • 建图:双向路径加两条有向边(权值 \(+T\)),虫洞加一条有向边(权值 \(-T\)
    • SPFA 检测
      • 初始化所有 dist\(0\)(或无穷大),所有节点入队
      • 队列非空时取出队首,松弛所有邻接边
      • dist[j] > dist[t] + w[i],更新距离,cnt[j] = cnt[t] + 1
      • cnt[j] >= n,返回 true(存在负环)
    • 输出 YES/NO
  4. 时间/空间复杂度

    • 时间复杂度:\(O(F \cdot (N \cdot M))\),SPFA 最坏情况,实际通常很快
    • 空间复杂度:\(O(N + M + W)\),邻接表存储
  5. SPFA 负环检测的核心思想

    • 负环与时光回溯的等价性:负权回路意味着绕一圈后总时间为负,即回到过去,满足题目要求
    • 全源初始化:不指定单一起点,所有节点同时入队,确保能检测到图中任意位置的负环
    • 入队次数阈值:在 \(N\) 个节点的图中,最短路最多经过 \(N-1\) 条边。若某节点第 \(N\) 次被更新,说明存在负环(可无限松弛)
    • 虫洞建模为负边:时间回溯等价于负权边,将物理问题转化为图论经典问题
    • 适用于"判断图中是否存在负权回路"的问题,核心在于 SPFA 的入队次数判环技巧

【算法标签】

普及 #SPFA

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 505, M = 3005 * 2;  // N: 最大节点数,M: 最大边数
int n, m, W, f;  // n: 节点数,m: 正边数,W: 负边数,f: 测试用例数
int h[N], e[M], w[M], ne[M], idx;  // 邻接表存储图
int dist[N], cnt[N];  // dist: 距离,cnt: 入队次数
bool st[N];  // 标记是否在队列中

// 添加有向边
void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}

// SPFA算法检测负环
bool spfa()
{
    queue<int> q;
    // 将所有节点加入队列,从每个点开始检测负环
    for (int i = 1; i <= n; i++)
    {
        q.push(i);
        st[i] = true;
    }
    while (!q.empty())
    {
        int t = q.front();
        q.pop();
        st[t] = false;
        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])  // 如果可以松弛
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n)  // 如果入队次数超过n,存在负环
                    return true;
                if (!st[j])  // 如果不在队列中,加入队列
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    return false;  // 没有负环
}
int main()
{
    cin >> f;  // 输入测试用例数
    while (f--)
    {
        cin >> n >> m >> W;  // 输入节点数、正边数、负边数
        idx = 0;
        memset(h, -1, sizeof(h));  // 初始化邻接表
        while (m--)  // 输入正边(双向边)
        {
            int a, b, c;
            cin >> a >> b >> c;
            add(a, b, c), add(b, a, c);
        }
        while (W--)  // 输入负边(单向边)
        {
            int s, e, t;
            cin >> s >> e >> t;
            add(s, e, -t);  // 虫洞为负边
        }
        memset(dist, 0x3f, sizeof(dist));  // 初始化距离为无穷大
        memset(cnt, 0, sizeof(cnt));  // 初始化入队次数
        dist[1] = 0;  // 起始点距离为0
        if (spfa())  // 检测负环
            cout << "YES" << endl;  // 存在负环
        else
            cout << "NO" << endl;  // 不存在负环
    }
    return 0;
}

【运行结果】

2
3 3 1
1 2 2
1 3 4
2 3 1
3 1 3
NO
3 2 1
1 2 3
2 3 4
3 1 8
YES
posted @ 2026-08-10 12:28  团爸讲算法  阅读(12)  评论(0)    收藏  举报