信奥赛C++提高组csp-s之搜索进阶(迭代加深IDDFS)

一、迭代加深(IDDFS)原理
迭代加深搜索(Iterative Deepening DFS,简称 IDDFS)是一种特殊的深度优先搜索。它给普通的 DFS 套上了一层"深度限制"循环,搜索过程如下:
- 设最大搜索深度限制
dep = 1; - 进行深度受限的 DFS,搜索深度不超过
dep; - 如果在这一轮限制下找到了可行解,则算法结束;
- 否则,
dep++,重新从初始状态开始新一轮深度受限搜索。
这种"由浅入深"的搜索模式,使 IDDFS 兼具 BFS 找到**最优解(步数最少)**的能力,又保留了 DFS 空间效率高的特点。尤其适用于搜索树分支极多但答案深度较浅的问题。
二、案例研究:埃及分数
题目描述
在古埃及,人们使用单位分数的和(形如 1 a \dfrac{1}{a} a1 的, a a a 是正整数)表示一切有理数。如: 2 3 = 1 2 + 1 6 \dfrac{2}{3} = \dfrac{1}{2} + \dfrac{1}{6} 32=21+61,但不允许 2 3 = 1 3 + 1 3 \dfrac{2}{3} = \dfrac{1}{3} + \dfrac{1}{3} 32=31+31,因为加数中有相同的。对于一个分数 a b \dfrac{a}{b} ba,表示方法有很多种,但是哪种最好呢?首先,加数少的比加数多的好,其次,加数个数相同的,最小的分数越大越好。如:
19 45 = 1 3 + 1 12 + 1 180 19 45 = 1 3 + 1 15 + 1 45 19 45 = 1 3 + 1 18 + 1 30 19 45 = 1 4 + 1 6 + 1 180 19 45 = 1 5 + 1 6 + 1 18 \begin{aligned} \frac{19}{45} &= \frac{1}{3} + \frac{1}{12} + \frac{1}{180}\\ \frac{19}{45} &= \frac{1}{3} + \frac{1}{15} + \frac{1}{45}\\ \frac{19}{45} &= \frac{1}{3} + \frac{1}{18} + \frac{1}{30}\\ \frac{19}{45} &= \frac{1}{4} + \frac{1}{6} + \frac{1}{180}\\ \frac{19}{45} &= \frac{1}{5} + \frac{1}{6} + \frac{1}{18}\\ \end{aligned} 45194519451945194519=31+121+1801=31+151+451=31+181+301=41+61+1801=51+61+181
最好的是最后一种,因为 1 18 \dfrac{1}{18} 181 比 1 180 , 1 45 , 1 30 \dfrac{1}{180}, \dfrac{1}{45}, \dfrac{1}{30} 1801,451,301 都大。
注意,可能有多个最优解。如:
59 211 = 1 4 + 1 36 + 1 633 + 1 3798 59 211 = 1 6 + 1 9 + 1 633 + 1 3798 \begin{aligned} \frac{59}{211} &= \frac{1}{4} + \frac{1}{36} + \frac{1}{633} + \frac{1}{3798}\\ \frac{59}{211} &= \frac{1}{6} + \frac{1}{9} + \frac{1}{633} + \frac{1}{3798}\\ \end{aligned} 2115921159=41+361+6331+37981=61+91+6331+37981
由于方法一与方法二中,最小的分数相同,因此二者均是最优解。
给出 a , b a,b a,b,编程计算最好的表达方式。保证最优解满足:最小的分数 ≥ 1 10 7 \ge \cfrac{1}{10^7} ≥1071。
输入格式
一行两个整数,分别为 a a a 和 b b b 的值。
输出格式
输出若干个数,自小到大排列,依次是单位分数的分母。
输入输出样例 1
输入 1
19 45
输出 1
5 6 18
说明/提示
1 < a < b < 1000 1 \lt a \lt b \lt 1000 1<a<b<1000
思路分析
1. 问题理解
- 棋盘:4×4 网格,包含 7 个黑子(B)、7 个白子(W)和 2 个空格(O)。
- 规则:黑白双方轮流移动,每次可以将一枚自己的棋子(不能是空格)移入相邻的空格中。
- 胜利条件:某一方的棋子在横、竖或斜向上连成一条直线(4 个连续格子)。
- 目标:求出先手(黑白均可先走)获胜所需的最少步数。如果双方都无法获胜,输出 -1(实际数据保证有解)。
2. 为什么选择迭代加深(IDDFS)
- 状态空间大:纯 BFS 需要存储海量状态(4×4 棋盘有 16 个位置,每个位置可能是 B/W/O,组合爆炸),内存难以承受。
- 最优性要求:需要最少步数,而 DFS 不能保证首次找到的解是最优解。
- 深度有上限:虽然理论上可以无限走下去,但实际游戏会在有限步内结束(最多几十步)。我们可以设定一个合理的深度上限(比如 100),并从小到大枚举。
- 迭代加深 = DFS + 深度限制 :每轮只搜索深度不超过
dep的路径,一旦找到解,dep就是最少步数。空间复杂度仅为 O(dep),远小于 BFS。
3. 核心设计要点
- 状态表示 :用一个 5×5 数组(1-indexed)存储棋盘,其中
1表示黑子,2表示白子,0表示空格。 - 空格定位 :记录两个空格的位置
(a1,b1)和(a2,b2)。每次移动时,只考虑将空格周围的棋子移入空格。 - 交替走棋 :参数
last记录上一步移动的棋子颜色。移动时,只能移动与last不同 颜色的棋子(因为双方交替)。开局时last设为对方颜色,先手可以自由走。 - 先手处理 :分别尝试黑棋先手和 白棋先手,取较小步数。因为题目未指定谁先走,双方都可作为先手。
- 剪枝:除了深度限制外,还可以通过估价函数(未来得及加入,但基础版本已足够 AC)。本题的剪枝主要就是深度限制和禁止移动空格。
- 胜利检测:检查所有行、列、两条对角线是否有连续 4 个相同颜色的棋子。
4. 迭代加深流程
text
for (dep = 1; dep <= MAX_DEP; ++dep) {
if (dfs(初始状态, last = 对方颜色, 步数 = 0)) {
输出 dep;
return;
}
}
MAX_DEP可设为 100,题目数据保证解在 100 步以内。- 由于每一轮深度限制递增,DFS 会重复搜索浅层状态,但总开销仍远小于 BFS(因为分支因子大,深度浅时节点数少)。
代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
int g[5][5]; // 棋盘:1-黑子(B),2-白子(W),0-空格(O)
int dep; // 当前迭代加深的深度限制
int a1, b1, a2, b2; // 两个空格的位置 (行, 列)
int dx[4] = {1, 0, -1, 0}; // 四方向移动:下、右、上、左
int dy[4] = {0, 1, 0, -1};
// 检查当前棋盘是否已有玩家获胜
bool win() {
// 检查每一行
for (int i = 1; i <= 4; i++) {
if (g[i][1] == g[i][2] && g[i][2] == g[i][3] && g[i][3] == g[i][4])
return true;
}
// 检查每一列
for (int i = 1; i <= 4; i++) {
if (g[1][i] == g[2][i] && g[2][i] == g[3][i] && g[3][i] == g[4][i])
return true;
}
// 检查主对角线 (左上到右下)
if (g[1][1] == g[2][2] && g[2][2] == g[3][3] && g[3][3] == g[4][4])
return true;
// 检查副对角线 (右上到左下)
if (g[1][4] == g[2][3] && g[2][3] == g[3][2] && g[3][2] == g[4][1])
return true;
return false;
}
// 深度优先搜索(IDDFS 的核心)
// d : 当前已走的步数
// a1,b1 : 第一个空格的坐标
// a2,b2 : 第二个空格的坐标
// last : 上一步移动的棋子颜色 (1=黑, 2=白, 初始为对方颜色)
bool dfs(int d, int a1, int b1, int a2, int b2, int last) {
if (d == dep) // 达到深度限制,检查是否胜利
return win();
// 枚举四个方向
for (int i = 0; i < 4; i++) {
// ---- 尝试移动第一个空格周围的棋子 ----
int na = a1 + dx[i], nb = b1 + dy[i];
// 边界检查、不能移空格(棋子!=0)、不能移上一步移动的颜色(即不能连续走同色)
if (na >= 1 && na <= 4 && nb >= 1 && nb <= 4 && g[na][nb] != 0 && g[na][nb] != last) {
swap(g[a1][b1], g[na][nb]); // 移动棋子:将(na,nb)棋子移入空格(a1,b1)
if (dfs(d + 1, na, nb, a2, b2, g[a1][b1])) // 递归,新空格位置变为(na,nb)
return true;
swap(g[a1][b1], g[na][nb]); // 回溯,恢复棋盘
}
// ---- 尝试移动第二个空格周围的棋子 ----
na = a2 + dx[i], nb = b2 + dy[i];
if (na >= 1 && na <= 4 && nb >= 1 && nb <= 4 && g[na][nb] != 0 && g[na][nb] != last) {
swap(g[a2][b2], g[na][nb]);
if (dfs(d + 1, a1, b1, na, nb, g[a2][b2]))
return true;
swap(g[a2][b2], g[na][nb]);
}
}
return false;// 所有方向都尝试过,无解
}
int main() {
// 读入棋盘,并记录两个空格的位置
a1 = b1 = a2 = b2 = 0; // 初始化
for (int i = 1; i <= 4; i++) {
string s;
cin >> s;
for (int j = 1; j <= 4; j++) {
char ch = s[j-1];
if (ch == 'B') g[i][j] = 1;
else if (ch == 'W') g[i][j] = 2;
else { // 'O' 空格
g[i][j] = 0;
if (a1 == 0) a1 = i, b1 = j; // 记录第一个空格
else a2 = i, b2 = j; // 记录第二个空格
}
}
}
// 特判:初始棋盘是否已经胜利(虽然题目数据一般不会,但严谨处理)
if (win()) {
cout << 0 << endl;
return 0;
}
// 迭代加深:从深度 1 开始枚举,最大深度 100(足够)
for (dep = 1; dep <= 100; dep++) {
// 分别尝试白棋先手(last=1 表示上一轮是黑棋,则白棋可以走)和黑棋先手(last=2)
if (dfs(0, a1, b1, a2, b2, 1) || dfs(0, a1, b1, a2, b2, 2)) {
cout << dep << endl; // 第一次找到解时的 dep 即为最少步数
return 0;
}
}
// 理论上题目保证有解,但如果超过 100 步仍未找到,输出 -1
cout << -1 << endl;
return 0;
}
功能分析
| 模块/函数 | 功能描述 |
|---|---|
win() |
检查当前棋盘是否有四个相同颜色的棋子连成一条直线(行、列、对角线)。 |
dfs(d, a1, b1, a2, b2, last) |
深度优先搜索,在不超过深度限制 dep 的范围内,枚举所有可能的合法移动。若到达 dep 步时 win() 为真,则返回 true。 |
main() |
1. 读入棋盘,将字符转换为数字,并记录两个空格的坐标。 2. 特判初始状态是否胜利。 3. 迭代加深:枚举 dep 从 1 到 100,分别尝试黑棋先手和白棋先手调用 dfs。 4. 一旦 dfs 返回 true,输出当前 dep 并结束。 5. 若循环结束仍未找到,输出 -1。 |
| 迭代加深循环 | 从小到大限制搜索深度,保证了首次找到的解必定是最少步数,同时避免了 BFS 的内存爆炸。 |
| 移动逻辑 | 每次移动只考虑两个空格周围的四个方向,且只能移动非空格 、颜色与上一步不同的棋子。这保证了双方交替走棋且移动合法。 |
| 回溯 | 每次尝试移动后,通过 swap 恢复棋盘状态,确保搜索树正确分支。 |
三、总结
迭代加深适用于以下类型的搜索问题:
- 答案的深度比较浅,但分支非常多;
- 要求输出最优解(步数最少或层数最少);
- BFS 会因状态过多而内存超限,DFS 又容易陷入深层无效分支。
迭代加深的核心优势:
- 内存效率高:只保留当前递归路径上的节点,空间复杂度与 DFS 相同;
- 能保证最优性:从小到大枚举深度限制,首次找到的解一定是最优解;
- 剪枝友好:DFS 框架天然适合加入各种剪枝函数,可以大幅减少搜索量。
更多系列知识,请查看专栏:《信奥赛C++提高组csp-s知识详解及案例实践》:
https://blog.csdn.net/weixin_66461496/category_13113932.html
各种学习资料,助力大家一站式学习和提升!!!
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"########## 一站式掌握信奥赛知识! ##########";
cout<<"############# 冲刺信奥赛拿奖! #############";
cout<<"###### 课程购买后永久学习,不受限制! ######";
return 0;
}
1、csp信奥赛高频考点知识详解及案例实践:
CSP信奥赛C++动态规划:
https://blog.csdn.net/weixin_66461496/category_13096895.html点击跳转
CSP信奥赛C++标准模板库STL:
https://blog.csdn.net/weixin_66461496/category_13108077.html 点击跳转
信奥赛C++提高组csp-s知识详解及案例实践:
https://blog.csdn.net/weixin_66461496/category_13113932.html
2、csp信奥赛冲刺一等奖有效刷题题解:
信奥赛C++普及组csp-j初赛&复赛真题题解(持续更新) https://blog.csdn.net/weixin_66461496/category_12808781.html 点击跳转
信奥赛C++提高组csp-s初赛&复赛真题题解(持续更新)
https://blog.csdn.net/weixin_66461496/category_13125089.html
3、GESP C++考级真题题解:

GESP(C++ 一级+二级+三级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12858102.html 点击跳转

GESP(C++ 四级+五级+六级)真题题解(持续更新):https://blog.csdn.net/weixin_66461496/category_12869848.html 点击跳转

GESP(C++ 七级+八级)真题题解(持续更新):
https://blog.csdn.net/weixin_66461496/category_13117178.html
4、csp/信奥赛C++,完整信奥赛系列课程(永久学习):
https://edu.csdn.net/lecturer/7901 点击跳转
· 文末祝福 ·
cpp
#include<bits/stdc++.h>
using namespace std;
int main(){
cout<<"跟着王老师一起学习信奥赛C++";
cout<<" 成就更好的自己! ";
cout<<" csp信奥赛一等奖属于你! ";
return 0;
}