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

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

一、迭代加深(IDDFS)原理

迭代加深搜索(Iterative Deepening DFS,简称 IDDFS)是一种特殊的深度优先搜索。它给普通的 DFS 套上了一层"深度限制"循环,搜索过程如下:

  1. 设最大搜索深度限制 dep = 1
  2. 进行深度受限的 DFS,搜索深度不超过 dep
  3. 如果在这一轮限制下找到了可行解,则算法结束;
  4. 否则,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 恢复棋盘状态,确保搜索树正确分支。

三、总结

迭代加深适用于以下类型的搜索问题:
  1. 答案的深度比较浅,但分支非常多;
  2. 要求输出最优解(步数最少或层数最少)
  3. 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;
}
相关推荐
liulilittle1 小时前
我从 BBRv1 到 KCC 的思考
网络·c++·tcp/ip·计算机网络·tcp·bbr·通信
落羽的落羽1 小时前
【项目】JsonRpc框架——开发实现1(细节功能、字段定义、抽象层、具象层)
linux·服务器·网络·c++·人工智能·算法·机器学习
handler012 小时前
【算法】并查集(普通/扩展/带权)模板与例题
数据结构·c++·笔记·算法·c·图论·查并集
繁星蓝雨2 小时前
C++中对比pragma once和ifndef的使用区别
开发语言·c++·ifndef·头文件·pragma once
.千余2 小时前
【C++】C++手写Vector容器:从底层源码模拟实现
开发语言·c++·经验分享·笔记·学习
a诠释淡然3 小时前
C++ vs Rust:哪个更适合你的下一个项目?
开发语言·c++·rust
小小de风呀3 小时前
de风——【从零开始学C++】(十二):stack和queue的基本使用和模拟实现
开发语言·c++
汉克老师3 小时前
GESP6级C++考试语法知识(五十三、动态规划----背包问题(六、分组背包)
c++·动态规划·背包问题·gesp6级·gesp六级·分组背
雪度娃娃3 小时前
转向现代C++——保证const成员函数的线程安全性
开发语言·c++