题单 · 习题解析

洛谷 P2850 [USACO06DEC] Wormholes G

★★★ 问「从**某块**田地出发」⇒ **全图负环** ⇒ 所有点 dist = 0、全部入队 —— 而这一行正是[隔壁 P3385](/sol/p3385/) 的**错法**,同一段代码两道题各占一头;★ 「只从 1 号出发」默认档抓 46(第一层 63.7%)、图分两块抓 **293**、★ 保证连通那档是**能证的 0**;★★★ 「存一遍还是两遍」的第七种形态:**按输入的段落分**(前 M 行双向、后 W 行单向);★★ 同一份 Floyd 这道题 **0.24 秒能过**、隔壁过不了 —— `n` 从 2000 掉到 500,`n³` 差 **64 倍**;⚠ 顶格档随机撒虫洞几乎必出负环 ⇒ 秒表量的是「谁先撞上」

原题:洛谷 P2850出自 第 33 章 最短路二:Floyd、Bellman-Ford、SPFA 与负环 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P2850,日期见页头。两边不一致时信原站。

题目描述

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

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

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

输入格式

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

每个农场:

  • 第 1 行:三个空格分隔的整数 N(田地数)、M(双向路径数)、W(虫洞数)。
  • 第 2 ~ M+1 行:每行三个空格分隔的整数 (S, E, T), 表示 SE 间有一条耗时 T 秒的双向路径。两块田地间可能存在多条路径。
  • M+2 ~ M+W+1 行:每行三个空格分隔的整数 (S, E, T), 表示一条从 SE单向虫洞,可将 FJ 带回 T 秒前。

输出格式

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

说明/提示

  • 农场 1:FJ 无法实现时间回溯。
  • 农场 2:FJ 可通过环 1 → 2 → 3 → 1 回到起点 1 秒前(可从环上任意点出发实现)。

翻译:DeepSeek-R1

时限 1 秒,内存 128 MB。

输入输出样例

输入

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

农场 2 的三条路径 1-2(3)2-3(4)3-1(8) 里没有虫洞…… ⚠ 看清楚3 1 8 那一行排在 W = 1 那一段里,它是虫洞3 → 1,回拨 8 秒)。 于是 1 →(3) 2 →(4) 3 →(−8) 1 总共 −1 秒YES。 ★ 而说明/提示那句「可从环上任意点出发实现」就是这道题的题眼 —— 见第 ① 步。

1★★★ 关键的一步:这道题问的是全图,而隔壁那道问的是「从 1 出发」

★★★ 同一段代码,在 P3385 上是错的,在这道题上是正解 —— 差的只有初始化那一行

题面说「从某块田地出发」,说明/提示又补了一句「可从环上任意点出发实现」 ⇒ 问的就是「这张图里有没有负环」,和出发点无关。

问的是什么 dist 怎么初始化
P3385(同一张题单) 从 1 出发能到达的负环 dist[1] = 0,其余 INF;只把 1 号入队
P2850(这道题) 全图有没有负环 所有点 dist = 0,全部入队

「所有点 dist 都是 0」等价于加一个连向所有点、边长 0 的超级源点第 14 章 P1332 那个多源 BFS 的同款想象)。

⇒ 这正是本章自测最后一条那道思考题的考场版 (「如果题目问的是图里有没有负环,三份代码各要改哪里」)—— 答案是只改初始化那一行,而这张题单把这一行的两个取值各考了一遍。

p2850.cpp★ 这一版就能 AC(全点入队的 SPFA,顶格 0.006 秒)
// 洛谷 P2850 [USACO06DEC] Wormholes G —— ★ 这一版就能 AC
//
// ★★★ 问的是「从**某块**田地出发……在初始离开时间之前回到起点」——
// 也就是「**这张图里有没有负环**」,和出发点无关。
// ⇒ 所有点 dist 初值 0、全部入队(等价于加一个连向所有点、边长 0 的超级源点)。
//
// ⚠ 这一行正是[隔壁 P3385](/sol/p3385/) 的**错法**:那道题问的是「从 1 出发能到达的负环」。
// 同一段代码,两道题各占一头。
//
// 读边两条规则,别搞混:
// · M 条**路径**:双向,耗时 T ≥ 0 ⇒ 存两遍
// · W 个**虫洞**:单向,把时间拨回 T ⇒ 存一条 −T 的边,**只存一遍**
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int F;
cin >> F;
while (F--) {
int n, m, w;
cin >> n >> m >> w;
vector<vector<pair<int, int>>> g(n + 1);
for (int i = 0; i < m; i++) {
int s, e, t;
cin >> s >> e >> t;
g[s].push_back({e, t});
g[e].push_back({s, t}); // 双向路径
}
for (int i = 0; i < w; i++) {
int s, e, t;
cin >> s >> e >> t;
g[s].push_back({e, -t}); // 单向虫洞,只存一遍
}
vector<int> dist(n + 1, 0), cnt(n + 1, 0); // ★ 所有点初值 0
vector<char> inq(n + 1, 1);
queue<int> q;
for (int i = 1; i <= n; i++) q.push(i); // ★ 全部入队
bool neg = false;
while (!q.empty() && !neg) {
int u = q.front();
q.pop();
inq[u] = 0;
for (auto [v, ww] : g[u])
if (dist[u] + ww < dist[v]) {
dist[v] = dist[u] + ww;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) { neg = true; break; }
if (!inq[v]) { inq[v] = 1; q.push(v); }
}
}
cout << (neg ? "YES" : "NO") << "\n";
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★★★ 第一个错法:只从 1 号田地出发 —— 而这一档的 0 是能证明的

p2850One.cpp✗ 只把 1 号入队(官方样例照过 —— 那两张图都连通)
// ✗ 错法①:只从 1 号田地出发 —— ★★★ 这一页的主线
//
// 它是[隔壁 P3385](/sol/p3385/) 的**正解**:那道题问的是「从顶点 1 出发能到达的负环」。
// 而这道题问的是「从**某块**田地出发」⇒ 全图。
// ⇒ 图一旦不连通,1 号点那一块之外的负环它就看不见。
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int F;
cin >> F;
while (F--) {
int n, m, w;
cin >> n >> m >> w;
vector<vector<pair<int, int>>> g(n + 1);
for (int i = 0; i < m; i++) {
int s, e, t;
cin >> s >> e >> t;
g[s].push_back({e, t});
g[e].push_back({s, t});
}
for (int i = 0; i < w; i++) {
int s, e, t;
cin >> s >> e >> t;
g[s].push_back({e, -t});
}
vector<int> dist(n + 1, INF), cnt(n + 1, 0);
vector<char> inq(n + 1, 0);
queue<int> q;
dist[1] = 0; inq[1] = 1; q.push(1); // ✗ 只有 1 号点
bool neg = false;
while (!q.empty() && !neg) {
int u = q.front(); q.pop(); inq[u] = 0;
for (auto [v, ww] : g[u])
if (dist[u] + ww < dist[v]) {
dist[v] = dist[u] + ww;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) { neg = true; break; }
if (!inq[v]) { inq[v] = 1; q.push(v); }
}
}
cout << (neg ? "YES" : "NO") << "\n";
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
300 轮 默认档 ★★★ 图故意分成两块、负环放在不含 1 号的那块 ★ 保证连通
第一层:1 号点走不到全图的农场 476 / 747(63.7% 747 / 747 0 / 747
真被抓(种子级) 46 293 精确的 0
★ 那个 0 不是「没抓到」,是「能证明的」恒等 —— 而它同时就是这一档的自检

「保证连通」那一档先串了一条链,而 M 条路径是双向的 ⇒ 从 1 号田地按双向路径就走得到所有田地 ⇒ 「只从 1 出发」和「从所有点出发」看到的是同一张图

⇒ ★★ 这就是那条老规矩最省事的一次现场: 造一档抽掉那个条件的数据,同时给「精确的 0」做了自检、又称出了那句话的分量。P1439 立的那条。)

⚠ 而默认档只抓 46 —— 第一层有 63.7% 却只抓到 15%:还要负环恰好落在 1 号走不到的那一块。 又一次「触发条件是两层的」。

3⚠ 两个读边的错法,方向正好相反

p2850Both.cpp✗ 虫洞也存双向(样例第一个农场就打 YES)
// ✗ 错法②:虫洞也存成双向
//
// 「虫洞的特性非常特殊 —— 它是一个**单向**通道」。存成双向的话,
// `s → e → s` 绕一圈是 `−2T < 0`,**它自己就是一个负环** ⇒ 只要有虫洞就无脑 YES。
//
// ★ 和[隔壁 P3385](/sol/p3385/) 那个「一律存双向」是同一个形状的错,
// 只是那道题由**边权的正负号**决定,这道题由**它排在输入的哪一段**决定。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int F;
cin >> F;
while (F--) {
int n, m, w;
cin >> n >> m >> w;
vector<vector<pair<int, int>>> g(n + 1);
for (int i = 0; i < m; i++) {
int s, e, t;
cin >> s >> e >> t;
g[s].push_back({e, t});
g[e].push_back({s, t});
}
for (int i = 0; i < w; i++) {
int s, e, t;
cin >> s >> e >> t;
g[s].push_back({e, -t});
g[e].push_back({s, -t}); // ✗ 虫洞不是双向的
}
vector<int> dist(n + 1, 0), cnt(n + 1, 0);
vector<char> inq(n + 1, 1);
queue<int> q;
for (int i = 1; i <= n; i++) q.push(i);
bool neg = false;
while (!q.empty() && !neg) {
int u = q.front(); q.pop(); inq[u] = 0;
for (auto [v, ww] : g[u])
if (dist[u] + ww < dist[v]) {
dist[v] = dist[u] + ww;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) { neg = true; break; }
if (!inq[v]) { inq[v] = 1; q.push(v); }
}
}
cout << (neg ? "YES" : "NO") << "\n";
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

「虫洞的特性非常特殊 —— 它是一个单向通道」。存成双向的话,S → E → S 绕一圈是 −2T < 0它自己就是一个负环 ⇒ 只要有虫洞就无脑 YES。

p2850Dir.cpp✗ 双向路径只存一遍(官方样例照过)
// ✗ 错法③:双向路径只存一遍
//
// 反过来的那一半:`M` 条路径是**双向**的,只存一遍会让图凭空少一半边。
// ⚠ 它的方向是可判的:边少了 ⇒ 环也只会更少 ⇒ **答案恒 ≤ 正解**(不会把 NO 说成 YES)。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int F;
cin >> F;
while (F--) {
int n, m, w;
cin >> n >> m >> w;
vector<vector<pair<int, int>>> g(n + 1);
for (int i = 0; i < m; i++) {
int s, e, t;
cin >> s >> e >> t;
g[s].push_back({e, t}); // ✗ 少了反着那一行
}
for (int i = 0; i < w; i++) {
int s, e, t;
cin >> s >> e >> t;
g[s].push_back({e, -t});
}
vector<int> dist(n + 1, 0), cnt(n + 1, 0);
vector<char> inq(n + 1, 1);
queue<int> q;
for (int i = 1; i <= n; i++) q.push(i);
bool neg = false;
while (!q.empty() && !neg) {
int u = q.front(); q.pop(); inq[u] = 0;
for (auto [v, ww] : g[u])
if (dist[u] + ww < dist[v]) {
dist[v] = dist[u] + ww;
cnt[v] = cnt[u] + 1;
if (cnt[v] >= n) { neg = true; break; }
if (!inq[v]) { inq[v] = 1; q.push(v); }
}
}
cout << (neg ? "YES" : "NO") << "\n";
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

反过来的那一半。⚠ 它的方向是可判的:边少了 ⇒ 环只会更少 ⇒ 答案恒 ≤ 正解(只会把 YES 说成 NO,不会反过来)—— 第 26 章 P1220 那条判据的又一次。

★★★ 「存一遍还是两遍」在这张题单里演到了第七遍,而这次是「按输入的段落」分
谁说了算
B3647 / P1339 / P1462 题面一句话,全存两遍
P1629 题面一句话,全存一遍
P1073 同一张输入里,第三个数 z 说了算
P3385 同一张输入里,边权的正负号说了算
P2850(这道题) ★ 同一张输入里,它排在哪一段说了算(前 M 行两遍、后 W 行一遍)

⇒ 第 52 章那条「上一章的正确写法就是这一章的 bug」,到这里已经七种形态了。

300 轮 默认档 图分两块 保证连通 ★ 一个虫洞都不给
虫洞也存双向 216 21 102 精确的 0
双向路径只存一遍 134 1 120 0

★ 「一个虫洞都不给」那一档是「虫洞也存双向」的自检:没有虫洞,那一行什么都没改。 ⚠ 而那一档 747 个农场答案全是 NO(没有负边就没有负环)—— 它只配当自检,不能拿来比抓获率

4★ 对拍这一页:参照物是 Floyd,而这一次它不用补那半句

p2850Floyd.cpp参照物:Floyd 判负环(判据就是干净的 d[k][k] < 0)
★★ 同一份 Floyd,在 P3385 上要多写半句、在这道题上不用 —— 而「能不能用它」是一道乘三次的算术

本章第 4 步那个判据是 d[k][k] < 0 && d[s][k] < INF。 后半句是为「只算 s 能走到的」补的 —— 这道题问全图,那半句直接删掉

规模 Floyd 的三重循环 本机秒表 时限 能不能用
P3385 n ≤ 2000T ≤ 10 8 × 10¹⁰ 一组就 2.21 秒 2 秒
P2850(这道题) n ≤ 500F ≤ 5 6.25 × 10⁸ 0.24 秒 1 秒 ★ ✓(余量 4 倍)

★ 差的就是 n 从 2000 掉到 500 —— 64 倍。 ⇒ 「这道题能不能用 Floyd」永远是一道拿题面的 n 乘三次的算术题,没有通用答案。 (和本轮 B3647 那条「Floyd 慢的主语是单源」是同一件事的两面。)

300 轮(n 随机 5~8) 默认档 ★ 图分两块 ★ 保证连通 ★ 无虫洞
全点入队 SPFA ≡ Floyd 0 0 0 0
只从 1 号出发 46 293 0(能证) 0
虫洞也存双向 216 21 102 0(自检)
双向路径只存一遍 134 1 120 0
⚠ 这一档答案为 YES 的农场数 387 / 747 616 / 747 584 / 747 0 / 747

★ 顺带一道三十秒的算术:发现负环就立刻停 ⇒ dist 最低只会跌到 −N × 10⁴ = −5 000 000int 余量 429 倍不用 long long

⚠ 题单那句「多组数据,注意每组都要清干净」

这道题也是多组数据(F 个农场)。这一页的正解用的是循环体内的 vector, 天生每组重来一遍,所以没再单独写一个「忘了清空」的版本 —— 那个错法在隔壁 P3385 第 ④ 步量过(默认档抓 136 / 300),道理一模一样。

5度量程序和生成器

p2850Count.cpp度量程序(连通性那一层 + 两行算术)
p2850Gen.cpp(六个档位)数据生成器
⚠ 顶格档踩的坑:随机撒 200 个虫洞,几乎必出负环

第一版的顶格档(F = 5n = 500m = 2500w = 200 全随机)五个农场全是 YES —— 两个算法一撞上负环就退出了,秒表量的是「谁先撞上」,不是「跑满要多久」。

⇒ 加了一个「顶格且保证无负环」的档(虫洞的回拨量用势函数配平), 两个算法才真的跑满:SPFA 0.006 秒、Floyd 0.236 秒

★ 又一次「一致有两种:都算对了,和都没算」, 而这一次跑偏的不是抓获率,是耗时表

6一页纸

★★★ 关键的一步 问「从某块田地出发」⇒ 全图负环 ⇒ 所有点 dist = 0、全部入队
★★★ 和隔壁那道的关系 这一行正是 P3385错法 —— 同一段代码,两道题各占一头
★ 只从 1 号出发 默认档抓 46(第一层 63.7%)、图分两块抓 293;★ 保证连通那档是能证的 0
★★★ 读边 按输入的段落分:前 M 行双向、后 W 行单向 —— 「存一遍还是两遍」的第七种形态
★ 方向可判 「路径只存一遍」边更少 ⇒ 环更少 ⇒ 答案恒 ≤ 正解
★★ Floyd 能不能用 这道题 0.24 秒能过、隔壁 P3385 过不了 —— n 从 2000 掉到 500,64 倍
⚠ 顶格档 随机撒虫洞几乎必出负环 ⇒ 秒表量的是「谁先撞上」,得另造一个保证无负环的档