搜索与启发式算法专项强化练习 (Search & Heuristics)
“在巨大的搜索树中,每一层剪枝都是对计算冗余的优雅反击。” —— 本专题旨在建立从“暴力 DFS”到“智能启发式搜索”的完整方法论,通过状态空间缩减与估价函数设计,攻克 NP-Hard 问题的近似求解与小规模精确求解。
🪜 练习阶梯与评价标准
| 等级 | 难度目标 | 核心考察点 | 期望达成 |
|---|---|---|---|
| ● Level A | 状态空间缩减 | 搜索顺序优化、可行性/最优性剪枝 | 能够识别并剪掉 90% 以上的冗余分支 |
| ● Level B | 状态建模创新 | 双向搜索 (Meet-in-the-middle)、迭代加深 | 能够处理状态空间达 级别的搜索 |
| ● Level C | 启发式设计 | A* 算法、IDA* 算法、估价函数 设计 | 能够设计出满足“可容性”的强约束估价函数 |
一、 基础剪枝与状态优化 (Level A)
练习 1:数字组合 - DFS 状态缩减
给定 个正整数 ,从中挑选若干数使其和为 ,求方案数。()
Check Solution (C++ Implementation)
解析:
- 状态定义:
dfs(u, current_sum)表示考虑到第 个数,当前和为current_sum。 - 优化策略:
- 可行性剪枝:若
current_sum > M,立即停止。 - 搜索顺序:虽然此题方案数统计受顺序影响较小,但在最优化问题中,从大到小排列能更快触发剪枝。
- 可行性剪枝:若
C++ 实现:
#include <iostream>
#include <algorithm>
using namespace std;
int n, m, a[25], ans;
void dfs(int u, int sum) {
if (sum == m) {
ans++;
return;
}
if (u == n || sum > m) return;
// 选当前数
dfs(u + 1, sum + a[u]);
// 不选当前数
dfs(u + 1, sum);
}
int main() {
cin >> n >> m;
for (int i = 0; i < n; i++) cin >> a[i];
dfs(0, 0);
cout << ans << endl;
}
练习 2:小猫爬山 - 搜索顺序与最优性剪枝
只猫,体重 ,缆车承重 。求最少缆车数。
Check Solution (C++ Implementation)
解析:
- 搜索顺序优化:将猫按体重降序排序。重猫放置灵活度低,先处理能极大减少搜索树深层的分支数。
- 最优性剪枝:记录当前已找到的最少车数
min_cabs。若当前已开k辆车且 ,则剪枝。
C++ 实现:
#include <iostream>
#include <algorithm>
using namespace std;
int n, W, ans;
int w[20], cabs[20];
void dfs(int u, int k) {
if (k >= ans) return; // 最优性剪枝
if (u == n) {
ans = k;
return;
}
for (int i = 0; i < k; i++) {
if (cabs[i] + w[u] <= W) {
cabs[i] += w[u];
dfs(u + 1, k);
cabs[i] -= w[u];
}
}
cabs[k] = w[u];
dfs(u + 1, k + 1);
cabs[k] = 0;
}
二、 进阶搜索模型 (Level B)
练习 3:送礼物 - 双向搜索与折半查找
件礼物,每件重 ,车承重 。求最多能装多少重的礼物?
Check Solution (C++ Implementation)
数学推导: 直接 DFS 复杂度 过大。 折半搜索 (Meet-in-the-middle):将礼物分为两部分 ( 件) 和 ( 件)。
- 搜索 所有组合重量,存储并排序。
- 搜索 的组合重量 ,在 的结果中二分查找最大的 。 复杂度:。
C++ 实现:
// 核心逻辑:两个 DFS + upper_bound
void dfs1(int u, LL sum) {
if (u == k) { weights.push_back(sum); return; }
if (sum + w[u] <= W) dfs1(u + 1, sum + w[u]);
dfs1(u + 1, sum);
}
// ... 第二次 DFS 时进行二分查找更新 ans
练习 4:排书 - IDA* 与后继关系估价
给定 本书的排列,每次可将一叠连续书抽出并插入他处。求 4 步内使有序的最少步数。
Check Solution (C++ Implementation)
估价函数 设计: 每次操作最多改变 3 个位置的后继关系。设当前不正确的后继关系总数为 ,则 。
C++ 代码实现:
int f() {
int tot = 0;
for (int i = 0; i < n - 1; i++)
if (q[i + 1] != q[i] + 1) tot++;
return (tot + 2) / 3;
}
// IDA* 框架
bool dfs(int depth) {
if (depth + f() > limit) return false;
if (f() == 0) return true;
// ... 尝试所有可能的区间切分与插入位置
}
三、 启发式与随机化搜索 (Level C)
练习 5:第 K 短路 - A* 与优先队列
给定有向图,求起点 到终点 的第 短路长度。
Check Solution (C++ Implementation)
A* 建模:
- 估价函数 :设 为节点 到终点 的真实最短距离(反向图 Dijkstra 预处理)。
- 性质:当终点 第 次从优先队列中取出时,路径长度 即为答案。
C++ 代码实现 (核心逻辑):
priority_queue<pair<int, pair<int, int>>> pq;
pq.push({-(dist[S]), {0, S}});
while (!pq.empty()) {
int f = -pq.top().first, g = -pq.top().second.first, u = pq.top().second.second;
pq.pop();
cnt[u]++;
if (cnt[T] == K) return g;
// ... 遍历邻边入队
}
练习 6:八数码问题 (IDA* 优化)
题目描述:在 的棋盘上,摆有八个棋子,每个棋子上标有 的数字,另有一个空格。求从初始状态到目标状态的最少步数。
Check Solution (C++ Implementation)
估价函数:使用 曼哈顿距离 (Manhattan Distance) 之和作为估价函数。 曼哈顿距离是满足可容性 (Admissible) 的,因为每次移动一个棋子,总曼哈顿距离最多改变 1。
C++ 实现 (核心框架):
int get_h() {
int res = 0;
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++) {
int t = g[i][j];
if (t) res += abs(i - (t-1)/3) + abs(j - (t-1)%3);
}
return res;
}
bool dfs(int depth, int last_op) {
int h = get_h();
if (h == 0) return true;
if (depth + h > limit) return false;
// ... 四向移动,注意不走回头路
}
练习 7:井字棋 AI - Minimax 与 Alpha-Beta 剪枝
实现一个在 井字棋中绝不会输的 AI。
Check Solution (C++ Implementation)
博弈搜索策略:
- 极大极小搜索:AI 试图最大化分数(自己赢 +10),玩家试图最小化分数(AI 输 -10)。
- Alpha-Beta 剪枝:维护 (已知的 AI 最小收益)和 (已知的玩家最大损失)。若 ,停止搜索该子树。
C++ 实现 (核心逻辑):
int evaluate(char b[3][3]) {
// 检查行、列、对角线是否有人获胜
// 返回 +10, -10 或 0
}
int minimax(char board[3][3], int depth, bool isMax, int alpha, int beta) {
int score = evaluate(board);
if (score == 10 || score == -10 || !isMovesLeft(board)) return score;
if (isMax) {
int best = -1000;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[i][j] == '_') {
board[i][j] = 'X';
best = max(best, minimax(board, depth + 1, !isMax, alpha, beta));
board[i][j] = '_';
alpha = max(alpha, best);
if (beta <= alpha) break;
}
}
}
return best;
} else {
int best = 1000;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[i][j] == '_') {
board[i][j] = 'O';
best = min(best, minimax(board, depth + 1, !isMax, alpha, beta));
board[i][j] = '_';
beta = min(beta, best);
if (beta <= alpha) break;
}
}
}
return best;
}
}
🏆 训练建议
- 估价函数的“紧致性”:估价函数 越接近真实值且不大于真实值,A*/IDA* 的效率越高。
- IDA vs A**:对于空间限制严格或状态数巨大的问题(如 15-puzzle),IDA* 优于 A*。
- 剪枝的艺术:在写搜索代码前,先在草稿纸上罗列出:1. 搜索顺序;2. 可行性剪枝;3. 最优性剪枝;4. 排除等价冗余。