【搜索】启发式迭代加深搜索(IDA*)

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 为什么能保证最优解?

因为:

  1. 每次迭代的阈值都是基于f值的
  2. f值是对总代价的估计,且h(n)是可采纳的
  3. 当找到解时,其f值就是最优代价
  4. 任何比当前解更优的路径都已经被搜索过(因为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*)
  • 启发式函数很差时
  • 需要频繁查询时(可用记忆化搜索)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值