0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1747,日期见页头。两边不一致时信原站。
题目背景
《爱与愁的故事第三弹 · shopping》娱乐章。调调口味来道水题。
题目描述
爱与愁大神坐在公交车上无聊,于是玩起了手机。一款奇怪的游戏进入了爱与愁大神的眼帘:
***(游戏名被打上了马赛克)。这个游戏类似象棋,但是只有黑白马各一匹,
在点 x₁, y₁ 和 x₂, y₂ 上。它们得从点 x₁, y₁ 和 x₂, y₂ 走到 (1, 1)。
这个游戏与普通象棋不同的地方是:马可以走「日」,也可以像象走「田」。
现在爱与愁大神想知道两匹马到 (1, 1) 的最少步数,你能帮他解决这个问题么?
注意不能走到 x 或 y 坐标 ≤ 0 的位置。满足此限制的 (x, y) 均可到达。
输入格式
第一行两个整数 x₁, y₁。
第二行两个整数 x₂, y₂。
输出格式
第一行一个整数,表示黑马到 (1, 1) 的步数。
第二行一个整数,表示白马到 (1, 1) 的步数。
数据规模与约定
对于 100% 数据,1 ≤ x₁, y₁, x₂, y₂ ≤ 20。
输入输出样例
输入
12 16 18 10
输出
8 9
洛谷给的样例输出是 8 (带一个尾随空格)和 9。
这一页的 .out 是仓库里的正解真跑出来的(npm run gen:samples),所以没有那个空格。
⇒ 这类空白差异洛谷是判对的,不用管; 但它提醒一件事 —— 样例输出里的空白肉眼看不出来, 第 14 章 P1443 末尾那条说的也是这个(那道题当年要求固定场宽)。
1第一反应:两匹马各跑一次 BFS —— ★ 这一版就已经能 AC
题单里这道题的注解就是「两个起点各跑一次 BFS」,而它是对的:
从马自己的位置出发,第一次碰到 (1, 1) 就是最短。
// ★ 第一版:照题单的注解「两个起点各跑一次 BFS」—— 这一版就已经能 AC//// 想法最直接:一匹马一次 BFS,从它自己的位置出发,走到 (1,1) 就停。// 12 个方向(8 个「日」+ 4 个「田」)、下界是 1、棋盘 20 × 20 —— 三处都对,它就是对的。//// ⚠ 它唯一「多做」的事是**把同一张 400 格的表算了两遍**:// 入队 800 次,而从 (1,1) 出发只算一次是 400 次(p1747Count.cpp 第 ④ 段)。// ⇒ 2 倍。而这道题一共 400 个格子 —— **2 倍在这里一文不值**。// 所以 p1747.cpp 换成一次 BFS,理由是「一张表、一个出处,说得清」,**不是快**。
#include <bits/stdc++.h>using namespace std;
const int N = 20;const int DX[12] = {1, 1, -1, -1, 2, 2, -2, -2, 2, 2, -2, -2};const int DY[12] = {2, -2, 2, -2, 1, -1, 1, -1, 2, -2, 2, -2};
int bfs(int sx, int sy) { static int dist_[N + 1][N + 1]; for (int i = 1; i <= N; i++) for (int j = 1; j <= N; j++) dist_[i][j] = -1;
queue<pair<int, int>> q; dist_[sx][sy] = 0; q.push({sx, sy}); while (!q.empty()) { auto [x, y] = q.front(); q.pop(); if (x == 1 && y == 1) return dist_[x][y]; // 第一次碰到 (1,1) 就是最短 for (int k = 0; k < 12; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > N || b < 1 || b > N) continue; if (dist_[a][b] != -1) continue; dist_[a][b] = dist_[x][y] + 1; q.push({a, b}); } } return -1;}
int main() { int x1_, y1_, x2_, y2_; if (scanf("%d %d %d %d", &x1_, &y1_, &x2_, &y2_) != 4) return 0; printf("%d\n%d\n", bfs(x1_, y1_), bfs(x2_, y2_)); return 0;}点「运行 ▶」看结果
方向数组从 P1443 的 8 个变成 12 个 —— 前 8 个是「日」,后 4 个是「田」:
DX[12] = { 1, 1, -1, -1, 2, 2, -2, -2, 2, 2, -2, -2}
DY[12] = { 2, -2, 2, -2, 1, -1, 1, -1, 2, -2, 2, -2}
\___________ 日 ___________/ \___ 田 ___/
算法一个字都不用新想。可题面末尾并排放着两句话,听起来同样像是提醒:
- 「注意不能走到
x或y坐标≤ 0的位置」 - 「对于
100%数据,1 ≤ x₁, y₁, x₂, y₂ ≤ 20」
⇒ 第 ②③ 步分别去量它们。一句是命门,一句是噪声 —— 而第 12 章 P1226 给过判据,两句用的是同一个动作: 造一档违反它的数据,看有没有哪一版的行为变了。
2★★★ 第一句是命门 —— 而它只在 400 个输入里的 1 个上起作用
数组是从下标 0 开始的,所以边界判断写成这样看着天经地义:
if (a < 0 || a > N || b < 0 || b > N) continue; // 只挡住了「下标越界」
它挡住了数组越界,却没有挡住题面那条规则 —— 第 0 行、第 0 列被当成了合法棋盘:
// ⚠ 错法一:边界的下界写成了 0 —— 于是马可以踩到 x 或 y = 0 的格子//// 题面在描述的末尾专门写了一句://// 「注意不能走到 x 或 y 坐标 ≤ 0 的位置。」//// 而数组是从下标 0 开始的,写 `if (a < 0 || a > N) continue;` 看着天经地义 ——// 它挡住了「下标越界」,却**没有挡住题面那条规则**:第 0 行、第 0 列被当成了合法棋盘。//// ★★★ 这一版的抓获率是这一页最值钱的数字:**400 个起点里只有 1 个答案会变。**// 那一个是 **(2,2)**:正确答案 3,这一版给 2 ——// 因为它可以走 (2,2) →日→ (3,0) →日→ (1,1),而 (3,0) 的纵坐标是 0。// ⇒ 官方样例(12 16 / 18 10)**一个字都说不出来**。//// 除了那一句边界判断,其余和 p1747.cpp 逐字相同。
#include <bits/stdc++.h>using namespace std;
const int N = 20;const int DX[12] = {1, 1, -1, -1, 2, 2, -2, -2, 2, 2, -2, -2};const int DY[12] = {2, -2, 2, -2, 1, -1, 1, -1, 2, -2, 2, -2};
int main() { int x1_, y1_, x2_, y2_; if (scanf("%d %d %d %d", &x1_, &y1_, &x2_, &y2_) != 4) return 0;
static int dist_[N + 1][N + 1]; for (int i = 0; i <= N; i++) for (int j = 0; j <= N; j++) dist_[i][j] = -1;
queue<pair<int, int>> q; dist_[1][1] = 0; q.push({1, 1});
while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 12; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 0 || a > N || b < 0 || b > N) continue; // ⚠ 下界写成了 0 if (dist_[a][b] != -1) continue; dist_[a][b] = dist_[x][y] + 1; q.push({a, b}); } }
printf("%d\n%d\n", dist_[x1_][y1_], dist_[x2_][y2_]); return 0;}点「运行 ▶」看结果
它逐字节过了官方样例(8 / 9)。而这道题的输入空间只有 20 × 20 = 400 个起点,
所以不用对拍 —— 全跑一遍:
把下界从 1 放宽到 |
0 |
-1 |
-2 |
-9 |
|---|---|---|---|---|
| 400 个起点里答案变了的 | ★ 1 | 1 | 1 | 1 |
它多出来的那条路是:
(2,2) --日(+1,-2)--> (3,0) --日(-2,+1)--> (1,1)
^^^^^
纵坐标是 0 —— 题面明说不许踩正确答案是 3((2,2) 的邻居里,离 (1,1) 最近的一个是 2 步)。
⇒ 这就是「命门」的样子:违反它,行为立刻变了。
⚠ 可它的抓获面积小到吓人 —— 400 分之 1,而且官方样例正好不在那一个上。
★ 再往下放宽(-1、-2、-9)一个新的都不会多:
(2,2) 那条捷径只需要踩到 0,更远的负坐标帮不上任何忙。
3⚠ 第二句是噪声 —— 「x, y ≤ 20」说的是输入,不是棋盘
题面没有说棋盘只有 20 × 20,反而写着「满足此限制的 (x, y) 均可到达」。
于是很自然会担心一件事:最优路径会不会先绕到 20 外面去,再拐回来?
这个担心值得量一下(真值是棋盘开到 1..400):
| 棋盘开到 | 20 |
21 |
25 |
40 |
100 |
|---|---|---|---|---|---|
| 和真值不同的 | ★ 0 | 0 | 0 | 0 | 0 |
写这一页的草稿时,第一条就是「棋盘一定要开大一点」。实测是 0 / 400。
20 × 20 里最大的答案只有 10 步,绕出去从来换不来更短的路。
⇒ 按第 12 章 P1226 那把尺子,「x, y ≤ 20」在这道题上是噪声:
造一档违反它(棋盘放大)的数据,没有任何一版的行为变了。
⚠ 注意这不是说「数据范围不重要」—— 它是输入范围,你得靠它决定数组开多大。 说它是噪声,只针对「棋盘要不要开得比 20 大」这一个具体问题。 ★ 而这两步合起来才是这一页的主线: 两句并排放着、语气一样的提醒,一句让 1 个输入翻车,一句一个都不影响 —— 分辨它们的唯一办法是各造一档数据跑一次。
4⚠ 第三个坑:把「象走田」漏了
这道题和上一道 P1443 只差一句话:马还能像象一样走「田」。
把 P1443 那份搬过来,最容易漏的就是后面那 4 个 (±2, ±2):
// ⚠ 错法二:只写了「日」,把「象走田」漏了//// 这道题和第 14 章题单上一道 P1443(马的遍历)只差一句话:// **「马可以走『日』,也可以像象走『田』」** —— 方向从 8 个变成 12 个。// 把 P1443 那份直接搬过来,漏掉的就是后面那 4 个 (±2, ±2)。//// ★ 好消息:**官方样例当场就挡住它** —— 第一行应该是 8,它给 10。// (p1747Count.cpp 第 ③ 段:400 个起点里 262 个答案变大,最多多 4 步。)//// ⚠ 但它顺带带来一件**和 P1443 完全相反**的事,见 p1747Late.cpp 文件头:// 「田」走一步**不换格子颜色**,而「日」换 —— 少了这 4 个方向,这张图就是二分图;// 加上它们,就有奇环了。
#include <bits/stdc++.h>using namespace std;
const int N = 20;const int DX[8] = {1, 1, -1, -1, 2, 2, -2, -2};const int DY[8] = {2, -2, 2, -2, 1, -1, 1, -1};
int main() { int x1_, y1_, x2_, y2_; if (scanf("%d %d %d %d", &x1_, &y1_, &x2_, &y2_) != 4) return 0;
static int dist_[N + 1][N + 1]; for (int i = 1; i <= N; i++) for (int j = 1; j <= N; j++) dist_[i][j] = -1;
queue<pair<int, int>> q; dist_[1][1] = 0; q.push({1, 1});
while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 8; k++) { // ⚠ 只有 8 个方向 int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > N || b < 1 || b > N) continue; if (dist_[a][b] != -1) continue; dist_[a][b] = dist_[x][y] + 1; q.push({a, b}); } }
printf("%d\n%d\n", dist_[x1_][y1_], dist_[x2_][y2_]); return 0;}点「运行 ▶」看结果
这一个官方样例当场就挡住了:第一行应该是 8,它给 10。
| 400 个起点里答案变大的 | 最多多几步 | 样例 (12,16) |
|
|---|---|---|---|
| 漏掉「田」 | 262 | 4 | 8 → ⚠ 10 |
⇒ 值得记住的仍然是比例:这一页四个真实错法里,官方样例只挡得住这一个 (这句话上一道 P1746 刚说过一次,那里是「三个主语被换掉,样例只挡住第一个」)。
5★★★ 第四个坑:上一页那条「少了守卫不会 WA」,在这道题上翻了过来
P1443 里演示过一种很自然的写法:标记不在入队时打,等它出队再打,
并且把那句 if (vis[x][y]) continue; 守卫也去掉。
那一页给它的判词是:
它答案全对 ——
n, m ≤ 14的 11025 组一次都没错,代价只在入队次数上。
原因写得很清楚:马每跳一步必换格子颜色 ⇒ 那是一张二分图, 队列里同一个格子的所有副本距离都相同,写几遍都写同一个数。
把同一份写法原样搬到这道题:
// ⚠⚠ 错法三:出队才标记,而且少了那句守卫//// 这一份是**照抄** [P1443](/sol/p1443/) 里的 `p1443LateBad.cpp`:// 标记不是在入队时打,而是等它出队才打,并且**没有** `if (vis[x][y]) continue;` 那句守卫。//// ★★★ 上一页给那份写法的判词是:「**它答案全对**,代价只在入队次数上」——// `n, m ≤ 14` 的 11025 组一次都没错。原因是**马每跳一步必换格子颜色 ⇒ 那是二分图**,// 队列里同一个格子的所有副本距离都相同,写几遍都写同一个数。//// ⚠⚠ **这道题把那句判词翻了过来**,而变的只有方向数组:// · 「日」走一步换颜色(1 + 2 = 3,奇数);// · 「田」走一步**不换**颜色(2 + 2 = 4,偶数)。// ⇒ 12 个方向的这张图**有奇环**:同一个格子既能用奇数步到、也能用偶数步到。// 于是队列里同一格的副本距离**不一样**,后到的那份把先到的正确值覆盖掉。//// 实测(p1747Count.cpp 第 ⑤ 段,同一份代码、同一张棋盘,只换方向数组)://// 12 个方向(有奇环):错 324 / 400// 8 个方向(二分图):错 ★ 0 / 400//// ⇒ **「这个写法会不会 WA」缺一个主语,主语是那张图有没有奇环。**//// ⚠ 队列装不下就喊出来,绝不悄悄少入几次队(P1443 那条教训)。
#include <bits/stdc++.h>using namespace std;
const int N = 20;const int DX[12] = {1, 1, -1, -1, 2, 2, -2, -2, 2, 2, -2, -2};const int DY[12] = {2, -2, 2, -2, 1, -1, 1, -1, 2, -2, 2, -2};const long long QCAP = 20000000LL;
int dist_[N + 1][N + 1];bool vis[N + 1][N + 1];
int main() { int x1_, y1_, x2_, y2_; if (scanf("%d %d %d %d", &x1_, &y1_, &x2_, &y2_) != 4) return 0; for (int i = 1; i <= N; i++) for (int j = 1; j <= N; j++) dist_[i][j] = -1;
// 把 (x, y, d) 压进一个 int:x, y ≤ 20 各占 5 位,d 占剩下的 vector<int> q; q.push_back((1 << 16) | (1 << 8) | 0); for (size_t h = 0; h < q.size(); h++) { int x = q[h] >> 16, y = (q[h] >> 8) & 255, d0 = q[h] & 255; vis[x][y] = true; // ⚠⚠ 少了 if (vis[x][y]) continue; dist_[x][y] = d0; for (int k = 0; k < 12; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > N || b < 1 || b > N) continue; if (vis[a][b]) continue; if ((long long)q.size() >= QCAP) { fprintf(stderr, "队列装不下:入队次数超过 %lld\n", QCAP); return 1; } q.push_back((a << 16) | (b << 8) | (d0 + 1)); } }
printf("%d\n%d\n", dist_[x1_][y1_], dist_[x2_][y2_]); return 0;}点「运行 ▶」看结果
它连官方样例都过不了(给 9 / 10,应该是 8 / 9)。
度量程序把这四格全跑了一遍(棋盘都是 20 × 20,起点都是 (1,1)):
| 有那句守卫 | ⚠ 没有那句守卫 | |
|---|---|---|
| 12 个方向(日 + 田,有奇环) | 错 0 / 400 | ⚠ 错 324 / 400 |
| 8 个方向(只有日,二分图) | 错 0 / 400 | ★ 错 0 / 400 |
四格里只有一格是错的。 变量只有一个:方向数组。
为什么:「日」走一步换颜色(1 + 2 = 3,奇数),
「田」走一步不换(2 + 2 = 4,偶数)。
加上「田」之后,同一个格子既能用奇数步到、也能用偶数步到 —— 那就是奇环。
度量程序找到的最小的那个是 (1,2):到 (1,1) 既有 2 步的走法,也有 3 步的走法。
于是队列里同一格的副本距离不一样,后到的那份把先到的正确值覆盖掉。
⇒ 「这个写法会不会 WA」缺一个主语,主语是那张图有没有奇环。 上一页是拿八连通造了一个反证,这一页里它是真的。
代价那一头也一起量了(入队次数,棋盘 20 × 20):
| 写法 | 入队次数 |
|---|---|
★ 入队时标记(p1747.cpp) |
400 |
| 出队才标记 + 有守卫 | 2017 |
| ⚠⚠ 出队才标记 + 没守卫(12 个方向) | 6119802 |
| 出队才标记 + 没守卫(8 个方向) | 1265271 |
6★ 最终版:一次 BFS 就够(而理由不是「快」)
12 个方向是成对相反的(有 (+1,+2) 就有 (-1,-2),有 (+2,+2) 就有 (-2,-2)),
而 x, y ≥ 1 这条限制对路径的两个端点是同一条。于是:
从 (x,y) 走到 (1,1) 的最少步数 == 从 (1,1) 走到 (x,y) 的最少步数
⇒ 从 (1,1) BFS 一次,整张 20 × 20 的表就出来了,两匹马各查一格:
// P1747 好奇怪的游戏 —— BFS(★ 最终版:一次 BFS 就够)//// 题目:马既能走「日」(8 个方向),也能像象一样走「田」(4 个方向),一共 12 个方向。// 两匹马分别在 (x1,y1) 和 (x2,y2),各自要走到 (1,1),求最少步数。// ⚠ 不能走到 x 或 y ≤ 0 的位置;满足这个限制的 (x,y) 都可以到达。//// ★ 为什么一次 BFS 就够(而题单的注解写的是「两个起点各跑一次」):// 12 个方向是**成对相反**的(有 (+1,+2) 就有 (-1,-2),有 (+2,+2) 就有 (-2,-2)),// 而「x, y ≥ 1」这个限制对两个端点是同一条 —— 于是//// 从 (x,y) 走到 (1,1) 的最少步数 == 从 (1,1) 走到 (x,y) 的最少步数//// ⇒ 从 (1,1) BFS 一次,整张表就出来了,两匹马各查一次。// ⚠ 这不是为了快(这道题一共才 400 个格子,两次也就 800 次入队)——// 是为了**说得清**:一张表、一个出处。//// ★ 棋盘开多大?题面只说输入 x, y ≤ 20,**没说棋盘只有 20 × 20**。// 实测(p1747Count.cpp 第 ② 段):棋盘开到 20 和开到 400,// 400 个起点的答案**一个不差** —— 最优路径从来不需要绕到 20 外面去。// ⇒ 所以这里就开 20。这是量出来的,不是猜的。
#include <bits/stdc++.h>using namespace std;
const int N = 20;// 前 8 个是「日」,后 4 个是「田」const int DX[12] = {1, 1, -1, -1, 2, 2, -2, -2, 2, 2, -2, -2};const int DY[12] = {2, -2, 2, -2, 1, -1, 1, -1, 2, -2, 2, -2};
int main() { int x1_, y1_, x2_, y2_; if (scanf("%d %d %d %d", &x1_, &y1_, &x2_, &y2_) != 4) return 0;
static int dist_[N + 1][N + 1]; for (int i = 1; i <= N; i++) for (int j = 1; j <= N; j++) dist_[i][j] = -1;
queue<pair<int, int>> q; dist_[1][1] = 0; q.push({1, 1});
while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 12; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > N || b < 1 || b > N) continue; // ★ 下界是 1,不是 0 if (dist_[a][b] != -1) continue; dist_[a][b] = dist_[x][y] + 1; // 入队时就定下距离 q.push({a, b}); } }
printf("%d\n%d\n", dist_[x1_][y1_], dist_[x2_][y2_]); return 0;}点「运行 ▶」看结果
两次 BFS 是 800 次入队,一次是 400 —— 正好 2 倍。 而这道题一共只有 400 个格子,2 倍在这里一文不值,两版都是瞬间出结果。
⇒ 换成一次 BFS 的理由是说得清:一张表、一个出处, 两匹马的答案不可能因为「其中一次写错了边界」而互相矛盾。
★ 这和第 5 章 P1996 那条正好是一对: 那道题的教训是「一个更优的算法用错了题就是零分」; 这道题的是「一个更优的写法用对了题,也可能一分钱都不值 —— 选它得有别的理由」。
7⚠ 这一页为什么没有对拍
这道题的输入是两个坐标,1 ≤ x, y ≤ 20 ⇒ 一匹马只有 400 种可能的位置,
而两匹马互不相干。整道题的答案就是一张 20 × 20 的表(最大的那个数是 10)。
⇒ 上面每一段给的都是「400 个起点全跑一遍,差几个」,不是抓获率。 这正是第 14 章 P1443 那条的第三次现场 (那里数「有多少种抄错」、P1746 数「有多少种输入抓得到」、 这里干脆把输入空间本身数完了):
数得完的时候,别抽样。
★ 判据是「数得完吗」,一次都不用问「有没有第二个算法」。
8这一页所有数字都出自这一份
// P1747 的度量程序 —— 这一页所有数字都出自这一份。//// `./p1747Count` 人看的版本// `./p1747Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用//// ★ 这道题的**输入空间只有 400 个起点**(x, y ≤ 20),所以这一页**不需要对拍** ——// 每一段都是把 400 个起点**全跑一遍**,给的是「一个不差」而不是「抓获率」。//// 六段:// ① 题面那句「不能走到 x 或 y ≤ 0」是**命门**:把下界放宽,400 个里只有 1 个答案变;// ② 而「x, y ≤ 20」是**输入范围不是棋盘范围**:棋盘开 20 和开 400,400 个起点一个不差;// ③ 漏掉「象走田」:262 个答案变大;// ④ 一次 BFS / 两次 BFS 的入队次数;// ⑤ 出队才标记、少了守卫:同一份写法,只换方向数组,12 个方向错 324 个、8 个方向 0 个;// ⑥ 为什么会这样:12 个方向的图**有奇环**(找出最小的那个「奇偶都能到」的格子)。
#include <bits/stdc++.h>using namespace std;
static bool CSV = false;// 前 8 个是「日」,后 4 个是「田」static const int DX[12] = {1, 1, -1, -1, 2, 2, -2, -2, 2, 2, -2, -2};static const int DY[12] = {2, -2, 2, -2, 1, -1, 1, -1, 2, -2, 2, -2};
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");}
/** 从 (1,1) BFS:坐标允许 lo..hi,方向取前 nd 个。返回 dist(按下标平移到 0 起)。 */static vector<vector<int>> bfs(int lo, int hi, int nd) { int n = hi - lo + 1; vector<vector<int>> d(n, vector<int>(n, -1)); auto id = [&](int v) { return v - lo; }; queue<pair<int, int>> q; d[id(1)][id(1)] = 0; q.push({1, 1}); while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < nd; k++) { int a = x + DX[k], b = y + DY[k]; if (a < lo || a > hi || b < lo || b > hi) continue; if (d[id(a)][id(b)] != -1) continue; d[id(a)][id(b)] = d[id(x)][id(y)] + 1; q.push({a, b}); } } return d;}
/** 出队才标记的写法。guard = 是否保留那句 if (vis) continue。返回 (dist, 入队次数)。 */static pair<vector<vector<int>>, long long> lateRun(int N, int nd, bool guard) { vector<vector<int>> d(N + 1, vector<int>(N + 1, -1)); vector<vector<char>> vis(N + 1, vector<char>(N + 1, 0)); vector<array<int, 3>> q; q.push_back({1, 1, 0}); long long pushes = 1; for (size_t h = 0; h < q.size(); h++) { int x = q[h][0], y = q[h][1], d0 = q[h][2]; if (guard && vis[x][y]) continue; vis[x][y] = true; d[x][y] = d0; for (int k = 0; k < nd; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > N || b < 1 || b > N) continue; if (vis[a][b]) continue; q.push_back({a, b, d0 + 1}); pushes++; } } return {d, pushes};}
int main(int argc, char** argv) { CSV = (argc > 1 && string(argv[1]) == "csv"); const int N = 20; auto ref = bfs(1, 400, 12); // 棋盘足够大 + 下界正确,当作真值 auto at = [&](const vector<vector<int>>& d, int lo, int x, int y) { return d[x - lo][y - lo]; };
/* ① 下界放宽(题面那句「不能走到 x 或 y ≤ 0」) */ { vector<long long> changed; vector<int> los = {0, -1, -2, -9}; int firstX = 0, firstY = 0, okv = 0, badv = 0; if (!CSV) printf("① 把下界从 1 放宽(题面:不能走到 x 或 y ≤ 0)\n"); for (int lo : los) { auto d = bfs(lo, 400, 12); int c = 0; for (int x = 1; x <= N; x++) for (int y = 1; y <= N; y++) if (at(ref, 1, x, y) != at(d, lo, x, y)) { c++; if (!firstX) { firstX = x; firstY = y; okv = at(ref, 1, x, y); badv = at(d, lo, x, y); } } changed.push_back(c); if (!CSV) printf(" 允许走到 %3d:400 个起点里 %d 个答案变了\n", lo, c); } if (!CSV) printf(" ★ 唯一变掉的那个是 (%d,%d):正确 %d,放宽之后 %d\n\n", firstX, firstY, okv, badv); row("loosenLo", changed); row("onlyPoint", {firstX, firstY, okv, badv}); }
/* ② 棋盘上界(题面只说输入 ≤ 20) */ { vector<long long> diffs; vector<int> caps = {20, 21, 25, 40, 100}; int maxAns = 0; for (int x = 1; x <= N; x++) for (int y = 1; y <= N; y++) maxAns = max(maxAns, at(ref, 1, x, y)); if (!CSV) printf("② 棋盘开多大够用(真值是开到 400)\n"); for (int cap : caps) { auto d = bfs(1, cap, 12); int c = 0; for (int x = 1; x <= N; x++) for (int y = 1; y <= N; y++) if (at(ref, 1, x, y) != at(d, 1, x, y)) c++; diffs.push_back(c); if (!CSV) printf(" 棋盘 1..%3d:和真值不同的 %d / 400\n", cap, c); } if (!CSV) printf(" ⇒ 20 × 20 就够;20 × 20 里最大的答案是 %d 步\n\n", maxAns); row("capDiff", diffs); row("maxAns", {maxAns}); }
/* ③ 漏掉「象走田」 */ { auto only8 = bfs(1, 400, 8); int c = 0, worst = 0; for (int x = 1; x <= N; x++) for (int y = 1; y <= N; y++) { int a = at(ref, 1, x, y), b = at(only8, 1, x, y); if (a != b) { c++; worst = max(worst, b - a); } } if (!CSV) printf("③ 只写「日」漏掉「田」:400 个起点里 %d 个答案变大(最多多 %d 步);" "样例 (12,16) 由 %d 变成 %d\n\n", c, worst, at(ref, 1, 12, 16), at(only8, 1, 12, 16)); row("noBishop", {c, worst, at(ref, 1, 12, 16), at(only8, 1, 12, 16)}); }
/* ④ 一次 BFS / 两次 BFS 的入队次数 */ { // 一次:从 (1,1) 铺满 20 × 20 long long once = 0; { vector<vector<int>> d(N + 1, vector<int>(N + 1, -1)); queue<pair<int, int>> q; d[1][1] = 0; q.push({1, 1}); once = 1; while (!q.empty()) { auto [x, y] = q.front(); q.pop(); for (int k = 0; k < 12; k++) { int a = x + DX[k], b = y + DY[k]; if (a < 1 || a > N || b < 1 || b > N || d[a][b] != -1) continue; d[a][b] = d[x][y] + 1; q.push({a, b}); once++; } } } if (!CSV) printf("④ 入队次数:一次 BFS(从 (1,1) 铺满)%lld 次;两匹马各跑一次就是 %lld 次\n\n", once, once * 2); row("pushOnce", {once, once * 2}); }
/* ⑤ 出队才标记:同一份写法,只换方向数组 */ { vector<long long> wrong, pushes; if (!CSV) printf("⑤ 出队才标记(同一份写法,只换方向数组和那句守卫)\n"); for (int nd : {12, 8}) for (bool guard : {true, false}) { auto [d, p] = lateRun(N, nd, guard); int c = 0; auto good = bfs(1, 400, nd); for (int x = 1; x <= N; x++) for (int y = 1; y <= N; y++) if (at(good, 1, x, y) != d[x][y]) c++; wrong.push_back(c); pushes.push_back(p); if (!CSV) printf(" %2d 个方向,%s:错 %3d / 400,入队 %8lld 次\n", nd, guard ? "有守卫" : "没守卫", c, p); } if (!CSV) printf(" ⇒ 只有「12 个方向 + 没守卫」这一格是错的 —— 主语是那张图有没有奇环\n\n"); row("lateWrong", wrong); row("latePush", pushes); }
/* ⑥ 奇环的证据:某个格子到 (1,1) 既有奇数步也有偶数步的走法 */ { // 状态是 (x, y, 步数奇偶) vector<vector<array<int, 2>>> d(N + 1, vector<array<int, 2>>(N + 1, {-1, -1})); queue<array<int, 3>> q; d[1][1][0] = 0; q.push({1, 1, 0}); while (!q.empty()) { auto [x, y, p] = q.front(); q.pop(); for (int k = 0; k < 12; k++) { int a = x + DX[k], b = y + DY[k], np = p ^ 1; if (a < 1 || a > N || b < 1 || b > N) continue; if (d[a][b][np] != -1) continue; d[a][b][np] = d[x][y][p] + 1; q.push({a, b, np}); } } int fx = 0, fy = 0, e = 0, o = 0; for (int x = 1; x <= N && !fx; x++) for (int y = 1; y <= N && !fx; y++) if (d[x][y][0] != -1 && d[x][y][1] != -1 && !(x == 1 && y == 1)) { fx = x; fy = y; e = d[x][y][0]; o = d[x][y][1]; } if (!CSV) printf("⑥ 有奇环的证据:格子 (%d,%d) 到 (1,1) 既有 %d 步的走法、也有 %d 步的走法\n" " (「日」换颜色、「田」不换 —— 少了「田」这张图就是二分图了)\n", fx, fy, e, o); row("oddCycle", {fx, fy, e, o}); } return 0;}点「运行 ▶」看结果
9一张总表
| 版本 | 错在哪 | 官方样例 | 400 个起点里错几个 | 结果 |
|---|---|---|---|---|
⚠ p1747Neg |
下界写成 0 |
★ 过 | ★ 1(只有 (2,2)) |
✗ WA |
⚠ p1747NoBishop |
漏了「象走田」 | ✗ 给 10 |
262 | ✗ WA |
⚠ p1747Late |
出队才标记、没守卫 | ✗ 给 9/10 |
324 | ✗ WA |
★ p1747Twice |
— | ✓ | 0 | ★ AC |
★ p1747 |
— | ✓ | 0 | ★ AC |
- ★★★ 并排放着、语气一样的两句提醒,可能一句是命门、一句是噪声。
「不能走到
x或y≤ 0」违反了就有 1 个输入翻车; 「x, y ≤ 20」放宽了 400 个输入一个不变。 ⇒ 分辨它们的唯一办法,是各造一档违反它的数据跑一次(第 12 章 P1226 的动作)。 - ★★★ 「这个写法会不会 WA」缺一个主语,主语是那张图有没有奇环。
同一份「出队才标记 + 没守卫」的代码、同一张
20 × 20棋盘, 12 个方向错 324 / 400,8 个方向错 ★ 0 / 400。 ⇒ 上一页的「它不会 WA」是对那道题说的,不是对那个写法说的。 - ★★ 输入空间数得完的时候,别抽样。 这道题一共 400 个起点,每一条结论都是「全跑一遍差几个」—— 于是「只有 1 个会变」这种话才敢说出口,抓获率是说不出这句话的。