题单 · 习题解析

洛谷 P2324 [SCOI2005] 骑士精神

★★★ IDA* 的估价函数「不许高估」要在**每个结点**上成立,不只是起点 —— 起点上只有 5% 高估,答案却错了 400 / 400

原题:洛谷 P2324出自 第 18 章 迭代加深与双向 BFS 的题单题面本地存档:2026-08-28
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

在一个 5 × 5 的棋盘上有 12 个白色的骑士和 12 个黑色的骑士,且有一个空位。 在任何时候一个骑士都能按照骑士的走法(它可以走到和他横坐标相差为 1,纵坐标相差为 2 或者横坐标相差为 2,纵坐标相差为 1 的格子)移动到空位上。

给定一个初始的棋盘,怎样才能经过移动变成如下目标棋盘:

目标棋盘:第一行全黑,往下白色逐行增多,正中间是空位

⚠ 这张图就是这道题的一半题面 —— 它写成字符是:

    1 1 1 1 1
    0 1 1 1 1
    0 0 * 1 1
    0 0 0 0 1
    0 0 0 0 0

0 白骑士、1 黑骑士、* 空位;黑白各 12 个,正中间空着。)

为了体现出骑士精神,他们必须以最小的步数完成任务。

输入格式

第一行有一个正整数 TT ≤ 10),表示一共有 T 组数据。

接下来有 T 个 5 × 5 的矩阵,0 表示白色骑士,1 表示黑色骑士,* 表示空位。 两组数据之间没有空行。

输出格式

对于每组数据都输出一行。如果能在 15 步以内(包括 15 步)到达目标状态,则输出步数,否则输出 -1

输入输出样例

输入

2
10110
01*11
10111
01001
00000
01011
110*1
01110
01010
00100

输出

7
-1

样例第二组数据的初始情况对应下面这张图 —— 它的答案是 -1(15 步以内到不了)。

样例第二组数据对应的棋盘

1★ 题面那句「15 步以内」,是这道题能做的全部理由

先把规模摆出来:12 白 + 12 黑 + 1 空,状态数是

    25 × C(24, 12) = 25 × 2 704 156 ≈ 6.8 × 10⁷

BFS 存得下一份,可题面说 T ≤ 10 —— 十组数据每组都铺一遍,时间和内存都不够。

而题面把深度钉死在 15。这正是第 18 章第 3 步那个工具的用武之地: 迭代加深 —— 深度上限从 1 试到 15,第一个搜得到的就是最优解, 内存只有一条路径那么深。

p2324Iddfs.cpp中间版:只有迭代加深
// P2324 的**中间版**:只有迭代加深,**没有估价函数**(也就是本章第 3 步那一版)。
//
// 题目:5 × 5 棋盘,12 个白骑士(0)、12 个黑骑士(1)、一个空位(*)。
// 每步把**任意一个骑士按马的走法跳到空位上**(等价于「空位按马的走法挪一格」)。
// 问最少几步能变成目标棋盘;**15 步以内**给不出就输出 -1。
//
// 它**是对的**,而且深度上限 15 是题面给的、不用自己猜。可它跑不完:
// 每层 8 个分支、15 层是 `8¹⁵ ≈ 3.5 × 10¹³`。
//
// ⚠⚠ 官方样例的第二组答案是 -1 —— 那意味着它要**把 15 层整棵树搜完**才敢下结论。
// ⇒ 这一版在官方样例上就跑不出来。页面第 ③ 步给它加了节点上限去量它到底走多远。
//
// ⇒ 差的那一步就是估价函数(p2324.cpp):一句 `depth + h > limit` 而已。
#include <bits/stdc++.h>
using namespace std;
static const char* TARGET = "1111101111" "00*11" "00001" "00000";
static const int DX[8] = {1, 1, -1, -1, 2, 2, -2, -2};
static const int DY[8] = {2, -2, 2, -2, 1, -1, 1, -1};
static char s[26];
static int limitDepth;
static long long nodes = 0;
/** 还有几个骑士不在位(★ 空位那一格不算) */
static int hval() {
int c = 0;
for (int i = 0; i < 25; i++) if (s[i] != '*' && s[i] != TARGET[i]) c++;
return c;
}
static bool dfs(int depth, int pos) { // pos = 空位在哪
nodes++;
if (hval() == 0) return true;
if (depth >= limitDepth) return false; // ★ 只有深度上限,没有估价
int x = pos / 5, y = pos % 5;
for (int t = 0; t < 8; t++) {
int nx = x + DX[t], ny = y + DY[t];
if (nx < 0 || nx >= 5 || ny < 0 || ny >= 5) continue;
int np = nx * 5 + ny;
swap(s[pos], s[np]);
if (dfs(depth + 1, np)) { swap(s[pos], s[np]); return true; }
swap(s[pos], s[np]);
}
return false;
}
int main(int argc, char** argv) {
int T;
if (!(cin >> T)) return 0;
while (T--) {
string row, all;
for (int i = 0; i < 5; i++) { cin >> row; all += row; }
memcpy(s, all.c_str(), 25);
s[25] = 0;
int pos = (int)all.find('*');
int ans = -1;
for (limitDepth = 0; limitDepth <= 15; limitDepth++) // ★ 迭代加深
if (dfs(0, pos)) { ans = limitDepth; break; }
cout << ans << "\n";
}
if (argc > 1 && string(argv[1]) == "count") cerr << "nodes=" << nodes << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 点上面那个「运行 ▶」会超时 —— 这就是重点

是对的,可每层 8 个分支、15 层是 8¹⁵ ≈ 3.5 × 10¹³

而官方样例第二组的答案是 -1 —— 那意味着它必须把 15 层整棵树搜完才敢下这个结论。

2★★★ 差的那一步:估价函数

★ 关键的一步

h(s) = 还有几个骑士不在自己该在的位置上(★ 空位那一格不算)。

它为什么成立,一句话:一步只能让一个骑士归位 —— 每步只有一个骑士动,它顶多从「不在位」变成「在位」。 ⇒ 从 s 出发至少还要 h(s) 步。

于是多一行剪枝:已走步数 + h > 深度上限 ⇒ 这条路必然超,直接回头。这就是 IDA*。

p2324.cpp★ 这一版就能 AC
// P2324 骑士精神 —— IDA*(★ 这一版就能 AC)
//
// 题目:5 × 5 棋盘,12 个白骑士(0)、12 个黑骑士(1)、一个空位(*)。
// 每步把**任意一个骑士按马的走法跳到空位上**(等价于「空位按马的走法挪一格」)。
// 问最少几步能变成目标棋盘;**15 步以内**给不出就输出 -1。
//
// ★ 题面那句「15 步以内」不是装饰,它是**这道题能做的全部理由**:
// 状态空间是 `25 × C(24,12) ≈ 6.8 × 10⁷`,BFS 存得下但十组数据存不起;
// 而深度被钉死在 15 ⇒ **迭代加深**(第 18 章第 3 步)正好用得上:
// 从 1 到 15 逐个试深度上限,第一个搜得到的就是答案。
//
// ⚠ 光有迭代加深还不够:每层 8 个分支、15 层是 8¹⁵ ≈ 3.5 × 10¹³。
// ⇒ 必须加**估价函数**,也就是本章第 7 步的 IDA*:
//
// ★★★ 估价函数 h(s) = **还有几个骑士不在自己该在的位置上**(空位不算)。
// 为什么它是对的 —— 一句话:**一步只能让一个骑士归位**
// (每步只有一个骑士动,它顶多从「不在位」变成「在位」)。
// ⇒ 至少还要 h 步 ⇒ `已走步数 + h > 深度上限` 时这条路必然超,直接回头。
//
// ⚠⚠ 这个「空位不算」不能省:把空位也算进去,h 就可能**比真实步数还大**(高估),
// 于是它会把**正确的那条路也剪掉** —— 那一版见 p2324Over.cpp,
// 而它的错法很有欺骗性:**答案不是偏小,是偏大或者变成 -1**。
#include <bits/stdc++.h>
using namespace std;
static const char* TARGET = "1111101111" "00*11" "00001" "00000";
static const int DX[8] = {1, 1, -1, -1, 2, 2, -2, -2};
static const int DY[8] = {2, -2, 2, -2, 1, -1, 1, -1};
static char s[26];
static int limitDepth;
static long long nodes = 0;
/** 还有几个骑士不在位(★ 空位那一格不算) */
static int hval() {
int c = 0;
for (int i = 0; i < 25; i++) if (s[i] != '*' && s[i] != TARGET[i]) c++;
return c;
}
static bool dfs(int depth, int pos) { // pos = 空位在哪
nodes++;
int h = hval();
if (h == 0) return true;
if (depth + h > limitDepth) return false; // ★ IDA* 的那一刀
int x = pos / 5, y = pos % 5;
for (int t = 0; t < 8; t++) {
int nx = x + DX[t], ny = y + DY[t];
if (nx < 0 || nx >= 5 || ny < 0 || ny >= 5) continue;
int np = nx * 5 + ny;
swap(s[pos], s[np]);
if (dfs(depth + 1, np)) { swap(s[pos], s[np]); return true; }
swap(s[pos], s[np]);
}
return false;
}
int main(int argc, char** argv) {
int T;
if (!(cin >> T)) return 0;
while (T--) {
string row, all;
for (int i = 0; i < 5; i++) { cin >> row; all += row; }
memcpy(s, all.c_str(), 25);
s[25] = 0;
int pos = (int)all.find('*');
int ans = -1;
for (limitDepth = 0; limitDepth <= 15; limitDepth++) // ★ 迭代加深
if (dfs(0, pos)) { ans = limitDepth; break; }
cout << ans << "\n";
}
if (argc > 1 && string(argv[1]) == "count") cerr << "nodes=" << nodes << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它值多少钱(节点上限 5000 万):

局面 真实步数 IDA* 纯迭代加深
官方样例 ① 7 391 28 181
官方样例 ②(答案 -1) -1 26 238 撞上限,跑不完
倒着走 15 步里最深的一个 13 60 890 撞上限,跑不完

⇒ 答案是 -1 或者步数很深的局面上,两者不是快慢之差,是能不能出结果之差

3★★★ 「空位不算」不能省 —— 而它错得很有欺骗性

h 写成「所有和目标不一样的格子数」(连空位也算),只差一个条件:

p2324Over.cpp错法:估价函数高估了
// P2324 的**错法**:估价函数**把空位那一格也算了进去**。
//
// 题目:5 × 5 棋盘,12 个白骑士(0)、12 个黑骑士(1)、一个空位(*)。
// 每步把**任意一个骑士按马的走法跳到空位上**(等价于「空位按马的走法挪一格」)。
// 问最少几步能变成目标棋盘;**15 步以内**给不出就输出 -1。
//
// 只差一个条件:`if (s[i] != TARGET[i]) c++;`(少了 `s[i] != '*' &&`)。
//
// ★★★ 而这一个字的后果,是把 IDA* 的地基抽掉了:
// 估价函数必须是**下界**(估出来的步数不能比真实的多),这样「`已走 + h > 上限` 就回头」
// 才不会误伤正确答案。空位算进去之后 h 可能**比真实步数大 1**,
// 于是它会把**正解那条路也剪掉**。
//
// ⚠⚠ 它的错法方向很有欺骗性:**答案不是偏小,是偏大或者干脆变成 -1**
// (搜不到就往下一个深度试,试到 15 还找不到就报 -1)。
// ★ 官方样例第一组它就给 **8**(正确答案 7)—— **这一次样例挡住了**。
// ⚠ 而更有意思的是它**比正解还快**(样例上 7221 个节点 vs 正解 26629)——
// 剪过头的程序总是更快,这正是第 16 章第 8 步说的那件事。
// ⇒ 页面第 ④ 步把「它的 h 到底高估了多少」量成了一个精确的计数。
#include <bits/stdc++.h>
using namespace std;
static const char* TARGET = "1111101111" "00*11" "00001" "00000";
static const int DX[8] = {1, 1, -1, -1, 2, 2, -2, -2};
static const int DY[8] = {2, -2, 2, -2, 1, -1, 1, -1};
static char s[26];
static int limitDepth;
static long long nodes = 0;
/** ★ 错法:连空位那一格也算进去了 —— 这会让 h 高估 */
static int hval() {
int c = 0;
for (int i = 0; i < 25; i++) if (s[i] != TARGET[i]) c++; // ★ 错在这里:空位也算了
return c;
}
static bool dfs(int depth, int pos) { // pos = 空位在哪
nodes++;
int h = hval();
if (h == 0) return true;
if (depth + h > limitDepth) return false; // ★ IDA* 的那一刀
int x = pos / 5, y = pos % 5;
for (int t = 0; t < 8; t++) {
int nx = x + DX[t], ny = y + DY[t];
if (nx < 0 || nx >= 5 || ny < 0 || ny >= 5) continue;
int np = nx * 5 + ny;
swap(s[pos], s[np]);
if (dfs(depth + 1, np)) { swap(s[pos], s[np]); return true; }
swap(s[pos], s[np]);
}
return false;
}
int main(int argc, char** argv) {
int T;
if (!(cin >> T)) return 0;
while (T--) {
string row, all;
for (int i = 0; i < 5; i++) { cin >> row; all += row; }
memcpy(s, all.c_str(), 25);
s[25] = 0;
int pos = (int)all.find('*');
int ans = -1;
for (limitDepth = 0; limitDepth <= 15; limitDepth++) // ★ 迭代加深
if (dfs(0, pos)) { ans = limitDepth; break; }
cout << ans << "\n";
}
if (argc > 1 && string(argv[1]) == "count") cerr << "nodes=" << nodes << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

估价函数必须是下界(估出来的不能比真实的多), 「已走 + h > 上限 就回头」才不会误伤正确答案。空位算进去之后 h 可能比真实步数大 1 —— 于是它会把正解那条路也剪掉

★ 官方样例挡住了它,而它比正解还快
正确答案 高估版
官方样例 ① 7 8 ← 挡住了
官方样例 ② -1 -1
节点数(两组合计) 26 629 7 221 ← 比正解少

剪过头的程序总是更快 —— 这正是第 16 章第 8 步说的那件事, 也是同一天 P1120 那条的另一种形态:那道题的剪过头是 WA 或 TLE, 这道题是答案偏大(搜不到就往下一个深度试,试到 15 还找不到就报 -1)。

4★★★ 而这里有一处反直觉:起点上几乎不高估,答案却全错

「高估」这件事能直接量:拿 IDA* 算出每个局面的真实最优步数 d, 再看两个估价函数在起点上估出来多少。400 个局面:

个数
正确的 h 在起点上 > d 0 ← 必须是 0,否则地基就塌了
错法的 h' 在起点上 > d 21 / 400(5%)
h' 严格大于 h 339 / 400

起点上只有 5% 高估。可它的答案错了多少?

生成器(倒着走 k 步,各 400 个局面) 答案和正解不同
k = 6 363 / 400
k = 9 400 / 400
k = 12 392 / 400
k = 15 400 / 400
★ 关键的一步

估价函数的「不许高估」必须在搜索树的每一个结点上成立,不只是在起点上。

起点上 h' 只有 5% 的时候偏大,可搜索会走到成千上万个中间局面, 只要其中任何一个上面 h' 偏大,通向正解的那条路就被剪掉了。 ⇒ 于是「起点几乎不高估」和「答案几乎全错」同时成立,一点也不矛盾。

★ 这是本书「触发条件要量、不要推」那条的一个新形态: 这次不是触发条件猜错了,而是量错了地方 —— 量了起点,而事情发生在树里。

5⚠ 生成器:「倒着随机走 k 步」不等于「距离 k」(又一次)

骑士的走法可逆,所以从目标棋盘倒着随机走 k 步造出来的局面一定解得开:

p2324Gen.cpp数据生成器
// P2324 对拍生成器:`./p2324Gen <seed> [倒着走几步] [组数]`
//
// 造法:**从目标棋盘倒着随机走 k 步**。骑士的走法是可逆的(空位挪过去再挪回来),
// 所以这样造出来的局面**一定在 k 步以内解得开**,天然是合法输入。
//
// ⚠⚠ 而这里有一个[第 15 章 P1379](/sol/p1379/) 已经踩过的坑,这一页又量了一遍:
// **「倒着走 k 步」不等于「距离 k」** —— 随机游走会绕回来。
// 页面第 ⑤ 步实测:倒着走 15 步造出来的局面,**真实最优步数平均只有 8.65 步**(P1379)
// 这道题上是另一个数,但方向一样。
// ⇒ 想要「难的数据」,光把 k 调大没用,得**把造出来的局面按真实距离筛一遍**。
#include <bits/stdc++.h>
using namespace std;
static const char* TARGET = "1111101111" "00*11" "00001" "00000";
static const int DX[8] = {1, 1, -1, -1, 2, 2, -2, -2};
static const int DY[8] = {2, -2, 2, -2, 1, -1, 1, -1};
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int steps = argc > 2 ? atoi(argv[2]) : 12;
int T = argc > 3 ? atoi(argv[3]) : 1;
mt19937 rng(seed * 2654435761u + 29u);
T = max(1, min(10, T));
printf("%d\n", T);
for (int t = 0; t < T; t++) {
string s = TARGET;
int pos = (int)s.find('*');
for (int i = 0; i < steps; i++) {
int cand[8], c = 0, x = pos / 5, y = pos % 5;
for (int d = 0; d < 8; d++) {
int nx = x + DX[d], ny = y + DY[d];
if (nx < 0 || nx >= 5 || ny < 0 || ny >= 5) continue;
cand[c++] = nx * 5 + ny;
}
int np = cand[rng() % c];
swap(s[pos], s[np]);
pos = np;
}
for (int i = 0; i < 5; i++) printf("%.5s\n", s.c_str() + i * 5);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

可它造出来的局面没有 k 步那么远

倒着走 6 步 9 步 12 步 15 步
真实最优步数(400 个的平均) 3.25 4.57 5.83 6.64
其中最大 6 9 12 13

倒着走 15 步,平均只有 6.64 步远。 随机游走会绕回来 —— 而且这道题的分支多达 8 个, 绕回来的机会比八数码还多。

⇒ 这是第 15 章 P1379 那条的第二个现场(那道题是「倒着走 30 步平均只有 8.65 步远」): 拧一个旋钮之前,先量一下它到底控制着什么。 ★ 想要真正难的数据,光把 k 调大没用 —— 得把造出来的局面按真实距离筛一遍 (第 ② 步那张表里「倒着走 15 步里最深的一个」就是这么挑出来的,13 步)。

6度量程序

p2324Count.cpp度量程序
// P2324 的度量程序 —— 这一页所有数字都出自这一份。
//
// `./p2324Count` 人看的版本
// `./p2324Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用
//
// 五段:
// ① 官方样例:三个版本各输出什么、各走多少节点;
// ② ★ 估价函数值多少钱:IDA* vs 纯迭代加深(后者要带节点上限,不然根本不返回);
// ③ ★★★ **估价函数是不是下界** —— 拿 IDA* 算出的真实最优步数 d 逐个比:
// 正确的 h 有没有一次 > d?错法那个 h' 呢?
// ④ 高估版的抓获率,以及它**比正解还快**(剪过头的程序总是更快);
// ⑤ ★★★ 回到[第 15 章 P1379](/sol/p1379/) 那条:**「倒着随机走 k 步」不等于「距离 k」**,
// 在这道题上再量一次。
#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 char* TARGET = "1111101111" "00*11" "00001" "00000";
static const int DX[8] = {1, 1, -1, -1, 2, 2, -2, -2};
static const int DY[8] = {2, -2, 2, -2, 1, -1, 1, -1};
static char s[26];
static int limitDepth;
static long long nodes, cap_;
static bool blew;
/** mode 0 = 正确的 h(空位不算)/ 1 = 错法(空位也算)/ 2 = 没有估价(纯迭代加深) */
static int hval(int mode) {
if (mode == 2) return 0;
int c = 0;
for (int i = 0; i < 25; i++) {
if (mode == 0 && s[i] == '*') continue;
if (s[i] != TARGET[i]) c++;
}
return c;
}
static bool dfs(int depth, int pos, int mode) {
if (cap_ && nodes >= cap_) { blew = true; return false; }
nodes++;
int h = hval(mode);
if (h == 0 && strcmp(s, TARGET) == 0) return true;
if (depth + h > limitDepth) return false;
if (mode == 2 && depth >= limitDepth) return false;
int x = pos / 5, y = pos % 5;
for (int t = 0; t < 8; t++) {
int nx = x + DX[t], ny = y + DY[t];
if (nx < 0 || nx >= 5 || ny < 0 || ny >= 5) continue;
int np = nx * 5 + ny;
swap(s[pos], s[np]);
bool got = dfs(depth + 1, np, mode);
swap(s[pos], s[np]);
if (got) return true;
if (blew) return false;
}
return false;
}
/** 返回答案(-1 = 15 步内做不到;-2 = 撞上节点上限)。 */
static int solve(const string& board, int mode, long long& outNodes, long long cap = 0) {
memcpy(s, board.c_str(), 25);
s[25] = 0;
int pos = (int)board.find('*');
nodes = 0; cap_ = cap; blew = false;
int ans = -1;
for (limitDepth = 0; limitDepth <= 15; limitDepth++) {
if (dfs(0, pos, mode)) { ans = limitDepth; break; }
if (blew) { outNodes = nodes; return -2; }
}
outNodes = nodes;
return ans;
}
/** 直接算某个局面的 h(不搜索) */
static int hOf(const string& board, int mode) {
memcpy(s, board.c_str(), 25);
s[25] = 0;
return hval(mode);
}
/** 和 p2324Gen.cpp 逐字一致:从目标态倒着随机走 steps 步 */
static string gen(int seed, int steps) {
mt19937 rng((unsigned)seed * 2654435761u + 29u);
string t = TARGET;
int pos = (int)t.find('*');
for (int i = 0; i < steps; i++) {
int cand[8], c = 0, x = pos / 5, y = pos % 5;
for (int d = 0; d < 8; d++) {
int nx = x + DX[d], ny = y + DY[d];
if (nx < 0 || nx >= 5 || ny < 0 || ny >= 5) continue;
cand[c++] = nx * 5 + ny;
}
int np = cand[rng() % c];
swap(t[pos], t[np]);
pos = np;
}
return t;
}
static const string S1 = "10110" "01*11" "10111" "01001" "00000";
static const string S2 = "01011" "110*1" "01110" "01010" "00100";
int main(int argc, char** argv) {
CSV = (argc > 1 && string(argv[1]) == "csv");
const long long CAP = 50000000;
/* ① 官方样例 */
{
long long n1, n2, n3, n4;
int a1 = solve(S1, 0, n1), a2 = solve(S2, 0, n2);
int b1 = solve(S1, 1, n3), b2 = solve(S2, 1, n4);
if (!CSV) printf("① 官方样例:正解 %d / %d(%lld + %lld 个节点)|"
"高估版 %d / %d(%lld + %lld 个节点 —— ★ 比正解还少)\n",
a1, a2, n1, n2, b1, b2, n3, n4);
row("sample", {a1, a2, n1, n2, b1, b2, n3, n4});
}
/* ② 估价函数值多少钱:IDA* vs 纯迭代加深(带节点上限) */
{
if (!CSV) printf("\n② 估价函数值多少钱(节点上限 %lld)\n", CAP);
vector<long long> out;
// ⚠ 草稿这里用「倒着走 k 步」造的局面,结果四档真实步数只有 4~6 步 ——
// 纯迭代加深几百个节点就出来了,**那张表量不到任何东西**。
// 真正要命的是**答案是 -1 的局面**:它必须把 15 层整棵树搜完才敢下结论。
const char* NM[3] = {"官方样例 ①(答案 7)", "官方样例 ②(答案 -1)", "倒着走 15 步里最深的一个"};
string deep = gen(1, 15);
int bestD = -1;
for (int seed = 1; seed <= 400; seed++) {
string b = gen(seed, 15);
long long nn;
int d = solve(b, 0, nn);
if (d > bestD) { bestD = d; deep = b; }
}
string BOARDS[3] = {S1, S2, deep};
for (int i = 0; i < 3; i++) {
long long na, nb;
int a = solve(BOARDS[i], 0, na);
int r = solve(BOARDS[i], 2, nb, CAP);
out.push_back(a); out.push_back(na); out.push_back(r < -1 ? -1 : nb);
if (!CSV) printf(" %-24s:真实 %2d 步|IDA* %lld 个节点|纯迭代加深 %s\n",
NM[i], a, na, r < -1 ? "撞上限 5000 万,跑不完" : (to_string(nb) + " 个节点").c_str());
}
row("heur", out);
}
/* ③ ★★★ 估价函数是不是下界 */
{
int badOk = 0, badOver = 0, overStrict = 0, cases = 0;
for (int seed = 1; seed <= 400; seed++) {
string b = gen(seed, 12);
long long nn;
int d = solve(b, 0, nn); // 真实最优步数
if (d < 0) continue;
cases++;
if (hOf(b, 0) > d) badOk++; // 正确的 h 高估了?(应该永远不会)
if (hOf(b, 1) > d) badOver++; // 错法的 h 高估了?
if (hOf(b, 1) > hOf(b, 0)) overStrict++;
}
if (!CSV) printf("\n③ %d 个局面上,估价函数和真实最优步数 d 的关系:\n"
" 正确的 h > d 的有 %d 个(必须是 0,否则 IDA* 的地基就塌了)\n"
" 错法的 h' > d 的有 %d 个|h' 严格大于 h 的有 %d 个\n",
cases, badOk, badOver, overStrict);
row("admissible", {cases, badOk, badOver, overStrict});
}
/* ④ 高估版的抓获率 + 它比正解快多少 */
{
if (!CSV) printf("\n④ 高估版:400 个局面里答案不同的有几个(按倒着走的步数分档)\n");
vector<long long> out;
for (int k : {6, 9, 12, 15}) {
int diff = 0;
long long totOk = 0, totBad = 0;
for (int seed = 1; seed <= 400; seed++) {
string b = gen(seed, k);
long long na, nb;
int a = solve(b, 0, na), c = solve(b, 1, nb);
if (a != c) diff++;
totOk += na; totBad += nb;
}
out.push_back(diff);
if (!CSV) printf(" 倒着走 %2d 步:答案不同 %3d / 400|节点数合计 正解 %lld、高估版 %lld(%.2f 倍)\n",
k, diff, totOk, totBad, totBad * 1.0 / totOk);
}
row("overCatch", out);
}
/* ⑤ ★★★ 「倒着走 k 步」不等于「距离 k」 */
{
if (!CSV) printf("\n⑤ 「倒着随机走 k 步」造出来的局面,真实最优步数是多少(每档 400 个,×100)\n");
vector<long long> out;
for (int k : {6, 9, 12, 15}) {
long long sum = 0;
int cnt = 0, mx = 0;
for (int seed = 1; seed <= 400; seed++) {
string b = gen(seed, k);
long long nn;
int d = solve(b, 0, nn);
if (d < 0) continue;
sum += d; cnt++; mx = max(mx, d);
}
out.push_back(sum * 100 / cnt); out.push_back(mx);
if (!CSV) printf(" 倒着走 %2d 步:真实最优平均 %.2f 步,最大 %d 步\n", k, sum * 1.0 / cnt, mx);
}
row("walkDist", out);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
⚠ 写这个度量程序时又踩了一脚(和 P1002 那次同一类)

第 ② 步那张表,草稿用的是「倒着走 4 / 6 / 8 / 10 步」造的局面 —— 结果四档的真实步数只有 4 ~ 6 步,纯迭代加深几百个节点就出来了, 那张表量不到任何东西(而它看起来完全正常)。

⇒ 换成「答案是 -1 的那一组」才量到真东西:那一组必须把 15 层整棵树搜完。 造对照数据之前,先确认它没有把要观察的现象本身消掉。

7一页纸

关键的一步 迭代加深(深度上限题面给了:15)+ 估价函数 h = 不在位的骑士数
哪一版能 AC p2324.cpp(IDA*);只有迭代加深那版在样例第二组就跑不出来
估价函数为什么成立 一步只能让一个骑士归位 ⇒ 至少还要 h
最容易写错的一处 h空位那一格也算进去 ⇒ 高估 ⇒ 剪掉正解(答案偏大,不是偏小)
这一页的主线 「不许高估」要在每个结点上成立,不只是起点
起点上只有 5% 高估,答案却错了 400 / 400
生成器那条 「倒着随机走 15 步」造出来的局面平均只有 6.64 步远 ——
P1379 那条的第二个现场