题解:洛谷 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
【核心思想】
-
问题分析:给定 \(F\) 个农场,每个农场有 \(N\) 块田地、\(M\) 条双向正权路径、\(W\) 条单向负权虫洞。判断是否存在从某点出发经过若干路径和虫洞后,在初始时间之前回到起点的路径。这等价于判断图中是否存在负权回路。这是一个SPFA 负环检测问题。
-
算法选择:
- SPFA(Shortest Path Faster Algorithm):用于检测图中是否存在负权回路
- 全源初始化:将所有节点同时加入队列,检测任意位置的负环(不只从节点1出发)
- 入队次数判负环:若某节点入队次数 \(\geq N\),则存在负环
-
关键步骤:
- 建图:双向路径加两条有向边(权值 \(+T\)),虫洞加一条有向边(权值 \(-T\))
- SPFA 检测:
- 初始化所有
dist为 \(0\)(或无穷大),所有节点入队 - 队列非空时取出队首,松弛所有邻接边
- 若
dist[j] > dist[t] + w[i],更新距离,cnt[j] = cnt[t] + 1 - 若
cnt[j] >= n,返回true(存在负环)
- 初始化所有
- 输出
YES/NO
-
时间/空间复杂度:
- 时间复杂度:\(O(F \cdot (N \cdot M))\),SPFA 最坏情况,实际通常很快
- 空间复杂度:\(O(N + M + W)\),邻接表存储
-
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
浙公网安备 33010602011771号