信奥赛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 表中取出 f(n) 最小的节点作为当前节点
- 若当前节点为目标节点,输出结果并结束
- 扩展当前节点的所有合法后继节点
- 对每个后继节点计算 g、h、f,若不在 OPEN/CLOSED 表中则加入 OPEN 表
优点:保证找到最优解;缺点:需要存储大量节点,空间开销大。
2.2 IDA* 算法(结合DFS+迭代加深)
IDA*(Iterative Deepening A*)是 A* 与迭代加深 DFS 的有机结合。它通过逐步加深搜索深度限制的方式,在每次 DFS 中使用启发函数进行剪枝,既保留了 DFS 的低内存占用优势,又继承了 A* 的启发引导能力。
IDA 核心机制*:设当前深度为 depth,启发估计值为 h,当前深度限制为 maxd。若 depth + h > maxd,则直接剪枝——因为即使在理想情况下,剩余所需步数 h 加上已经走过的 depth 步,也超过了允许的最大深度,当前路径不可能在限制内到达目标。
IDA 算法流程:*
2.3 A vs 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
T≤10),表示一共有
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;
}


![【题解】[COCI 2025/2026 #6] 滑雪 / Skijanje(李超树 0 基础友好喵)-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260826083930-6a8ea642697bc-220x25.png)