欢迎光临
我们一直在努力

信奥赛C++提高组csp-s之搜索进阶(启发式搜索)

信奥赛C++提高组csp-s之搜索进阶(启发式搜索)

在这里插入图片描述

一、启发式搜索算法思想

1.1 什么是启发式搜索

启发式搜索是一种利用启发函数(估价函数) 来指导搜索方向的高级搜索算法。相比盲目搜索(如DFS和BFS)在状态空间中无差别地探索所有可能路径,启发式搜索能够通过评估当前状态与目标状态之间的“接近程度”,优先探索最有希望的路径,从而大幅减少无效搜索。

举个直观的例子:想象你在操场上被要求走向国旗。如果你是盲人(盲目搜索),你可能需要走遍整个操场才能找到国旗;但如果你能看到国旗(启发式搜索),你会直接朝着国旗的方向走去,永远不会向相反方向走。这就是启发式搜索的核心思想——用“经验”指导搜索方向。

1.2 估价函数 f(n) = g(n) + h(n)

启发式搜索的核心是估价函数,通常表示为:

f(n) = g(n) + h(n)

  • g(n):从初始状态到当前状态 n 的实际代价(如已走的步数)
  • h(n):从当前状态 n 到目标状态的估计代价(启发函数)
  • f(n):通过当前状态 n 到达目标状态的总代价估计

算法每次选择 f(n) 值最小的节点进行扩展,从而高效地逼近最优解。

1.3 启发函数 h(n) 的设计原则

启发函数的设计是启发式搜索成败的关键,需要满足以下重要性质:

性质含义重要性
可采纳性 h(n) ≤ 实际剩余代价(不高估) 保证找到最优解
一致性 三角不等式:h(n) ≤ cost(n→m) + h(m) 保证算法效率
信息性 h(n) 越大,信息越丰富,搜索节点越少 影响搜索效率

当 h(n) = 0 时,启发式搜索退化为 Dijkstra 算法;当 h(n) 接近于真实代价时,搜索效率最高。


二、A* 算法与 IDA* 算法详解

2.1 A* 算法(结合BFS)

A* 算法将启发式搜索与 BFS 结合,使用优先队列(最小堆)来管理待扩展节点,每次扩展 f(n) 最小的节点。

A 算法流程:*

  • 初始化:将初始节点加入 OPEN 表(优先队列),g(start)=0,计算 f(start)
  • 重复以下步骤直到 OPEN 表为空:
    • 从 OPEN 表中取出 f(n) 最小的节点作为当前节点
    • 若当前节点为目标节点,输出结果并结束
    • 扩展当前节点的所有合法后继节点
    • 对每个后继节点计算 g、h、f,若不在 OPEN/CLOSED 表中则加入 OPEN 表
  • 若 OPEN 表为空,则无解
  • 优点:保证找到最优解;缺点:需要存储大量节点,空间开销大。

    2.2 IDA* 算法(结合DFS+迭代加深)

    IDA*(Iterative Deepening A*)是 A* 与迭代加深 DFS 的有机结合。它通过逐步加深搜索深度限制的方式,在每次 DFS 中使用启发函数进行剪枝,既保留了 DFS 的低内存占用优势,又继承了 A* 的启发引导能力。

    IDA 核心机制*:设当前深度为 depth,启发估计值为 h,当前深度限制为 maxd。若 depth + h > maxd,则直接剪枝——因为即使在理想情况下,剩余所需步数 h 加上已经走过的 depth 步,也超过了允许的最大深度,当前路径不可能在限制内到达目标。

    IDA 算法流程:*

  • 设置深度限制 maxd(从 0 开始递增)
  • 对每个 maxd,执行深度限制为 maxd 的 DFS
  • 在 DFS 中,若 depth + h() > maxd,立即返回(剪枝)
  • 若在某个 maxd 下找到目标,输出结果
  • 若 maxd 超过预设上限(如题目给定的 15 步),输出 -1
  • 2.3 A vs IDA 对比:**
    特性A*IDA*
    数据结构 优先队列(BFS) DFS + 递归栈
    内存占用 大(需存储所有节点) 小(只需递归栈)
    最优解保证 是(h 可纳时) 是(迭代加深保证)
    适用场景 状态空间中等 状态空间巨大、深度有限

    三、案例研究:骑士精神

    题目描述

    在一个

    5

    ×

    5

    5\\times 5

    5×5 的棋盘上有

    12

    12

    12 个白色的骑士和

    12

    12

    12 个黑色的骑士,且有一个空位。在任何时候一个骑士都能按照骑士的走法(它可以走到和他横坐标相差为

    1

    1

    1,纵坐标相差为

    2

    2

    2 或者横坐标相差为

    2

    2

    2,纵坐标相差为

    1

    1

    1 的格子)移动到空位上。

    给定一个初始的棋盘,怎样才能经过移动变成如下目标棋盘 在这里插入图片描述

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

    输入格式

    第一行有一个正整数

    T

    T

    T

    T

    10

    T \\le 10

    T10),表示一共有

    T

    T

    T 组数据。

    接下来有

    T

    T

    T

    5

    ×

    5

    5 \\times 5

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

    输出格式

    对于每组数据都输出一行。如果能在

    15

    15

    15 步以内(包括

    15

    15

    15 步)到达目标状态,则输出步数,否则输出 -1。

    输入输出样例 #1
    输入 #1

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

    输出 #1

    7
    -1

    说明/提示

    样例中第二个数据的初始情况对应该图: 在这里插入图片描述

    思路分析

    题目要求:给定一个5×5棋盘,上有12个白骑士(0)、12个黑骑士(1)和一个空格(*),每次可将一个骑士按“日”字形移动到空格位置。问最少多少步能变成目标棋盘,若步数>15则输出-1。

    解题思路:IDA(迭代加深A)**

    • 状态空间极大(5×5棋盘,但骑士颜色固定数量),但深度限制很小(≤15),适合迭代加深。
    • 迭代加深:从小到大尝试最大深度 maxd(0~15),在深度限制内进行DFS,如果找到解则输出当前maxd。
    • 启发函数:计算当前棋盘与目标棋盘不同位置的数量(包括空格)。因为每次移动最多只能将一个棋子放到正确位置,但空格移动也可能改变差异数,实际每次移动最多减少1个差异(有时甚至会增多)。严格来说,启发值 h() 是剩余最少需要移动的步数下限,满足 h() ≤ 实际剩余步数。经典写法:d + h() – 1 > maxd 剪枝(因为若当前差异数为h,至少需要h-1步才能完成?这里要仔细分析:实际上移动一次最多减少2个差异(交换空格和骑士可能同时修正两个位置),所以更保守的剪枝是 d + h() > maxd 或 d + h() – 1 > maxd。经验上 d + h() – 1 > maxd 能AC)。
    • 移动规则:8个方向,交换空格与骑士位置。
    • 剪枝:深度+启发值超过maxd则回溯。

    代码实现

    #include<bits/stdc++.h>
    using namespace std;

    // 目标棋盘布局(固定)
    const char goal[5][5] = {
    {'1','1','1','1','1'},
    {'0','1','1','1','1'},
    {'0','0','*','1','1'},
    {'0','0','0','0','1'},
    {'0','0','0','0','0'}
    };

    // 骑士的8个移动方向(行增量,列增量)
    const int dx[8] = {2, 1, 1, 2, 2, 1, 1, 2};
    const int dy[8] = {1, 2, 2, 1, 1, 2, 2, 1};

    int T; // 测试数据组数
    char a[5][5]; // 当前棋盘

    // 启发函数:返回当前棋盘与目标棋盘不同位置的个数(包括空格)
    int h() {
    int cnt = 0;
    for(int i = 0; i < 5; ++i)
    for(int j = 0; j < 5; ++j)
    if(a[i][j] != goal[i][j]) cnt++;
    return cnt;
    }

    // 深度优先搜索(IDA*核心)
    // d: 当前已走步数
    // x, y: 空格当前坐标
    // maxd: 允许的最大深度
    bool dfs(int d, int x, int y, int maxd) {
    int hv = h(); // 当前差异数
    if(d + hv 1 > maxd) return false; // 剪枝:剩余步数不足以修正所有差异
    if(hv == 0) return true; // 到达目标状态
    if(d == maxd) return false; // 已达深度上限但未成功

    // 枚举8个移动方向
    for(int i = 0; i < 8; ++i) {
    int nx = x + dx[i];
    int ny = y + dy[i];
    if(nx < 0 || nx >= 5 || ny < 0 || ny >= 5) continue; // 越界跳过
    // 移动:交换空格与骑士
    swap(a[x][y], a[nx][ny]);
    if(dfs(d+1, nx, ny, maxd)) return true; // 递归搜索
    swap(a[x][y], a[nx][ny]); // 回溯
    }
    return false;
    }

    int main() {
    ios::sync_with_stdio(false); // 加速输入输出
    cin.tie(0);

    cin >> T;
    while(T) {
    // 读入5行棋盘(每行字符串)
    for(int i = 0; i < 5; ++i) {
    string s;
    cin >> s;
    for(int j = 0; j < 5; ++j)
    a[i][j] = s[j];
    }
    // 找到空格的初始位置
    int sx, sy;
    for(int i = 0; i < 5; ++i)
    for(int j = 0; j < 5; ++j)
    if(a[i][j] == '*') { sx = i; sy = j; break; }

    int ans = 1; // 默认无解
    // 迭代加深:尝试深度上限从0到15
    for(int maxd = 0; maxd <= 15; ++maxd) {
    if(dfs(0, sx, sy, maxd)) {
    ans = maxd; // 找到最小步数
    break;
    }
    }
    cout << ans << '\\n'; // 输出结果
    }
    return 0;
    }


    功能分析

  • 目标状态定义 常量goal[5][5]存储了题目要求的目标棋盘布局,用于后续比较。

  • 移动方向数组 dx[8]和dy[8]定义了骑士走法的8个偏移量。

  • 启发函数 h() 遍历整个棋盘,统计当前棋盘与目标棋盘对应位置字符不同的格子数(包括空格)。该数值作为剩余步数的乐观估计。

  • DFS搜索函数 dfs()

    • 参数:当前步数d,空格坐标(x,y),最大深度maxd。
    • 首先调用h()得到差异数hv。
    • 剪枝条件:d + hv – 1 > maxd 说明即使最理想情况下(每次移动修正一个差异)也无法在剩余步数内完成,故剪枝。
    • 若hv == 0则当前状态与目标一致,返回成功。
    • 若d == maxd但未成功,返回失败。
    • 枚举8个方向,交换空格与骑士,递归调用dfs,若成功则层层返回。
    • 递归返回后回溯还原棋盘。
  • 主函数流程

    • 读入数据组数T。
    • 对每组数据:读入5行字符串到a数组,定位空格初始位置(sx,sy)。
    • 迭代加深:maxd从0递增到15,调用dfs(0, sx, sy, maxd)。
    • 若某次dfs返回true,则当前maxd即为最小步数,记录并跳出循环。
    • 若循环结束仍未找到,则ans保持-1。
    • 输出ans。

  • 更多系列知识,请查看专栏:《信奥赛C++提高组csp-s知识详解及案例实践》: https://blog.csdn.net/weixin_66461496/category_13113932.html


    各种学习资料,助力大家一站式学习和提升!!!

    #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 点击跳转

    · 文末祝福 ·

    #include<bits/stdc++.h>
    using namespace std;
    int main(){
    cout<<"跟着王老师一起学习信奥赛C++";
    cout<<" 成就更好的自己! ";
    cout<<" csp信奥赛一等奖属于你! ";
    return 0;
    }

    赞(0)
    未经允许不得转载:171主机测评 » 信奥赛C++提高组csp-s之搜索进阶(启发式搜索)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址