0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2853,日期见页头。两边不一致时信原站。 ⚠ 这道题的题面是英文原文(USACO 2006 Dec Silver),下面括号里是转录时补的中译。
题目描述
The cows are having a picnic! Each of Farmer John’s K (1 ≤ K ≤ 100) cows is grazing in one of
N (1 ≤ N ≤ 1,000) pastures, conveniently numbered 1…N.
The pastures are connected by M (1 ≤ M ≤ 10,000) one-way paths
(no path connects a pasture to itself).
The cows want to gather in the same pasture for their picnic, but (because of the one-way paths) some cows may only be able to get to some pastures. Help the cows out by figuring out how many pastures are reachable by all cows, and hence are possible picnic locations.
(K 头牛各自在某个牧场吃草,N 个牧场之间有 M 条单向路,没有自环。
问有多少个牧场是所有牛都能走到的 —— 那些就是可能的野餐地点。)
输入格式
- 第 1 行:三个空格分隔的整数,依次是
K、N、M; - 第 2 …
K+1行:第i+1行是一个整数(1…N),表示第i头牛所在的牧场编号; - 第
K+2…M+K+1行:每行两个空格分隔的整数A和B(都在1…N,且A ≠ B), 表示一条从牧场A到牧场B的单向路。
输出格式
一行一个整数:所有牛都能到达的牧场数目。
洛谷 P2853 页面上的「输出格式」一节,逐字重复了「输入格式」那三行(Line 1: K, N, M …),
根本没写输出什么。真正的输出格式只能从原题(USACO 官方)和样例反推:
Line 1: The number of possible picnic locations。
⇒ 这正是「题面必须在本地存一份」那条规矩想防的事的近亲: 原站的题面本身也可能是残的,而残在哪一句,只有真去做题的人知道。
说明/提示
The cows can meet in pastures 3 or 4.(牛可以在 3 号或 4 号牧场碰头。)
输入输出样例
输入
2 4 4 2 3 1 2 1 4 2 3 3 4
输出
2
K = 2, N = 4, M = 4。两头牛在 2 号和 3 号。路是 1→2、1→4、2→3、3→4。
从 2 出发能到 {2, 3, 4};从 3 出发能到 {3, 4}。交集是 {3, 4} ⇒ 答案 2。
1★ 正解:从每头牛各搜一次,给走到的牧场计数
题目问「哪些牧场所有牛都到得了」,而「牛 c 到得了哪些牧场」正是一次从 c 出发的 DFS。
for (每头牛 c) { 清空 vis;从 c 的牧场 DFS;每碰到一个牧场 cnt[v]++ }
答案 = #{ v : cnt[v] == K }
// ★★ P2853 正解:**从每头牛出发各搜一次**,给走到的牧场计数;数满 K 次的就是答案。//// 反过来想是错的(见 p2853Rev.cpp):题目问「哪些牧场**所有牛都到得了**」,// 而「牛 c 到得了哪些牧场」正是一次从 c 出发的 DFS。//// for (每头牛 c) { 清空 vis;从 c 的牧场 DFS;每碰到一个牧场 cnt[v]++ }// 答案 = #{ v : cnt[v] == K }//// ⚠ 两头牛可能在**同一个牧场**,那就从那个牧场搜两次 —— cnt 照样各加一次,`== K` 仍然对。//// 复杂度 O(K(N + M)):顶格 100 × (1000 + 10000) = **1.1 × 10⁶**。// ★★ 而同一份算法换成邻接矩阵存图就是 O(KN²) = **10⁸**(见 p2853Mat.cpp)——// 这道题是[第 29 章](/ch/29-graph-storage/)「三种存法实测比一比」最干净的场子。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int K, n, m; if (!(cin >> K >> n >> m)) return 0; // ⚠ 第一行是 K N M,别读反 vector<int> cow(K); for (int i = 0; i < K; i++) cin >> cow[i]; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; g[a].push_back(b); // ★ 单向路,只存一遍 }
vector<int> cnt(n + 1, 0), st; vector<char> vis(n + 1); for (int c : cow) { fill(vis.begin(), vis.end(), 0); // ★★ 每头牛都要从头再来 st.clear(); st.push_back(c); vis[c] = 1; while (!st.empty()) { int u = st.back(); st.pop_back(); cnt[u]++; for (int v : g[u]) if (!vis[v]) { vis[v] = 1; st.push_back(v); } } } int ans = 0; for (int i = 1; i <= n; i++) if (cnt[i] == K) ans++; printf("%d\n", ans); return 0;}点「运行 ▶」看结果
⚠ 两头牛可能在同一个牧场,那就从那个牧场搜两次 —— cnt 照样各加一次,== K 仍然对。
2★★★ 这道题是「三种存法各写一遍」最干净的场子
第 29 章的题单在这道题下面写着「适合拿三种存法各写一遍,实测比一比」。那就比:
// ⚠ P2853:同一个算法,存法换成**邻接矩阵** —— 答案一个字都不错,复杂度从 O(K(N+M)) 变成 O(KN²)。//// 矩阵版找「u 的邻居」要把整整一行 n 个格子扫一遍,不管 u 有几条出边。// ⇒ 顶格 K = 100、N = 1000:**10⁸** 次,而邻接表只要 1.1 × 10⁶ —— 差约 **90 倍**。//// ★★ 顺带回答一件[正文第 9 步](/ch/29-graph-storage/)留下的事:// `char` 矩阵**记不住重边**。可这道题只问「到不到得了」,// **它丢掉的那部分信息,这道题根本不问** ⇒ 矩阵版在这里是**完全正确**的。// ⇒ 「存法丢信息」和「这道题会不会被坑」是两句话。//// ⚠ 命令行给一个 `count` 参数,它就只打「一共看了多少个格子 / 多少条边」。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { bool countOnly = (argc > 1 && string(argv[1]) == "count"); ios::sync_with_stdio(false); cin.tie(nullptr);
int K, n, m; if (!(cin >> K >> n >> m)) return 0; vector<int> cow(K); for (int i = 0; i < K; i++) cin >> cow[i]; vector<vector<char>> a(n + 1, vector<char>(n + 1, 0)); for (int i = 0; i < m; i++) { int x, y; cin >> x >> y; a[x][y] = 1; // ★ 重边在这里被合并了(这道题不在乎) }
vector<int> cnt(n + 1, 0), st; vector<char> vis(n + 1); long long looks = 0; for (int c : cow) { fill(vis.begin(), vis.end(), 0); st.clear(); st.push_back(c); vis[c] = 1; while (!st.empty()) { int u = st.back(); st.pop_back(); cnt[u]++; for (int v = 1; v <= n; v++) { // ★ 整整一行,n 个格子 looks++; if (a[u][v] && !vis[v]) { vis[v] = 1; st.push_back(v); } } } } if (countOnly) { printf("%lld\n", looks); return 0; } int ans = 0; for (int i = 1; i <= n; i++) if (cnt[i] == K) ans++; printf("%d\n", ans); return 0;}点「运行 ▶」看结果
顶格 K = 100、N = 1000、M = 10⁴(每头牛都走遍全图的那种数据):
| 存法 | 一趟总共「看」了多少次 | 40 次端到端合计 | 内存 |
|---|---|---|---|
vector 邻接表 |
1 000 000 条边 | 0.14 秒 | 64 KB |
| 链式前向星 | ★ 一次不差,也是 1 000 000 | 0.19 秒 | 约 84 KB |
| 邻接矩阵 | ★ 100 000 000 个格子 | ★ 1.97 秒 | 0.96 MB |
★★ 次数差正好 100.0 倍(就是 n²/(n+m) 那个比值),可秒表只差约 14 倍。
原因不神秘:矩阵那一行是连续的 char,顺序扫对 cache 和向量化都极友好;
而追邻接表是一串指针跳。
⇒ 这和第 16 章 P1074(次数一样、秒表差 9.5 倍)、
第 23 章 P2925(次数一样、秒表只差 1.13 倍)凑成一组:
「换尺子数次数」和「掐秒表」量的从来不是同一件事,两个都要报。
★ 而前向星和 vector 的次数一次不差,秒表在这个规模上(毫秒量级、含进程启动) 分不出高下 —— 别把 3.5 毫秒和 4.8 毫秒的差当成结论。
⚠ 结论也别过头:这道题上三种存法都过得了(时限 1 秒,最慢的矩阵版单次约 49 毫秒)。
真正的分界在下一道题:把 N 换成 10⁵(P5318),矩阵连开都开不出来。
第 29 章第 9 步演示过:char 矩阵记不住「有几条边」,重边一进来就被合并。
可这道题只问「到不到得了」—— 它丢掉的那部分信息,这道题根本不问。 实测 300 轮,矩阵版和邻接表版 0 次不一致。
⇒ ★★ 「存法丢信息」和「这道题会不会被坑」是两句话。 (对照正文第 12 步那张表:同一个矩阵,在「带权求和」那个问题上被抓 215/300。)
3⚠ 错法一:vis 只清了一次 —— 官方样例一测就死
// ✗ P2853 错法一:`vis` 数组**只清了一次**(放在了循环外面)。//// 于是第二头牛开始,凡是上一头牛走过的牧场都被当成「已经访问过」直接跳过 ——// cnt 再也加不上去。表现是**答案偏小**(多半直接是 0)。//// ★ 恒 ≤ 正解:漏计不会凭空多出可达关系。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int K, n, m; if (!(cin >> K >> n >> m)) return 0; vector<int> cow(K); for (int i = 0; i < K; i++) cin >> cow[i]; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; g[a].push_back(b); }
vector<int> cnt(n + 1, 0), st; vector<char> vis(n + 1, 0); // ★ 清空只发生在这里 for (int c : cow) { if (vis[c]) continue; st.clear(); st.push_back(c); vis[c] = 1; while (!st.empty()) { int u = st.back(); st.pop_back(); cnt[u]++; for (int v : g[u]) if (!vis[v]) { vis[v] = 1; st.push_back(v); } } } int ans = 0; for (int i = 1; i <= n; i++) if (cnt[i] == K) ans++; printf("%d\n", ans); return 0;}点「运行 ▶」看结果
第二头牛开始,凡是上一头牛走过的牧场都被当成「已访问」跳过 ⇒ cnt 再也加不上去。
恒 ≤ 正解(对拍 300 轮 300 / 300),被抓 133 / 300,而官方样例当场打出 0。
4★★ 错法二:方向反了 —— 而官方样例蒙对了
// ✗ P2853 错法二:**方向反了** —— 建反图,从每头牛出发。//// 反图上「从牛 c 走得到 v」= 原图上「**v 走得到 c**」。// ⇒ 它算的是「**能到达所有牛**的牧场有几个」,而题目问的是「**所有牛都到得了**的牧场」。//// ★★ 说清楚它算了什么之后,一切都是白送的推论:// 在**每条边都双向**的图上两者一模一样(那时原图 = 反图),// 而这道题的路是**单向**的(题面第一句就写着 one-way paths)⇒ 它错。//// ⚠ 上一道 [P3916](/sol/p3916/) 的正解**正是**「反向建图」,这道题反过来就错 ——// [「上一道题的正确写法就是这一道题的 bug」](/sol/p1171/) 在同一张题单里又一次。
#include <bits/stdc++.h>using namespace std;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int K, n, m; if (!(cin >> K >> n >> m)) return 0; vector<int> cow(K); for (int i = 0; i < K; i++) cin >> cow[i]; vector<vector<int>> g(n + 1); for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; g[b].push_back(a); } // ★ 反着存
vector<int> cnt(n + 1, 0), st; vector<char> vis(n + 1); for (int c : cow) { fill(vis.begin(), vis.end(), 0); st.clear(); st.push_back(c); vis[c] = 1; while (!st.empty()) { int u = st.back(); st.pop_back(); cnt[u]++; for (int v : g[u]) if (!vis[v]) { vis[v] = 1; st.push_back(v); } } } int ans = 0; for (int i = 1; i <= n; i++) if (cnt[i] == K) ans++; printf("%d\n", ans); return 0;}点「运行 ▶」看结果
反图上「从牛 c 走得到 v」= 原图上「v 走得到 c」
⇒ 它算的是「能到达所有牛的牧场有几个」,而题目问的是「所有牛都到得了的牧场」。
在官方样例上,正解的集合是 {3, 4}、它的集合是 {1, 2} —— 两个集合完全不同,个数却都是 2。
⇒ ★ 只比答案的题,样例能挡住的东西比你以为的少。(对拍 300 轮它被抓 139 次。)
★★ 而「什么时候它一定对」也是白送的:把每条路都存两个方向(原图 = 反图)
⇒ 它变成精确的 0(自检:同一档里「vis 忘清」仍被抓 151 次,说明这段代码是活的)。
⇒ 按第 12 章那套三分法,题面那句 one-way paths 对这个写法是命门。
⚠⚠ 而最值得记的是:上一道 P3916 的正解就是「反向建图」 —— 同一张题单里挨着的两道题,一道非反不可,一道一反就错。 ⇒ 「上一道题的正确写法就是这一道题的 bug」,本轮第三次。
5⚠ 和算法无关但会挂人的那一条:第一行是 K N M
USACO 那批题的第一行常常是「几个查询、几个点、几条边」这种和直觉相反的顺序。
官方样例当场打出 0。
本书连着好多轮的那条规律是「官方样例挡住的都是每组都错的」。
这一页又是个反例(本轮 B3643 已经出过一次):
读成 N M K 在 300 轮小数据里只被抓 207 次 —— 因为默认档的 K 只有 14、10,
三个数常常凑巧还兼容,剩下那部分输入被当成边读掉,答案居然一样。n 只有 4
⇒ ★★ 那条规律是个经验,不是定理;每一页仍然要自己量一遍。
(而两次反例的根子是同一个:默认档太小,问不出那个问题 —— 见 B3643 第 ⑦ 步那张按 n 拧的表。)
6★ 对拍这一页
300 轮(n 随机 4K 随机 1 |
|
|---|---|
| 邻接矩阵版 | ★ 不一致 0 轮(存法不改答案) |
| 前向星版 | ★ 不一致 0 轮 |
vis 只清一次 |
133(恒 ≤ 正解,300/300) |
| 方向反了 | 139 |
第一行读成 N M K |
207(⚠ 不是 300,见第 ⑤ 步) |
| ⚠ 每条路存两遍那一档:方向反了 | ★ 精确的 0(自检:vis 忘清仍 151) |
7度量程序和生成器
8一页纸
| ★ 关键的一步 | 「所有牛都到得了」= 从每头牛各搜一次,cnt[v] == K |
| ★★★ 三种存法 | 次数 1 000 000 vs 1 000 000 vs 100 000 000(正好 100.0 倍),秒表只差 14 倍 |
| ★★ 两把尺子 | 矩阵一行是连续 char,顺序扫极快 ⇒ 次数和秒表要一起报 |
| ★ 矩阵丢重边 | 这道题只问可达性 ⇒ 丢了也不在乎(300 轮 0 次不一致) |
| 错法一 | vis 只清一次 ⇒ 恒 ≤ 正解,样例打 0(133/300) |
| ★★ 错法二 | 方向反了 ⇒ 算的是「能到达所有牛的牧场」;样例集合不同但个数相同,蒙对了 |
| ⚠ 题面陷阱 | 第一行是 K N M(读反 207/300);★ 而原站的「输出格式」一节是残的(重复了输入格式) |