题单 · 习题解析

洛谷 P1518 [USACO2.4] 两只塔姆沃斯牛

★★★ 「永远抓不到」凭什么下结论:状态有限 + 过程确定;而「实测最大不是安全上限」—— 随机两万张最大 220,专门搜能搜到 635

原题:洛谷 P1518出自 第 16 章 DFS 剪枝:可行性、最优性、搜索顺序 的题单题面本地存档:2026-08-28
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

两只牛逃跑到了森林里。Farmer John 开始用他的专家技术追捕这两头牛。 你的任务是模拟他们的行为(牛和 John)。

追击在 10 × 10 的平面网格内进行。一个格子可以是:空地,一个障碍物,两头牛(它们总在一起), 或者 Farmer John。两头牛和 Farmer John 可以在同一个格子内(当他们相遇时), 但是他们都不能进入有障碍的格子。

一个格子可以是:

  • . 空地;
  • * 障碍物;
  • C 两头牛;
  • F Farmer John。

这里有一个地图的例子:

*...*.....
......*...
...*...*..
..........
...*.F....
*.....*...
...*......
..C......*
...*.*....
.*.*......

牛在地图里以固定的方式游荡。每分钟,它们可以向前移动或是转弯。 如果前方无障碍(地图边沿也是障碍),它们会按照原来的方向前进一步。 否则它们会用这一分钟顺时针转 90 度。同时,它们不会离开地图。

Farmer John 深知牛的移动方法,他也这么移动。

每次(每分钟)Farmer John 和两头牛的移动是同时的。 如果他们在移动的时候穿过对方,但是没有在同一格相遇,我们不认为他们相遇了。 当他们在某分钟末在某格子相遇,那么追捕结束。

读入十行表示地图。每行都只包含 10 个字符,表示的含义和上面所说的相同。 保证地图中只有一个 F 和一个 CFC 一开始不会处于同一个格子中。

计算 Farmer John 需要多少分钟来抓住他的牛,假设牛和 Farmer John 一开始的行动方向都是正北(即上)。 如果 John 和牛永远不会相遇,输出 0。

输入格式

输入共十行,每行 10 个字符,表示如上文描述的地图。

输出格式

输出一个数字,表示 John 需要多少时间才能抓住牛们。如果 John 无法抓住牛,则输出 0。

数据规模与约定

USACO 2.4,翻译来自 NOCOW。地图固定是 10 × 10。

输入输出样例

输入

*...*.....
......*...
...*...*..
..........
...*.F....
*.....*...
...*......
..C......*
...*.*....
.*.*......

输出

49

就是题面里那张示例地图,49 分钟后两人落在同一格。

1第一反应:照着题面模拟,跑够多次就当抓不到

这道题没有算法。规则题面写得清清楚楚,照抄就是:

    每分钟:
      面前那格能走 -> 往前一步
      不能走       -> 用这一分钟顺时针转 90 度
    牛和农夫同时走完,再看是不是同一格

难的只有最后一句话:「永远不会相遇,输出 0」 —— 程序不可能真的跑到永远,所以第一版几乎都长这样:循环个十万次,还没抓到就输出 0。

p1518Guess.cpp第一版:循环十万次
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
先说清楚:这一版能 AC

这一页不打算假装它是错的 —— 它交上去就是过的,而且快得没影。

它唯一的问题是一句话:「十万」是拍出来的,你说不清为什么够。 第 ④ ⑤ 步会把这句话变成数字,而结论比想象中难看。

2★ 题面里有三句话,漏一句就错 —— 而这次官方样例全挡住了

① 「用这一分钟顺时针转 90 度」——转向本身就是这一分钟做的事。

读成「转到能走为止,再走一步」更顺口,也更像常识。可那样每分钟都保证走一步, 同样的分钟数里走得更远:

p1518Turn.cpp错法一:转向不占时间
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

② 「移动是同时的;穿过对方不算相遇」。

顺手写的模拟往往是「先动牛、看看抓到没有、再动农夫」—— 那就把擦肩而过也算成了抓到:

p1518Cross.cpp错法二:顺序移动
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

③ 「地图边沿也是障碍」。 这句在括号里,最容易看丢;丢了就会走出地图,多半直接崩。

★ 官方样例这次很称职:两个错法它都挡住了
正确答案 输出
转向不占时间 49 50
顺序移动(穿过也算相遇) 49 42

⇒ 这和同一轮的 P1074 正好相反 —— 那道题的官方样例 ① 放过了一个错法。 样例挡不挡得住,是一件要一个一个试出来的事,不能假设。

★ 顺带记一条被实测打回来的猜测:我原以为「顺序移动」要两人正好互换格子才触发、 会很难抓。实测不是 —— 只要某一分钟「牛走进了农夫还没离开的那一格」就够, 官方样例第 42 分钟就发生了。⇒ 又一次「我知道这个 bug 靠什么现形,其实只知道了一半」。

3★★★ 「永远抓不到」凭什么下结论 —— 这才是这道题真正在问的

把「跑十万次」换成一句能证明的话,需要的只有两个观察:

    ① 整个过程是**完全确定**的 —— 这一分钟的局面,唯一决定下一分钟的局面。
    ② 局面的种数是**有限**的:

         牛的格子 100 × 牛的朝向 4 × 农夫的格子 100 × 农夫的朝向 4 = 160000

⇒ 一旦某个局面第二次出现,后面就是一模一样的循环,再走一亿分钟也不会有新事发生。 见到重复局面就可以立刻输出 0 —— 这个「立刻」是证明,不是猜。

p1518.cpp★ 这一版就能 AC
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
代价是一个 156 KB 的标记数组,换来「不需要任何拍出来的数」

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

三件事,一件比一件难听:

  1. 随机永远见不到 635 这种图 —— 两万张里的天花板是 220,差了 2.9 倍。
  2. 「搜到的」不等于「最大的」:头一轮爬山(12 次重启)只搜到 506, 把重启次数加到 40 就变成了 635。搜得更久还能更大 —— 这个数只是一个下界
  3. ⚠⚠ 直接对着一个指标搜,反而输给了另一个指标的副产品: 专门为「判环最长」搜出来的那张图只有 839,连随机的 843 都没赢过; 而为「最大答案」搜出来的那张图顺带就有 1060。爬山会卡在局部最优。

⇒ 三条说的是同一件事:「我跑了两万组,最大才 220」推不出「300 够用」, 而「我搜过了,最大就是 635」同样推不出「1000 够用」。 唯一能给出上限的是状态数:160000。而第 ③ 步那一版连这个数都不需要

5★★★ 上限拍成 300 是错的 —— 而随机对拍一次都抓不到

把第一版的 100000 换成别的数,在同样 20000 张随机地图上和正解比:

上限 100 200 300 500 1000 2000
错的组数 4 0 0 0 0 0
p1518Small.cpp错法三:上限拍成 300
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 它是错的(第 ④ 步那张图答案 635),可 20000 张随机地图一组都没抓到

这是本书见过的「对拍抓不到」里最干净的一种:

  • 不是概率低 —— 随机地图的答案最大就是 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 的断言。 两张最坏图存成了常量(搜索是离线做的),度量程序每次都把它们重新验一遍。

p1518Count.cpp度量程序
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1518Gen.cpp数据生成器
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

7一页纸

关键的一步 状态有限(160000 种)+ 过程确定 ⇒ 局面一重复就可以断定「永远抓不到」
哪一版能 AC 第三步那一版 p1518.cpp(⚠ 「循环十万次」那版也能 AC —— 它只是说不清)
题面里的三句话 转向用掉这一分钟/移动是同时的、穿过不算相遇/地图边沿也是障碍
官方样例 这次很称职:两个错法(50、42)都挡住了 —— ⚠ 但这是试出来的,不能假设
这一页的主线 实测最大不是安全上限:随机 20000 张最大 220,专门搜能搜到 635(而且还只是下界);
⇒ 上限拍 300 / 500 都是错的,而随机对拍结构上一次都抓不到
还要记一条 这道题七成以上的随机图「永远抓不到」,对拍那些轮验的是零