题单 · 习题解析

洛谷 P1747 好奇怪的游戏

★★★ 题面末尾那两句听着一样危险的话,一句是命门(400 个输入里只有 1 个会变)、一句是噪声;而上一道「少了守卫不会 WA」的结论在这儿翻了过来

原题:洛谷 P1747出自 第 14 章 BFS 广度优先搜索:迷宫最短路 的题单题面本地存档:2026-08-28
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

《爱与愁的故事第三弹 · shopping》娱乐章。调调口味来道水题。

题目描述

爱与愁大神坐在公交车上无聊,于是玩起了手机。一款奇怪的游戏进入了爱与愁大神的眼帘: ***(游戏名被打上了马赛克)。这个游戏类似象棋,但是只有黑白马各一匹, 在点 x₁, y₁x₂, y₂ 上。它们得从点 x₁, y₁x₂, y₂ 走到 (1, 1)。 这个游戏与普通象棋不同的地方是:马可以走「日」,也可以像象走「田」。 现在爱与愁大神想知道两匹马到 (1, 1) 的最少步数,你能帮他解决这个问题么?

注意不能走到 xy 坐标 ≤ 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) 就是最短。

p1747Twice.cpp★ 这一版就能 AC
// ★ 第一版:照题单的注解「两个起点各跑一次 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

方向数组从 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}
                \___________ 日 ___________/   \___ 田 ___/
这道题的全部难度,在题面最后那两句话上

算法一个字都不用新想。可题面末尾并排放着两句话,听起来同样像是提醒

  1. 「注意不能走到 xy 坐标 ≤ 0 的位置」
  2. 「对于 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 列被当成了合法棋盘:

p1747Neg.cpp⚠ 下界写成了 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它逐字节过了官方样例8 / 9)。而这道题的输入空间只有 20 × 20 = 400 个起点, 所以不用对拍 —— 全跑一遍

把下界从 1 放宽到 0 -1 -2 -9
400 个起点里答案变了的 1 1 1 1
★★★ 那唯一变掉的一个是 (2,2):正确 3,放宽之后 2

它多出来的那条路是:

    (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)

p1747NoBishop.cpp⚠ 只有「日」,漏了「田」
// ⚠ 错法二:只写了「日」,把「象走田」漏了
//
// 这道题和第 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

这一个官方样例当场就挡住了:第一行应该是 8,它给 10

400 个起点里答案变大的 最多多几步 样例 (12,16)
漏掉「田」 262 4 8 → ⚠ 10

⇒ 值得记住的仍然是比例:这一页四个真实错法里,官方样例只挡得住这一个 (这句话上一道 P1746 刚说过一次,那里是「三个主语被换掉,样例只挡住第一个」)。

5★★★ 第四个坑:上一页那条「少了守卫不会 WA」,在这道题上翻了过来

P1443 里演示过一种很自然的写法:标记不在入队时打,等它出队再打, 并且把那句 if (vis[x][y]) continue; 守卫也去掉。 那一页给它的判词是:

它答案全对 —— n, m ≤ 14 的 11025 组一次都没错,代价只在入队次数上。

原因写得很清楚:马每跳一步必换格子颜色 ⇒ 那是一张二分图, 队列里同一个格子的所有副本距离都相同,写几遍都写同一个数。

把同一份写法原样搬到这道题:

p1747Late.cpp⚠⚠ 出队才标记,还少了守卫
// ⚠⚠ 错法三:出队才标记,而且少了那句守卫
//
// 这一份是**照抄** [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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它连官方样例都过不了(给 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.cpp★ 最终版:一次 BFS
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 但请注意:这个改进和「快」没有关系

两次 BFS 是 800 次入队,一次是 400 —— 正好 2 倍。 而这道题一共只有 400 个格子,2 倍在这里一文不值,两版都是瞬间出结果。

⇒ 换成一次 BFS 的理由是说得清:一张表、一个出处, 两匹马的答案不可能因为「其中一次写错了边界」而互相矛盾。

★ 这和第 5 章 P1996 那条正好是一对: 那道题的教训是「一个更优的算法用错了题就是零分」; 这道题的是「一个更优的写法用对了题,也可能一分钱都不值 —— 选它得有别的理由」。

7⚠ 这一页为什么没有对拍

★★ 输入空间一共 400 个起点 —— 那就别抽样了

这道题的输入是两个坐标,1 ≤ x, y ≤ 20一匹马只有 400 种可能的位置, 而两匹马互不相干。整道题的答案就是一张 20 × 20 的表(最大的那个数是 10)。

⇒ 上面每一段给的都是「400 个起点全跑一遍,差几个」,不是抓获率。 这正是第 14 章 P1443 那条的第三次现场 (那里数「有多少种抄错」、P1746 数「有多少种输入抓得到」、 这里干脆把输入空间本身数完了):

数得完的时候,别抽样。

★ 判据是「数得完吗」,一次都不用问「有没有第二个算法」。

8这一页所有数字都出自这一份

p1747Count.cpp度量:两句约束 + 方向数组 + 奇环
// 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
这一页记住三句话
  1. ★★★ 并排放着、语气一样的两句提醒,可能一句是命门、一句是噪声。 「不能走到 xy ≤ 0」违反了就有 1 个输入翻车; 「x, y ≤ 20」放宽了 400 个输入一个不变。 ⇒ 分辨它们的唯一办法,是各造一档违反它的数据跑一次第 12 章 P1226 的动作)。
  2. ★★★ 「这个写法会不会 WA」缺一个主语,主语是那张图有没有奇环。 同一份「出队才标记 + 没守卫」的代码、同一张 20 × 20 棋盘, 12 个方向错 324 / 400,8 个方向错 ★ 0 / 400。 ⇒ 上一页的「它不会 WA」是对那道题说的,不是对那个写法说的。
  3. ★★ 输入空间数得完的时候,别抽样。 这道题一共 400 个起点,每一条结论都是「全跑一遍差几个」—— 于是「只有 1 个会变」这种话才敢说出口,抓获率是说不出这句话的。