NeFut Logo NeFut
EN 管理员登录

高效状态空间搜索:迭代加深与双向广度优先算法的深度解析

发布于:2026-05-27 09:17 最后更新:2026-06-28 13:31
#C++ #Tutorial

核心逻辑与数学原理

在高维状态空间中,普通广度优先搜索(BFS)会因状态爆炸导致内存崩溃,而深度优先搜索(DFS)则容易在没有最优解的深层分支中盲目死磕。

状态空间 是指从初始状态出发,通过所有合法的操作(如走一步棋、向左转),能够到达的所有可能状态的集合。

维度 指的是描述一个状态所需要的独立变量(特征)的数量

高维状态空间 当一个问题的状态需要极多的变量来描述,或者每次状态转移都有极多的选择(即分支系数大)。

迭代加深搜索(Iterative Deepening DFS, IDS): 其核心思想是将 DFS 的空间优势与 BFS 的步数最优性相结合。通过显式限制搜索深度上限 max_dep,从 1 开始逐步递增。若在当前深度限制下未找到解,则废弃整棵搜索树,将 max_dep + 1 重新开始 DFS。对于分支系数为 k 的搜索树,第 m 层的节点数为 k^m。尽管 IDS 重复搜索了前 m-1 层,其总时间复杂度仍为:

$$T(m)=\frac{k(k^m-1)}{k-1}\approx k^m$$

首项 $a_1 = k$,公比 $q = k$,项数为 $m$。 根据等比数列求和公式 $S_m = \frac{a_1(1-q^m)}{1-q}$

$T(m) = k^1 + k^2 + \dots + k^m$。 当 $k=10$ 时: $T(m) = 10^1 + 10^2 + \dots + 10^m$。 $10^m$ 比之前所有项之和还要大。 从渐进复杂度的角度来看,常数项和低阶项被忽略,主导项即为 $k^m$。

这与直接执行 BFS 的时间复杂度同阶,空间复杂度从$O(k^m)$降为 $O(m)$。

双向广度优先搜索(Bidirectional BFS, Bi-BFS): 当搜索的起始状态与目标状态均已知时,从起点(正向)和终点(逆向)同时扩展状态。传统 BFS 拓展至深度 d 的状态空间规模为 $O(k^d)$。而 Bi-BFS 双方各自拓展 $\frac{d}{2}$ 的深度并在中间相遇,状态空间规模缩减为 $O(2\times k^{\frac{d}{2}})=O(k^{\frac{d}{2}})$。从几何角度看,它将一个大半径的高维球体膨胀,转化为两个小半径球体对位拦截,在空间与时间上实现了数量级的优化。

这里的“球体”,是指以起始节点为圆心,在状态空间中向外等距离扩散的层级结构。


状态设计与算法推导

1. IDS 状态设计与剪枝推导

定义函数 bool dfs(int cur, int dep, int max_dep)。

2. Bi-BFS 空间与相遇对位推导

使用两个队列 q_start 和 q_end,以及哈希表 vis_start 和 vis_end 维护两端扩展出的步数。


模板

IDA*

// h(u) 为估价函数,需根据具体题目定义
bool dfs(int u, int dep, int max_dep) {
    if (dep + h(u) > max_dep) return false; // 乐观估计剪枝:当前深度+预估距离超出限制
    if (h(u) == 0) return true;             // h(u)==0 表示已到达目标状态

    for (int v : get_next(u)) {             // 枚举所有可能的后继状态
        if (dfs(v, dep + 1, max_dep)) return true; // 递归搜索,找到解则返回
    }
    return false;
}

void solve() {
    int max_dep = h(start);                 // 从理论最小深度开始迭代
    while (!dfs(start, 0, max_dep)) {       // 若当前深度无解,增加深度限制
        max_dep++;
    }
    // 输出 max_dep 即为最优步数
}

双向BFS

// q[0]为起点队列,q[1]为终点队列;vis记录从两端出发到达某点的距离
queue<int> q[2];
int vis[MAXN][2]; 

int bfs() {
    q[0].push(start); vis[start][0] = 0;
    q[1].push(end);   vis[end][1] = 0;

    while (!q[0].empty() && !q[1].empty()) {
        // 动态选择规模较小的一侧扩展,平衡搜索空间
        int d = (q[0].size() <= q[1].size()) ? 0 : 1;
        int sz = q[d].size();

        while (sz--) {
            int u = q[d].front(); q[d].pop();
            for (int v : get_next(u, d)) { // d=0为正向边,d=1为反向边
                if (vis[v][d] != -1) continue;
                if (vis[v][1 - d] != -1) return vis[u][d] + 1 + vis[v][1 - d]; // 两端相遇

                vis[v][d] = vis[u][d] + 1;
                q[d].push(v);
            }
        }
    }
    return -1; // 无法连通
}

核心逻辑:选择规模较小的队列,把这一层所有节点全部处理完


NOIP 实战避坑指南

  1. Bi-BFS 逆向转移算错算漏:双向 BFS 的终点逆向扩展时,状态转移必须是正向转移的逆元。如果图是有向图,逆向 BFS 必须跑在反向图上。若直接套用正向转移函数,两路搜索将在高维空间中平行错过,导致队列打满爆内存或死循环。
  2. IDS 忘记重置全局标记:在编写迭代加深(IDS)时,每一轮 max_dep 增加后,本质上是重新开一棵 DFS 树,必须清理上一轮遗留的全局 vis 标记或全局最优解变量。否则第二轮搜索刚进第一层就会被上一轮残存的标记错误剪枝,返回无解。

经典真题与核心解法代码

1. 洛谷 P2326 移动玩具

#include <queue>
#include <cstring>
#include <algorithm>

int vis[65536][2]; // 0表示正向,1表示逆向

int bfs_double(int start, int target) {
    if (start == target) return 0;
    std::queue<int> q[2];
    std::memset(vis, -1, sizeof(vis));

    q[0].push(start);  vis[start][0] = 0;
    q[1].push(target); vis[target][1] = 0;

    // 定义16宫格相邻的位移变换矩阵
    int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};

    while (!q[0].empty() && !q[1].empty()) {
        int dir = q[0].size() <= q[1].size() ? 0 : 1;
        int sz = q[dir].size();

        while (sz--) {
            int u = q[dir].front(); 
            q[dir].pop();

            for (int i = 0; i < 16; ++i) {
                int x = i / 4, y = i % 4;
                for (int k = 0; k < 4; ++k) {
                    int nx = x + dx[k], ny = y + dy[k];
                    if (nx >= 0 && nx < 4 && ny >= 0 && ny < 4) {
                        int j = nx * 4 + ny;
                        // 提取第i位和第j位的值
                        int bit_i = (u >> i) & 1;
                        int bit_j = (u >> j) & 1;
                        if (bit_i == bit_j) continue; // 颜色相同无需交换

                        // 交换第i位和第j位,得到新状态
                        int next_state = u ^ (1 << i) ^ (1 << j);

                        if (vis[next_state][dir] != -1) continue;
                        if (vis[next_state][1 - dir] != -1) {
                            return vis[u][dir] + 1 + vis[next_state][1 - dir];
                        }
                        vis[next_state][dir] = vis[u][dir] + 1;
                        q[dir].push(next_state);
                    }
                }
            }
        }
    }
    return -1;
}

2. 洛谷 P1074 靶形数独

#include <algorithm>

int row[9], col[9], cell[3][3], score_map[9][9];
int blank_count[9], row_order[9]; 
int ans = -1;
int grid[9][9];

// lowbit 提取二进制中最右侧的 1
inline int lowbit(int x) { return x & -x; }

void dfs(int r_idx, int c, int current_score) {
    if (r_idx == 9) {
        ans = std::max(ans, current_score);
        return;
    }

    int r = row_order[r_idx];
    if (c == 9) {
        dfs(r_idx + 1, 0, current_score);
        return;
    }
    if (grid[r][c] != 0) {
        dfs(r_idx, c + 1, current_score + grid[r][c] * score_map[r][c]);
        return;
    }

    // 掩码 0x1FF 对应 9 个 1 (表示 1-9 都可用)
    // 找出当前行列宫均未使用的数字交集
    int available = ~(row[r] | col[c] | cell[r / 3][c / 3]) & 0x1FF;

    while (available) {
        int p = lowbit(available); 
        available ^= p; // 抹除该位

        // __builtin_ctz 计算末尾 0 的个数,刚好映射回填入的数字 (0~8)
        int num = __builtin_ctz(p) + 1; 

        row[r] |= p; col[c] |= p; cell[r / 3][c / 3] |= p;
        dfs(r_idx, c + 1, current_score + num * score_map[r][c]);
        row[r] ^= p; col[c] ^= p; cell[r / 3][c / 3] ^= p; // 回溯
    }
}

void solve() {
    // 根据每行空格数量从小到大排序行索引,大幅减少搜索树分支
    for (int i = 0; i < 9; ++i) row_order[i] = i;
    std::sort(row_order, row_order + 9, [](int a, int b) {
        return blank_count[a] < blank_count[b];
    });
    dfs(0, 0, 0);
}

[h] 返回首页