0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1518,日期见页头。两边不一致时信原站。
题目描述
两只牛逃跑到了森林里。Farmer John 开始用他的专家技术追捕这两头牛。 你的任务是模拟他们的行为(牛和 John)。
追击在 10 × 10 的平面网格内进行。一个格子可以是:空地,一个障碍物,两头牛(它们总在一起), 或者 Farmer John。两头牛和 Farmer John 可以在同一个格子内(当他们相遇时), 但是他们都不能进入有障碍的格子。
一个格子可以是:
.空地;*障碍物;C两头牛;FFarmer John。
这里有一个地图的例子:
*...*.....
......*...
...*...*..
..........
...*.F....
*.....*...
...*......
..C......*
...*.*....
.*.*......
牛在地图里以固定的方式游荡。每分钟,它们可以向前移动或是转弯。 如果前方无障碍(地图边沿也是障碍),它们会按照原来的方向前进一步。 否则它们会用这一分钟顺时针转 90 度。同时,它们不会离开地图。
Farmer John 深知牛的移动方法,他也这么移动。
每次(每分钟)Farmer John 和两头牛的移动是同时的。 如果他们在移动的时候穿过对方,但是没有在同一格相遇,我们不认为他们相遇了。 当他们在某分钟末在某格子相遇,那么追捕结束。
读入十行表示地图。每行都只包含 10 个字符,表示的含义和上面所说的相同。
保证地图中只有一个 F 和一个 C。F 和 C 一开始不会处于同一个格子中。
计算 Farmer John 需要多少分钟来抓住他的牛,假设牛和 Farmer John 一开始的行动方向都是正北(即上)。 如果 John 和牛永远不会相遇,输出 0。
输入格式
输入共十行,每行 10 个字符,表示如上文描述的地图。
输出格式
输出一个数字,表示 John 需要多少时间才能抓住牛们。如果 John 无法抓住牛,则输出 0。
数据规模与约定
USACO 2.4,翻译来自 NOCOW。地图固定是 10 × 10。
输入输出样例
输入
*...*..... ......*... ...*...*.. .......... ...*.F.... *.....*... ...*...... ..C......* ...*.*.... .*.*......
输出
49
就是题面里那张示例地图,49 分钟后两人落在同一格。
1第一反应:照着题面模拟,跑够多次就当抓不到
这道题没有算法。规则题面写得清清楚楚,照抄就是:
每分钟:
面前那格能走 -> 往前一步
不能走 -> 用这一分钟顺时针转 90 度
牛和农夫同时走完,再看是不是同一格
难的只有最后一句话:「永远不会相遇,输出 0」 —— 程序不可能真的跑到永远,所以第一版几乎都长这样:循环个十万次,还没抓到就输出 0。
// P1518 的**第一版**:老老实实模拟,**循环十万次**没抓到就当抓不到。//// 题目:10 × 10 的地图,`.` 空地、`*` 障碍、`C` 两头牛、`F` 农夫。// 每分钟:面前那格能走就往前一步,否则**用这一分钟顺时针转 90 度**(地图外也算障碍)。// 牛和农夫**同时**动,一开始都朝北。某分钟末落在同一格就算抓到,输出用了几分钟;// 永远抓不到输出 0。//// ⚠ 这一版**也能 AC** —— 这一页不打算假装它是错的。// 它的问题只有一句话:**「十万」是拍出来的,你说不清为什么够。**//// 页面第 ③ ④ 步把这句话量成了数字:// · 20000 张随机地图上,答案最大只有 **205**,判定「抓不到」最多只要 **572** 步;// · 可**专门去找**,答案能到 **506**、判定「抓不到」要 **839** 步。// ⇒ **实测最大不是安全上限。** 拍 1000 也许够,拍 300 就错 —— 而你手上没有判据。//// ★ 而这道题**有**一个不用拍的判据(见 p1518.cpp):状态有限 + 过程确定 ⇒ 状态一重复就停。
#include <bits/stdc++.h>using namespace std;
static const int DR[4] = {-1, 0, 1, 0}; // 北 东 南 西 —— 顺时针static const int DC[4] = {0, 1, 0, -1};
static char g[10][11];
/** 走一分钟:能往前就往前,否则原地顺时针转 90 度。 */static void step(int& r, int& c, int& d) { int nr = r + DR[d], nc = c + DC[d]; if (nr < 0 || nr >= 10 || nc < 0 || nc >= 10 || g[nr][nc] == '*') { d = (d + 1) % 4; return; } r = nr; c = nc;}
int main(int argc, char** argv) { int cr = 0, cc = 0, fr = 0, fc = 0; for (int i = 0; i < 10; i++) { if (!(cin >> g[i])) return 0; for (int j = 0; j < 10; j++) { if (g[i][j] == 'C') { cr = i; cc = j; } if (g[i][j] == 'F') { fr = i; fc = j; } } }
const long long LIMIT = 100000; // ★ 拍出来的:为什么是十万?说不清 int cd = 0, fd = 0; long long t = 0, steps = 0; while (steps < LIMIT) { step(cr, cc, cd); step(fr, fc, fd); steps++; if (cr == fr && cc == fc) { t = steps; break; } } cout << t << "\n"; if (argc > 1 && string(argv[1]) == "count") cerr << "steps=" << steps << "\n"; return 0;}点「运行 ▶」看结果
这一页不打算假装它是错的 —— 它交上去就是过的,而且快得没影。
它唯一的问题是一句话:「十万」是拍出来的,你说不清为什么够。 第 ④ ⑤ 步会把这句话变成数字,而结论比想象中难看。
2★ 题面里有三句话,漏一句就错 —— 而这次官方样例全挡住了
① 「用这一分钟顺时针转 90 度」——转向本身就是这一分钟做的事。
读成「转到能走为止,再走一步」更顺口,也更像常识。可那样每分钟都保证走一步, 同样的分钟数里走得更远:
// P1518 的**错法二**:把「转 90 度**用掉这一分钟**」读成了「转到能走为止,再走一步」。//// 题目:10 × 10 的地图,`.` 空地、`*` 障碍、`C` 两头牛、`F` 农夫。// 每分钟:面前那格能走就往前一步,否则**用这一分钟顺时针转 90 度**(地图外也算障碍)。// 牛和农夫**同时**动,一开始都朝北。某分钟末落在同一格就算抓到,输出用了几分钟;// 永远抓不到输出 0。//// 题面原话:「如果前方无障碍……它们会按照原来的方向前进一步。**否则它们会用这一分钟// 顺时针转 90 度**。」—— 转向**本身就是这一分钟做的事**,转完这一分钟就没了。//// ⚠ 而「转到能走再走一步」读起来更顺,也更像常识。这一版就是那么写的:// 它每一分钟都保证走一步,于是**同样的分钟数里走得更远**,答案偏小或者变成 0。//// ★ 页面第 ⑤ 步:它在官方样例上就挂了(给的不是 49)—— 这是本页三个错法里最好抓的一个。
#include <bits/stdc++.h>using namespace std;
static const int DR[4] = {-1, 0, 1, 0}; // 北 东 南 西 —— 顺时针static const int DC[4] = {0, 1, 0, -1};
static char g[10][11];
/** 走一分钟:能往前就往前,否则原地顺时针转 90 度。 */static void step(int& r, int& c, int& d) { for (int t = 0; t < 4; t++) { // ★ 错在这里:转向不占用这一分钟 int nr = r + DR[d], nc = c + DC[d]; if (nr >= 0 && nr < 10 && nc >= 0 && nc < 10 && g[nr][nc] != '*') { r = nr; c = nc; return; } d = (d + 1) % 4; }}
int main(int argc, char** argv) { int cr = 0, cc = 0, fr = 0, fc = 0; for (int i = 0; i < 10; i++) { if (!(cin >> g[i])) return 0; for (int j = 0; j < 10; j++) { if (g[i][j] == 'C') { cr = i; cc = j; } if (g[i][j] == 'F') { fr = i; fc = j; } } }
static bool vis[100][4][100][4]; // 160000 个 bool,1.6 KB 都不到 int cd = 0, fd = 0; // 都朝北 long long t = 0, steps = 0; while (true) { int s1 = cr * 10 + cc, s2 = fr * 10 + fc; if (vis[s1][cd][s2][fd]) { t = 0; break; } // ★ 状态重复 ⇒ 从此循环,永远抓不到 vis[s1][cd][s2][fd] = true; step(cr, cc, cd); step(fr, fc, fd); steps++; if (cr == fr && cc == fc) { t = steps; break; } } cout << t << "\n"; if (argc > 1 && string(argv[1]) == "count") cerr << "steps=" << steps << "\n"; return 0;}点「运行 ▶」看结果
② 「移动是同时的;穿过对方不算相遇」。
顺手写的模拟往往是「先动牛、看看抓到没有、再动农夫」—— 那就把擦肩而过也算成了抓到:
// P1518 的**错法三**:牛和农夫**不是同时**动 —— 先动牛、立刻比一次,再动农夫、又比一次。//// 题目:10 × 10 的地图,`.` 空地、`*` 障碍、`C` 两头牛、`F` 农夫。// 每分钟:面前那格能走就往前一步,否则**用这一分钟顺时针转 90 度**(地图外也算障碍)。// 牛和农夫**同时**动,一开始都朝北。某分钟末落在同一格就算抓到,输出用了几分钟;// 永远抓不到输出 0。//// 题面原话:「每次(每分钟)Farmer John 和两头牛的移动是**同时**的。// 如果他们在移动的时候**穿过对方**,但是没有在同一格相遇,我们**不认为**他们相遇了。」//// ⚠ 这句话是题面里唯一一句「反常识」的规定,而顺手写的模拟正好会违反它:// 一根一根地动、动完就比,等于把「擦肩而过」也算成了抓到。//// ★ 我原来以为这个要「两人正好互换格子」才触发,所以会很难抓 —— **实测不是**:// 它在**官方样例上就挂了**(给 42,正确答案 49)。触发条件比想象宽得多:// 只要某一分钟「牛走进了农夫**还没离开**的那一格」就够,不必真的互换。// ⇒ 又一次「我知道这个 bug 靠什么现形,其实只知道了一半」。
#include <bits/stdc++.h>using namespace std;
static const int DR[4] = {-1, 0, 1, 0}; // 北 东 南 西 —— 顺时针static const int DC[4] = {0, 1, 0, -1};
static char g[10][11];
/** 走一分钟:能往前就往前,否则原地顺时针转 90 度。 */static void step(int& r, int& c, int& d) { int nr = r + DR[d], nc = c + DC[d]; if (nr < 0 || nr >= 10 || nc < 0 || nc >= 10 || g[nr][nc] == '*') { d = (d + 1) % 4; return; } r = nr; c = nc;}
int main(int argc, char** argv) { int cr = 0, cc = 0, fr = 0, fc = 0; for (int i = 0; i < 10; i++) { if (!(cin >> g[i])) return 0; for (int j = 0; j < 10; j++) { if (g[i][j] == 'C') { cr = i; cc = j; } if (g[i][j] == 'F') { fr = i; fc = j; } } }
static bool vis[100][4][100][4]; // 160000 个 bool,1.6 KB 都不到 int cd = 0, fd = 0; // 都朝北 long long t = 0, steps = 0; while (true) { int s1 = cr * 10 + cc, s2 = fr * 10 + fc; if (vis[s1][cd][s2][fd]) { t = 0; break; } // ★ 状态重复 ⇒ 从此循环,永远抓不到 vis[s1][cd][s2][fd] = true; step(cr, cc, cd); steps++; if (cr == fr && cc == fc) { t = steps; break; } // ★ 错在这里:农夫还没动就比了 step(fr, fc, fd); if (cr == fr && cc == fc) { t = steps; break; } } cout << t << "\n"; if (argc > 1 && string(argv[1]) == "count") cerr << "steps=" << steps << "\n"; return 0;}点「运行 ▶」看结果
③ 「地图边沿也是障碍」。 这句在括号里,最容易看丢;丢了就会走出地图,多半直接崩。
| 正确答案 | 输出 | |
|---|---|---|
| 转向不占时间 | 49 | 50 |
| 顺序移动(穿过也算相遇) | 49 | 42 |
⇒ 这和同一轮的 P1074 正好相反 —— 那道题的官方样例 ① 放过了一个错法。 样例挡不挡得住,是一件要一个一个试出来的事,不能假设。
★ 顺带记一条被实测打回来的猜测:我原以为「顺序移动」要两人正好互换格子才触发、 会很难抓。实测不是 —— 只要某一分钟「牛走进了农夫还没离开的那一格」就够, 官方样例第 42 分钟就发生了。⇒ 又一次「我知道这个 bug 靠什么现形,其实只知道了一半」。
3★★★ 「永远抓不到」凭什么下结论 —— 这才是这道题真正在问的
把「跑十万次」换成一句能证明的话,需要的只有两个观察:
① 整个过程是**完全确定**的 —— 这一分钟的局面,唯一决定下一分钟的局面。
② 局面的种数是**有限**的:
牛的格子 100 × 牛的朝向 4 × 农夫的格子 100 × 农夫的朝向 4 = 160000
⇒ 一旦某个局面第二次出现,后面就是一模一样的循环,再走一亿分钟也不会有新事发生。 见到重复局面就可以立刻输出 0 —— 这个「立刻」是证明,不是猜。
// P1518 两只塔姆沃斯牛 —— 模拟 + ★ 用「状态重复」判定永不相遇(★ 这一版就能 AC)//// 题目:10 × 10 的地图,`.` 空地、`*` 障碍、`C` 两头牛、`F` 农夫。// 每分钟:面前那格能走就往前一步,否则**用这一分钟顺时针转 90 度**(地图外也算障碍)。// 牛和农夫**同时**动,一开始都朝北。某分钟末落在同一格就算抓到,输出用了几分钟;// 永远抓不到输出 0。//// 模拟本身没有任何难点 —— 这道题真正要回答的是**另一个问题**://// ⚠⚠ **「永远抓不到」这件事,你凭什么敢下结论?**//// ★★★ 关键的一步:**整个过程是完全确定的,而状态是有限的。**// 一个状态 =(牛的格子、牛的朝向、农夫的格子、农夫的朝向)// = 100 × 4 × 100 × 4 = **160000** 种。// 下一分钟的状态只由这一分钟的状态决定 ⇒ 一旦某个状态第二次出现,// 后面就是一模一样的循环,**再走一亿分钟也不会有新事发生**。// ⇒ 见到重复状态就可以立刻输出 0,而且这个「立刻」是**证明**,不是猜。//// ⚠ 对照:大多数人的第一版是「循环个十万次,没抓到就当抓不到」(p1518Guess.cpp)。// 那一版**也能 AC** —— 可它说不清「十万」为什么够。页面第 ③ ④ 步量了这件事。
#include <bits/stdc++.h>using namespace std;
static const int DR[4] = {-1, 0, 1, 0}; // 北 东 南 西 —— 顺时针static const int DC[4] = {0, 1, 0, -1};
static char g[10][11];
/** 走一分钟:能往前就往前,否则原地顺时针转 90 度。 */static void step(int& r, int& c, int& d) { int nr = r + DR[d], nc = c + DC[d]; if (nr < 0 || nr >= 10 || nc < 0 || nc >= 10 || g[nr][nc] == '*') { d = (d + 1) % 4; return; } r = nr; c = nc;}
int main(int argc, char** argv) { int cr = 0, cc = 0, fr = 0, fc = 0; for (int i = 0; i < 10; i++) { if (!(cin >> g[i])) return 0; for (int j = 0; j < 10; j++) { if (g[i][j] == 'C') { cr = i; cc = j; } if (g[i][j] == 'F') { fr = i; fc = j; } } }
static bool vis[100][4][100][4]; // 160000 字节 = 156 KB,随便开 int cd = 0, fd = 0; // 都朝北 long long t = 0, steps = 0; while (true) { int s1 = cr * 10 + cc, s2 = fr * 10 + fc; if (vis[s1][cd][s2][fd]) { t = 0; break; } // ★ 状态重复 ⇒ 从此循环,永远抓不到 vis[s1][cd][s2][fd] = true; step(cr, cc, cd); step(fr, fc, fd); steps++; if (cr == fr && cc == fc) { t = steps; break; } } cout << t << "\n"; if (argc > 1 && string(argv[1]) == "count") cerr << "steps=" << steps << "\n"; return 0;}点「运行 ▶」看结果
bool vis[100][4][100][4] 是 160000 字节 = 156 KB,题目给 128 MB,随便开。
换来的是:程序里不再有任何一个「拍出来的数」。
★ 而且它更快:随机地图上它平均只走 46.74 分钟就能给出答案(两万张里最多 843), 而「循环十万次」那版在抓不到的时候每次都要走满十万步。 只不过十万步也才几毫秒 —— 所以这一版的价值不在快,在说得清。
4★★★ 那「十万」到底够不够?随机最大 220,专门去找能到 635
先量随机地图。每档 20000 张,障碍密度从 5% 到 40%:
| 障碍密度 | 永远抓不到的图 | 抓得到时的中位数 | 最大答案 | 判定「抓不到」最多走 |
|---|---|---|---|---|
| 5% | 14638(73.2%) | 10 | 198 | 482 |
| 10% | 14030(70.2%) | 15 | 220 | 624 |
| 20% | 14580(72.9%) | 15 | 152 | 627 |
| 30% | 15831(79.2%) | 12 | 204 | 621 |
| 40% | 16909(84.5%) | 9 | 87 | 424 |
看完这张表,很容易得出「上限拍 1000 绰绰有余」。而这个结论是错的。
拿一个爬山搜索(随机地图起步,每次翻转一个格子,只保留不变差的)去专门找难例:
| 随机 20000 张里最大 | 爬山,目标 = 最大答案 | 爬山,目标 = 最长判环 | |
|---|---|---|---|
| 最大答案(抓到用了几分钟) | 220 | 635 | 56 |
| 判定「抓不到」要走几步 | 843 | 1060 | 839 |
三件事,一件比一件难听:
- 随机永远见不到 635 这种图 —— 两万张里的天花板是 220,差了 2.9 倍。
- ⚠ 「搜到的」不等于「最大的」:头一轮爬山(12 次重启)只搜到 506, 把重启次数加到 40 就变成了 635。搜得更久还能更大 —— 这个数只是一个下界。
- ⚠⚠ 直接对着一个指标搜,反而输给了另一个指标的副产品: 专门为「判环最长」搜出来的那张图只有 839,连随机的 843 都没赢过; 而为「最大答案」搜出来的那张图顺带就有 1060。爬山会卡在局部最优。
⇒ 三条说的是同一件事:「我跑了两万组,最大才 220」推不出「300 够用」, 而「我搜过了,最大就是 635」同样推不出「1000 够用」。 唯一能给出上限的是状态数:160000。而第 ③ 步那一版连这个数都不需要。
5★★★ 上限拍成 300 是错的 —— 而随机对拍一次都抓不到
把第一版的 100000 换成别的数,在同样 20000 张随机地图上和正解比:
| 上限 | 100 | 200 | 300 | 500 | 1000 | 2000 |
|---|---|---|---|---|---|---|
| 错的组数 | 4 | 0 | 0 | 0 | 0 | 0 |
// P1518 的**错法一**:和 p1518Guess.cpp 只差一个数字 —— 上限从 100000 拍成了 **300**。//// 题目:10 × 10 的地图,`.` 空地、`*` 障碍、`C` 两头牛、`F` 农夫。// 每分钟:面前那格能走就往前一步,否则**用这一分钟顺时针转 90 度**(地图外也算障碍)。// 牛和农夫**同时**动,一开始都朝北。某分钟末落在同一格就算抓到,输出用了几分钟;// 永远抓不到输出 0。//// ★★★ 它存在的意义,是把「拍一个上限」这件事的风险变成一个可以量的东西:// 300 这个数**看起来非常宽裕** —— 20000 张随机地图上答案的中位数只有十几步。// 可它是错的,而且页面第 ④ 步会告诉你,随机对拍要多少轮才碰得到一次。
#include <bits/stdc++.h>using namespace std;
static const int DR[4] = {-1, 0, 1, 0}; // 北 东 南 西 —— 顺时针static const int DC[4] = {0, 1, 0, -1};
static char g[10][11];
/** 走一分钟:能往前就往前,否则原地顺时针转 90 度。 */static void step(int& r, int& c, int& d) { int nr = r + DR[d], nc = c + DC[d]; if (nr < 0 || nr >= 10 || nc < 0 || nc >= 10 || g[nr][nc] == '*') { d = (d + 1) % 4; return; } r = nr; c = nc;}
int main(int argc, char** argv) { int cr = 0, cc = 0, fr = 0, fc = 0; for (int i = 0; i < 10; i++) { if (!(cin >> g[i])) return 0; for (int j = 0; j < 10; j++) { if (g[i][j] == 'C') { cr = i; cc = j; } if (g[i][j] == 'F') { fr = i; fc = j; } } }
const long long LIMIT = 300; // ★ 错在这里:拍小了 int cd = 0, fd = 0; long long t = 0, steps = 0; while (steps < LIMIT) { step(cr, cc, cd); step(fr, fc, fd); steps++; if (cr == fr && cc == fc) { t = steps; break; } } cout << t << "\n"; if (argc > 1 && string(argv[1]) == "count") cerr << "steps=" << steps << "\n"; return 0;}点「运行 ▶」看结果
这是本书见过的「对拍抓不到」里最干净的一种:
- 不是概率低 —— 随机地图的答案最大就是 220,结构上到不了 300;
- 不是题面挡死 —— 那张答案 635 的图是完全合法的输入,只是随机撒不出来。
★ 而且被那张图打掉的不止 300:上限 500 也错,看起来很宽裕的数字一样不安全。
⇒ 所以它属于第 12 章 P1226 分出来的「生成器缺一档」, 但这一次那一档不是「配个边界值」就能补上的: 要造出反例,得拿一个搜索去找(第 ④ 步那个爬山),生成器本身写不出来。
★ 这一条值得单独记住:当 bug 的触发条件是「某个量特别大」,而那个量在随机数据上 有一个远低于阈值的天花板时,加多少轮对拍都没有用 —— 该做的是去搜一个反例。
三个错法各跑 300 轮对拍(默认档生成器):
| 错法 | 抓到 | 其中「两版一起输出 0」的轮数 |
|---|---|---|
| 上限拍成 300 | 0 | 237 |
| 转向不占时间 | 66 | 233 |
| 顺序移动 | 40 | 209 |
随机地图里 70% ~ 85% 是「永远抓不到」的,那些轮里正解和错法一起输出 0 —— 对拍记「通过」,可它什么都没验。 ⇒ 第 14 章 P1746 那条「『一致』有两种:都算对了,和都没算」在这道题上 不是例外,是常态。真正有效的轮数只有六七十轮。
6度量程序:这一页的每个数字都出自它
上面四张表 —— 随机地图的分布、爬山搜出来的两张最坏图、上限那一行、三个错法的抓获率 ——
都是这一份跑出来的,并且逐条写进了 scripts/check-viz.mjs 的断言。
两张最坏图存成了常量(搜索是离线做的),度量程序每次都把它们重新验一遍。
// P1518 的度量程序 —— 这一页所有数字都出自这一份。//// `./p1518Count` 人看的版本// `./p1518Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用//// 六段:// ① 官方样例:五个版本各输出什么(★ 两个错法在这一组就挂了);// ② ★★★ 随机地图上的分布:**七成以上的图是「永远抓不到」的**,// 答案的中位数只有十几步、最大 205,判定「抓不到」最多 572 步;// ③ ★★★ 可**专门去找**:爬山搜出来的两张图,答案 506 / 判环 839 步 ——// ⇒ **实测最大不是安全上限**;// ④ ★ 「上限拍多少才够」:把 100000 换成 100 / 200 / 300 / 500 / 1000,各错多少组;// ⑤ 三个错法的抓获率 —— ⚠ 并且**剔掉「两版一起输出 0」的轮数**,那些轮验的是零;// ⑥ 判重版实际走了多少步:平均和最大。//// ⚠ 这里的 vis 用「时间戳」而不是 memset:160000 个格子 memset 十万遍要 16 GB 的写入。
#include <bits/stdc++.h>using namespace std;
static bool CSV = false;static void row(const char* key, const vector<long long>& v) { if (!CSV) return; printf("%s", key); for (long long x : v) printf(",%lld", x); printf("\n");}
static const int DR[4] = {-1, 0, 1, 0};static const int DC[4] = {0, 1, 0, -1};static char g[10][11];
/* 四种走法:0 正解/Guess(转向占一分钟)、1 Turn(转向不占时间) */static void stepOk(int& r, int& c, int& d) { int nr = r + DR[d], nc = c + DC[d]; if (nr < 0 || nr >= 10 || nc < 0 || nc >= 10 || g[nr][nc] == '*') { d = (d + 1) % 4; return; } r = nr; c = nc;}static void stepTurn(int& r, int& c, int& d) { for (int t = 0; t < 4; t++) { int nr = r + DR[d], nc = c + DC[d]; if (nr >= 0 && nr < 10 && nc >= 0 && nc < 10 && g[nr][nc] != '*') { r = nr; c = nc; return; } d = (d + 1) % 4; }}
static int vis[100][4][100][4];static int stamp_ = 0;
enum Kind { OK, GUESS, TURN, CROSS };
/** 返回答案;steps 里放实际走的分钟数。limit 只对 GUESS 那一类有意义。 */static long long solve(int cr, int cc, int fr, int fc, Kind k, long long limit, long long& steps) { void (*mv)(int&, int&, int&) = (k == TURN) ? stepTurn : stepOk; int cd = 0, fd = 0; steps = 0; if (k == OK) { stamp_++; while (true) { int a = cr * 10 + cc, b = fr * 10 + fc; if (vis[a][cd][b][fd] == stamp_) return 0; vis[a][cd][b][fd] = stamp_; mv(cr, cc, cd); mv(fr, fc, fd); steps++; if (cr == fr && cc == fc) return steps; } } while (steps < limit) { mv(cr, cc, cd); steps++; if (k == CROSS && cr == fr && cc == fc) return steps; // ★ 顺序移动那个错法 mv(fr, fc, fd); if (cr == fr && cc == fc) return steps; } return 0;}
/** 按 seed 和密度造一张图(和 p1518Gen.cpp 逐字一致),起点写进 cr/cc/fr/fc。 */static void gen(int seed, int forcePct, int& cr, int& cc, int& fr, int& fc) { mt19937 rng((unsigned)seed * 2654435761u + 11u); int pct = forcePct > 0 ? forcePct : (int)(rng() % 36 + 5); if (forcePct > 0) rng(); // ⚠ 和生成器对齐:不取密度时那一次调用也要走掉 for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) g[i][j] = ((int)(rng() % 100) < pct) ? '*' : '.'; g[i][10] = 0; } do { cr = rng() % 10; cc = rng() % 10; } while (g[cr][cc] == '*'); do { fr = rng() % 10; fc = rng() % 10; } while (g[fr][fc] == '*' || (fr == cr && fc == cc));}
/* ★ 爬山搜出来的两张图(搜法见页面第 ④ 步;这里只把结果存下来复验)。 ⚠ 它们是「搜到的」,不是「最大的」—— 头一轮爬山(12 次重启)搜到答案 506, 把重启次数加到 40 就变成了 635。**搜得更久还能更大,所以这个数只是下界。** */static const char* WORST_ANS[10] = { "**.***.***", "*...**.*.*", ".*.*.*....", "**.*.....*", "*.*.....*.", "*.....*..*", "...**.****", "....*.*.**", "**.*......", ".*.**.*...",};static const char* WORST_LOOP[10] = { ".*.***....", "*.....*.*.", "........*.", ".*....*.*.", "....***.*.", ".....*..*.", "*...**....", "*...***.**", ".**.**..*.", "*.*.*.*.**",};
/** 一张固定地图上,枚举所有 (牛, 农夫) 起点,返回最大答案和最大步数。 */static pair<long long, long long> scanAll() { long long ba = 0, bs = 0; for (int a = 0; a < 100; a++) { if (g[a / 10][a % 10] == '*') continue; for (int b = 0; b < 100; b++) { if (b == a || g[b / 10][b % 10] == '*') continue; long long st; long long r = solve(a / 10, a % 10, b / 10, b % 10, OK, 0, st); ba = max(ba, r); bs = max(bs, st); } } return {ba, bs};}
static const char* SAMPLE[10] = { "*...*.....", "......*...", "...*...*..", "..........", "...*.F....", "*.....*...", "...*......", "..C......*", "...*.*....", ".*.*......",};
int main(int argc, char** argv) { CSV = (argc > 1 && string(argv[1]) == "csv");
/* ① 官方样例 */ { int cr = 0, cc = 0, fr = 0, fc = 0; for (int i = 0; i < 10; i++) { memcpy(g[i], SAMPLE[i], 11); for (int j = 0; j < 10; j++) { if (g[i][j] == 'C') { cr = i; cc = j; } if (g[i][j] == 'F') { fr = i; fc = j; } } } long long st; long long a = solve(cr, cc, fr, fc, OK, 0, st); long long b = solve(cr, cc, fr, fc, GUESS, 100000, st); long long c = solve(cr, cc, fr, fc, GUESS, 300, st); long long d = solve(cr, cc, fr, fc, TURN, 100000, st); long long e = solve(cr, cc, fr, fc, CROSS, 100000, st); if (!CSV) printf("① 官方样例:正解 %lld|十万上限 %lld|上限 300 %lld|转向不占时间 %lld|顺序移动 %lld\n", a, b, c, d, e); row("sample", {a, b, c, d, e}); }
/* ② 随机地图上的分布 */ const int R = 20000; { if (!CSV) printf("\n② 随机地图 %d 组(按障碍密度分档)\n", R); vector<long long> out; for (int pct : {5, 10, 20, 30, 40}) { int zero = 0; long long mxA = 0, mxS = 0; vector<long long> ans; for (int s = 1; s <= R; s++) { int cr, cc, fr, fc, st2; (void)st2; gen(s, pct, cr, cc, fr, fc); long long st; long long a = solve(cr, cc, fr, fc, OK, 0, st); if (a == 0) zero++; else ans.push_back(a); mxA = max(mxA, a); mxS = max(mxS, st); } sort(ans.begin(), ans.end()); long long med = ans.empty() ? 0 : ans[ans.size() / 2]; out.push_back(zero); out.push_back(med); out.push_back(mxA); out.push_back(mxS); if (!CSV) printf(" 密度 %2d%%:永远抓不到 %d 组(%.1f%%)|抓得到的中位数 %lld 步、最大 %lld" "|判定「抓不到」最多走 %lld 步\n", pct, zero, zero * 100.0 / R, med, mxA, mxS); } row("dist", out); }
/* ③ ★★★ 专门去找:爬山搜出来的两张图 */ { for (int i = 0; i < 10; i++) memcpy(g[i], WORST_ANS[i], 11); auto p = scanAll(); for (int i = 0; i < 10; i++) memcpy(g[i], WORST_LOOP[i], 11); auto q = scanAll(); if (!CSV) printf("\n③ 爬山搜出来的两张图:最大答案 %lld(那张图上判环也要 %lld 步)" "|最长判环 %lld 步(那张图上最大答案 %lld)\n", p.first, p.second, q.second, q.first); row("worst", {p.first, p.second, q.first, q.second}); }
/* ④ 上限拍多少才够 */ { if (!CSV) printf("\n④ 把上限从 100000 换成别的:%d 组随机地图里错多少组\n", R); vector<long long> out; for (long long lim : {100, 200, 300, 500, 1000, 2000}) { int bad = 0; for (int s = 1; s <= R; s++) { int cr, cc, fr, fc; gen(s, 0, cr, cc, fr, fc); long long st; long long ref = solve(cr, cc, fr, fc, OK, 0, st); long long got = solve(cr, cc, fr, fc, GUESS, lim, st); if (ref != got) bad++; } out.push_back(bad); if (!CSV) printf(" 上限 %5lld:错 %d 组\n", lim, bad); } row("limit", out); }
/* ⑤ 三个错法的抓获率 —— ⚠ 「两版一起输出 0」的轮要单独数出来 */ { if (!CSV) printf("\n⑤ 三个错法各被抓多少(每档 300 轮)\n"); vector<long long> out; const char* nm[3] = {"上限拍成 300", "转向不占时间", "顺序移动"}; const Kind KS[3] = {GUESS, TURN, CROSS}; const long long LS[3] = {300, 100000, 100000}; for (int k = 0; k < 3; k++) { int bad = 0, bothZero = 0; for (int s = 1; s <= 300; s++) { int cr, cc, fr, fc; gen(s, 0, cr, cc, fr, fc); long long st; long long ref = solve(cr, cc, fr, fc, OK, 0, st); long long got = solve(cr, cc, fr, fc, KS[k], LS[k], st); if (ref == 0 && got == 0) bothZero++; if (ref != got) bad++; } out.push_back(bad); out.push_back(bothZero); if (!CSV) printf(" %s:抓到 %d / 300(其中 %d 轮两版一起输出 0,那些轮验的是零)\n", nm[k], bad, bothZero); } row("catch", out); }
/* ⑥ 判重版实际走了多少步 */ { long long tot = 0, mx = 0; for (int s = 1; s <= R; s++) { int cr, cc, fr, fc; gen(s, 0, cr, cc, fr, fc); long long st; solve(cr, cc, fr, fc, OK, 0, st); tot += st; mx = max(mx, st); } if (!CSV) printf("\n⑥ 判重版实际走的分钟数:%d 组平均 %.2f 步、最大 %lld 步" "(而「循环十万次」那版在抓不到时**每次都走满十万步**)\n", R, tot * 1.0 / R, mx); row("steps", {tot * 100 / R, mx}); } return 0;}点「运行 ▶」看结果
// P1518 对拍生成器:`./p1518Gen <seed> [密度]`// 密度 = 障碍格的百分比(默认 seed 决定,5 ~ 40)//// 造法:10 × 10 的格子按密度随机撒 `*`,再在空地里随机放 `C` 和 `F`(不同格)。// ⇒ 造出来的一定是合法输入(题面保证只有一个 C、一个 F,且不在同一格)。//// ★ 这道题的生成器没有「造得对不对」的问题 —— 任何 10 × 10 的图都是合法输入。// ⚠ 但它有另一个问题,而且是这一页的主线之一:// **随机地图上,70% ~ 85% 的图是「永远抓不到」的**(页面第 ③ 步),// 而那些图上正解和一堆错法都输出 0,**对拍在那些轮里验的是零**。// ⇒ [第 14 章 P1746](/sol/p1746/) 那条「『一致』有两种」在这道题上是**常态**,不是例外。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1; mt19937 rng(seed * 2654435761u + 11u); int pct = argc > 2 ? atoi(argv[2]) : (int)(rng() % 36 + 5); pct = max(0, min(90, pct));
char g[10][11]; for (int i = 0; i < 10; i++) { for (int j = 0; j < 10; j++) g[i][j] = ((int)(rng() % 100) < pct) ? '*' : '.'; g[i][10] = 0; } int cr, cc, fr, fc; do { cr = rng() % 10; cc = rng() % 10; } while (g[cr][cc] == '*'); do { fr = rng() % 10; fc = rng() % 10; } while (g[fr][fc] == '*' || (fr == cr && fc == cc)); g[cr][cc] = 'C'; g[fr][fc] = 'F';
for (int i = 0; i < 10; i++) printf("%s\n", g[i]); return 0;}点「运行 ▶」看结果
7一页纸
| 关键的一步 | 状态有限(160000 种)+ 过程确定 ⇒ 局面一重复就可以断定「永远抓不到」 |
| 哪一版能 AC | 第三步那一版 p1518.cpp(⚠ 「循环十万次」那版也能 AC —— 它只是说不清) |
| 题面里的三句话 | 转向用掉这一分钟/移动是同时的、穿过不算相遇/地图边沿也是障碍 |
| 官方样例 | 这次很称职:两个错法(50、42)都挡住了 —— ⚠ 但这是试出来的,不能假设 |
| 这一页的主线 | 实测最大不是安全上限:随机 20000 张最大 220,专门搜能搜到 635(而且还只是下界); ⇒ 上限拍 300 / 500 都是错的,而随机对拍结构上一次都抓不到 |
| 还要记一条 | 这道题七成以上的随机图「永远抓不到」,对拍那些轮验的是零 |