0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2850,日期见页头。两边不一致时信原站。
题目描述
Farmer John 在探索他的农场时发现了许多神奇的虫洞。虫洞的特性非常特殊 ——
它是一个单向通道,能将你传送到它的目的地,而且时间还会回溯到过去!
FJ 的每个农场包含 N(1 ≤ N ≤ 500)块编号为 1 ~ N 的田地、
M(1 ≤ M ≤ 2500)条双向路径和 W(1 ≤ W ≤ 200)个虫洞。
作为狂热的时间旅行爱好者,FJ 希望实现:从某块田地出发,经过若干路径和虫洞后, 在初始离开时间之前回到起点。这样或许他能遇见自己 :)
为了判断可行性,FJ 将提供 F(1 ≤ F ≤ 5)个农场的完整地图。
所有路径通行耗时不超过 10000 秒,虫洞最多能将 FJ 带回 10000 秒前。
输入格式
第 1 行:一个整数 F,表示农场数。后续为 F 个农场的数据。
每个农场:
- 第 1 行:三个空格分隔的整数
N(田地数)、M(双向路径数)、W(虫洞数)。 - 第 2 ~
M+1行:每行三个空格分隔的整数(S, E, T), 表示S和E间有一条耗时T秒的双向路径。两块田地间可能存在多条路径。 - 第
M+2~M+W+1行:每行三个空格分隔的整数(S, E, T), 表示一条从S到E的单向虫洞,可将 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 出发」
题面说「从某块田地出发」,说明/提示又补了一句「可从环上任意点出发实现」 ⇒ 问的就是「这张图里有没有负环」,和出发点无关。
| 问的是什么 | dist 怎么初始化 | |
|---|---|---|
| P3385(同一张题单) | 从 1 出发能到达的负环 | dist[1] = 0,其余 INF;只把 1 号入队 |
| P2850(这道题) | 全图有没有负环 | ★ 所有点 dist = 0,全部入队 |
「所有点 dist 都是 0」等价于加一个连向所有点、边长 0 的超级源点 (第 14 章 P1332 那个多源 BFS 的同款想象)。
⇒ 这正是本章自测最后一条那道思考题的考场版 (「如果题目问的是图里有没有负环,三份代码各要改哪里」)—— 答案是只改初始化那一行,而这张题单把这一行的两个取值各考了一遍。
// 洛谷 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;}点「运行 ▶」看结果
2★★★ 第一个错法:只从 1 号田地出发 —— 而这一档的 0 是能证明的
// ✗ 错法①:只从 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;}点「运行 ▶」看结果
| 300 轮 | 默认档 | ★★★ 图故意分成两块、负环放在不含 1 号的那块 | ★ 保证连通 |
|---|---|---|---|
| 第一层:1 号点走不到全图的农场 | 476 / 747(63.7%) | ★ 747 / 747 | ★ 0 / 747 |
| 真被抓(种子级) | 46 | ★ 293 | ★ 精确的 0 |
「保证连通」那一档先串了一条链,而 M 条路径是双向的
⇒ 从 1 号田地按双向路径就走得到所有田地 ⇒ 「只从 1 出发」和「从所有点出发」看到的是同一张图。
⇒ ★★ 这就是那条老规矩最省事的一次现场: 造一档抽掉那个条件的数据,同时给「精确的 0」做了自检、又称出了那句话的分量。 (P1439 立的那条。)
⚠ 而默认档只抓 46 —— 第一层有 63.7% 却只抓到 15%:还要负环恰好落在 1 号走不到的那一块。 又一次「触发条件是两层的」。
3⚠ 两个读边的错法,方向正好相反
// ✗ 错法②:虫洞也存成双向//// 「虫洞的特性非常特殊 —— 它是一个**单向**通道」。存成双向的话,// `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;}点「运行 ▶」看结果
「虫洞的特性非常特殊 —— 它是一个单向通道」。存成双向的话,S → E → S 绕一圈是 −2T < 0,
它自己就是一个负环 ⇒ 只要有虫洞就无脑 YES。
// ✗ 错法③:双向路径只存一遍//// 反过来的那一半:`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;}点「运行 ▶」看结果
反过来的那一半。⚠ 它的方向是可判的:边少了 ⇒ 环只会更少 ⇒ 答案恒 ≤ 正解(只会把 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,而这一次它不用补那半句
本章第 4 步那个判据是 d[k][k] < 0 && d[s][k] < INF。
后半句是为「只算 s 能走到的」补的 —— 这道题问全图,那半句直接删掉。
| 规模 | Floyd 的三重循环 | 本机秒表 | 时限 | 能不能用 | |
|---|---|---|---|---|---|
| P3385 | n ≤ 2000,T ≤ 10 |
★ 8 × 10¹⁰ | 一组就 2.21 秒 | 2 秒 | ✗ |
| P2850(这道题) | n ≤ 500,F ≤ 5 |
6.25 × 10⁸ | ★ 0.24 秒 | 1 秒 | ★ ✓(余量 4 倍) |
★ 差的就是 n 从 2000 掉到 500 —— n³ 差 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 000,int 余量 429 倍 ⇒ 不用 long long。
这道题也是多组数据(F 个农场)。这一页的正解用的是循环体内的 vector,
天生每组重来一遍,所以没再单独写一个「忘了清空」的版本 ——
那个错法在隔壁 P3385 第 ④ 步量过(默认档抓 136 / 300),道理一模一样。
5度量程序和生成器
第一版的顶格档(F = 5、n = 500、m = 2500、w = 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,n³ 差 64 倍 |
| ⚠ 顶格档 | 随机撒虫洞几乎必出负环 ⇒ 秒表量的是「谁先撞上」,得另造一个保证无负环的档 |