1. 算法背景与动机
1.1 传统搜索算法的局限性
广度优先搜索(BFS):
- 优点:保证找到最优解
- 缺点:内存消耗巨大,O(b^d),其中b是分支因子,d是深度
深度优先搜索(DFS):
- 优点:内存消耗小,O(d)
- 缺点:可能陷入无限深的分支,不保证找到最优解
A*算法:
- 优点:利用启发式信息,搜索效率高
- 缺点:需要存储所有访问过的状态,内存消耗大
迭代加深搜索(IDDFS):
- 结合BFS的完备性和DFS的内存效率
- 通过逐步增加深度限制来搜索
1.2 IDA*的创新点
IDA将A算法的启发式评估与迭代加深的内存效率相结合,解决了大状态空间下的最优路径搜索问题。
2. 核心机制详解
2.1 评估函数f(n) = g(n) + h(n)
让我们深入理解这个公式:
g(n) - 实际代价:
- 从起始节点到当前节点n的实际路径代价
- 在8数码问题中,g(n)就是移动步数
- 在路径规划中,g(n)可能是实际行驶距离
- g(n)是精确计算的,没有估计误差
h(n) - 启发式估计:
- 从当前节点n到目标节点的估计代价
- 必须满足可采纳性:h(n) ≤ h*(n),其中h*(n)是实际最小代价
- 常见启发式:
- 曼哈顿距离:在网格中,水平和垂直距离之和
- 欧几里得距离:直线距离
- 错位棋子数:在8数码中,不在目标位置的棋子数
f(n) - 综合评估:
- f(n)是对从起点经过n到终点的总代价的估计
- A*算法总是优先扩展f值最小的节点
2.2 迭代加深过程
第一次迭代:
- 设置阈值 = 起始节点的f值
- 进行深度优先搜索,但只扩展f值 ≤ 阈值的节点
- 记录搜索过程中遇到的所有f值 > 阈值的节点
- 找到这些节点中的最小f值作为下一次迭代的阈值
后续迭代:
- 使用新的阈值继续搜索
- 每次迭代都比前一次"看得更远"
- 直到找到目标节点
2.3 算法执行流程图解
开始
↓
设置初始阈值 = f(起始节点)
↓
进行深度优先搜索(f值限制)
↓
找到目标? → 是 → 返回解
↓ 否
记录超过阈值的最小f值
↓
更新阈值 = 最小超过值
↓
阈值合理? → 是 → 返回上一步
↓ 否
无解或超出限制
3. 详细工作示例(以8数码问题)
假设我们有以下初始状态和目标状态:
初始状态: 目标状态:
2 8 3 1 2 3
1 6 4 4 5 6
7 5 7 8
第一次迭代:
- 计算起始节点f值:
- g(n) = 0(还未移动)
- h(n) = 曼哈顿距离 = 4(2差1步,8差2步,6差1步…)
- f(n) = 0 + 4 = 4
- 阈值 = 4
- 搜索所有f值 ≤ 4的节点
- 发现无法到达目标,记录超过4的最小f值,假设为5
第二次迭代:
- 阈值 = 5
- 搜索范围扩大,可以访问f值为5的节点
- 仍然无法到达目标,记录超过5的最小f值,假设为6
第三次迭代:
- 阈值 = 6
- 继续搜索…
- 最终找到目标节点
4. C++实现(以8数码问题为例)
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <climits>
using namespace std;
// 8数码问题的节点结构
struct Node {
vector<int> state; // 当前状态
int g; // 从起点到当前节点的实际代价
int h; // 启发式估计代价
int f; // f = g + h
int blank_pos; // 空格位置 (0-8)
Node(vector<int> s, int g_val = 0, int b_pos = 0) :
state(s), g(g_val), blank_pos(b_pos) {
h = calculateHeuristic();
f = g + h;
}
// 计算曼哈顿距离启发式
int calculateHeuristic() {
int distance = 0;
for (int i = 0; i < 9; i++) {
if (state[i] != 0) { // 0代表空格
int target_row = (state[i] - 1) / 3;
int target_col = (state[i] - 1) % 3;
int current_row = i / 3;
int current_col = i % 3;
distance += abs(target_row - current_row) + abs(target_col - current_col);
}
}
return distance;
}
};
class IDAStar {
private:
vector<int> goal_state = {1, 2, 3, 4, 5, 6, 7, 8, 0}; // 目标状态
vector<vector<int>> moves = {{-3, 3}, {-1, 1}, {-3, 3}, {-1, 1, 3}, {-3, -1, 1, 3},
{-3, -1, 1, 3}, {-3, 1}, {-3, -1, 1}, {-3, -1}}; // 每个位置可能的移动
// 检查状态是否为目标状态
bool isGoal(const vector<int>& state) {
return state == goal_state;
}
// 交换两个位置的数字
void swap(vector<int>& state, int pos1, int pos2) {
swap(state[pos1], state[pos2]);
}
// IDA*递归搜索函数
int search(Node& node, int threshold, int max_depth, vector<vector<int>>& path) {
if (node.f > threshold) {
return node.f; // 返回新的阈值
}
if (isGoal(node.state)) {
path.push_back(node.state);
return -1; // 找到解
}
if (node.g >= max_depth) {
return INT_MAX; // 达到最大深度限制
}
int min_threshold = INT_MAX;
// 尝试所有可能的移动
for (int move : moves[node.blank_pos]) {
int new_pos = node.blank_pos + move;
// 检查移动是否有效(不越界)
if (new_pos < 0 || new_pos >= 9) continue;
// 避免来回移动(简化剪枝)
if (node.g > 0 && new_pos == path.back()[node.blank_pos]) continue;
// 创建新状态
vector<int> new_state = node.state;
swap(new_state, node.blank_pos, new_pos);
// 创建新节点
Node new_node(new_state, node.g + 1, new_pos);
// 递归搜索
path.push_back(new_state);
int result = search(new_node, threshold, max_depth, path);
path.pop_back();
if (result == -1) return -1; // 找到解
if (result < min_threshold) min_threshold = result;
}
return min_threshold;
}
public:
// IDA*主函数
bool solve(vector<int>& initial_state, vector<vector<int>>& solution_path) {
// 找到空格位置
int blank_pos = 0;
for (int i = 0; i < 9; i++) {
if (initial_state[i] == 0) {
blank_pos = i;
break;
}
}
Node start_node(initial_state, 0, blank_pos);
// 初始阈值为起始节点的f值
int threshold = start_node.f;
int max_depth = 100; // 设置最大搜索深度
while (threshold <= max_depth) {
vector<vector<int>> path;
path.push_back(initial_state);
int result = search(start_node, threshold, max_depth, path);
if (result == -1) {
solution_path = path;
return true; // 找到解
}
if (result == INT_MAX) {
return false; // 无解或超出深度限制
}
threshold = result; // 更新阈值
}
return false; // 未找到解
}
};
// 打印状态
void printState(const vector<int>& state) {
for (int i = 0; i < 9; i++) {
cout << state[i] << " ";
if ((i + 1) % 3 == 0) cout << endl;
}
cout << endl;
}
// 测试函数
int main() {
IDAStar ida;
// 初始状态(可调整)
vector<int> initial = {2, 8, 3, 1, 6, 4, 7, 0, 5};
vector<vector<int>> solution;
cout << "初始状态:" << endl;
printState(initial);
if (ida.solve(initial, solution)) {
cout << "找到解!步数: " << solution.size() - 1 << endl;
cout << "求解过程:" << endl;
for (int i = 0; i < solution.size(); i++) {
cout << "步骤 " << i << ":" << endl;
printState(solution[i]);
}
} else {
cout << "未找到解或超出搜索深度限制" << endl;
}
return 0;
}
5. 关键特性分析
5.1 为什么能保证最优解?
因为:
- 每次迭代的阈值都是基于f值的
- f值是对总代价的估计,且h(n)是可采纳的
- 当找到解时,其f值就是最优代价
- 任何比当前解更优的路径都已经被搜索过(因为f值更小)
5.2 内存效率的奥秘
传统A*需要存储:
- 开放列表:所有待扩展的节点
- 关闭列表:所有已扩展的节点
IDA*只需要存储:
- 当前搜索路径上的节点(递归调用栈)
- 空间复杂度从O(b^d)降低到O(d)
5.3 重复状态处理
IDA*的一个有趣特性是它可能会重复访问同一状态,但这在大多数情况下是可以接受的,因为:
- 启发式搜索通常能快速找到最优路径
- 重复访问的状态往往处于不同的搜索深度
- 内存节省的好处通常超过重复计算的代价
6. 启发式函数的重要性
6.1 启发式的质量影响
弱启发式(如错位棋子数):
- h(n)值较小
- 搜索范围较大
- 运行时间较长
强启发式(如曼哈顿距离):
- h(n)值较大(但仍 ≤ h*(n))
- 搜索范围较小
- 运行时间较短
6.2 启发式的一致性
如果启发式还满足一致性(单调性):
- h(n) ≤ c(n,n’) + h(n’)
- 其中c(n,n’)是从n到n’的实际代价
- 可以保证每个节点最多被扩展一次
7. 算法变种与优化
7.1 IDA*的变种
- Recursive IDA*:更清晰的递归实现
- Depth-First IDA*:特定深度优先版本
- Memory-Bounded A*:结合IDA和A的优点
7.2 实现优化
- 剪枝策略:避免明显无效的移动
- 启发式改进:使用更精确的估计
- 预计算:提前计算部分启发式值
8. 实际应用考虑
8.1 何时使用IDA*
- 状态空间巨大,内存受限
- 需要保证最优解
- 有良好的启发式函数可用
- 解的深度不太大
8.2 何时避免使用
- 内存不是瓶颈时(可用A*)
- 启发式函数很差时
- 需要频繁查询时(可用记忆化搜索)
&spm=1001.2101.3001.5002&articleId=150525621&d=1&t=3&u=b72a53166221416ea40004b8983fe2d4)
1358

被折叠的 条评论
为什么被折叠?



