14 搜索(回溯 / 剪枝)与迷宫类问题

GESP 6 级CSP-J 高频

级别标注:GESP 6 级(考级要求)/ CSP-J 高频(竞赛进阶:状态压缩搜索、迭代加深、双向 BFS、剪枝艺术)。

一、搜索的本质

搜索 = 穷举所有可能的状态,找到符合要求的解。空间小时直接搜;空间大时靠剪枝与模型转换。

两大流派: - DFS(深度优先):一条路走到黑,回溯换路;栈/递归实现,适合"求可行解/计数/全排列"。 - BFS(广度优先):按层扩展,找"最短步数"最优解;队列实现。

形象理解搜索像闭着眼睛在迷宫里试路:DFS 是一条道走到黑,撞墙就退回岔路口换一条(深度优先);BFS 是像水波扩散一样一圈圈试探,先碰到终点的那圈就是最短路径(广度优先)。
考级视角选 DFS 还是 BFS 有口诀:求可行解/计数/全排列用 DFS,求最短步数用 BFS。GESP 6 级常考两者区别:DFS 用栈(递归),BFS 用队列。

二、DFS 回溯模版(重点)

C++ 实例
#include <iostream>
using namespace std;
const int N = 20;
int n;
bool used[N];        // used[i]:数字 i 是否已被使用
int a[N];            // a[k]:第 k 个位置填的数

// 回溯求全排列:dfs(k) 表示正在填第 k 个位置
void dfs(int k) {
    if (k == n) {                    // 已填满 -> 输出一个完整解
        for (int i = 0; i < n; i++) cout << a[i] << " ";
        cout << endl;
        return;
    }
    for (int i = 1; i <= n; i++) {   // 枚举当前位可选的数字
        if (!used[i]) {              // 数字 i 还没用过
            used[i] = true;          // 占用:标记
            a[k] = i;                // 填数
            dfs(k + 1);              // 递归填下一位
            used[i] = false;         // 回溯:撤销标记,供下一分支使用
        }
    }
}
int main() {
    n = 4;
    dfs(0);                          // 输出 1..4 的全排列
    return 0;
}

回溯三要素:选择列表、已选择路径、结束条件。核心口诀:进入前标记,返回后撤销。

形象理解回溯像玩华容道时试错再还原:尝试放一个数,标记“占用了”,递归填下一个;填完或失败就撤销标记,试下一个数。进入前标记、返回后撤销,是回溯的灵魂。
易错点忘记撤销标记是回溯最常见的 bug:used[i] = false; 漏写会导致后面的分支“看到”上一个分支用过的数,解全部污染。记住口诀:进入前标记,返回后撤销。

三、迷宫问题(DFS 求所有路径 / 连通性)

C++ 实例
#include <iostream>
#include <string>
using namespace std;
const int N = 105;
int n, m;
char mp[N][N];       // 地图
bool vis[N][N];      // 访问标记
int dx[4] = {-1, 1, 0, 0};   // 上下左右四个方向
int dy[4] = {0, 0, -1, 1};

// DFS 判断从 (x,y) 能否到达终点 'E'
bool dfs(int x, int y) {
    if (mp[x][y] == 'E') return true;   // 到达终点
    vis[x][y] = true;                   // 标记当前格
    for (int d = 0; d < 4; d++) {       // 枚举四个方向
        int nx = x + dx[d], ny = y + dy[d];
        if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;  // 越界跳过
        if (vis[nx][ny] || mp[nx][ny] == '#') continue;        // 已走过或是墙
        if (dfs(nx, ny)) return true;   // 找到路立即返回
    }
    return false;                       // 四条路都走不通
}
int main() {
    n = 3; m = 3;
    string raw[3] = {"S..", "#.#", "..E"};   // S 起点,# 墙,. 空地
    int sx = 0, sy = 0;
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            mp[i][j] = raw[i][j];
            if (mp[i][j] == 'S') sx = i, sy = j;   // 记录起点
        }
    }
    cout << (dfs(sx, sy) ? "可达" : "不可达") << endl;
    return 0;
}
形象理解迷宫 DFS 像在迷宫里做记号探路:走过的格子用粉笔标记(vis),墙(#)不能走,越界不能走,能走到终点 E 就成功。
易错点四方向移动数组 dx[4]/dy[4] 要先判越界再访问数组:if (nx < 0 || nx >= n || ...) continue; 写在访问 mp[nx][ny] 之前,否则数组越界直接崩溃或读到脏数据。

四、BFS 求迷宫最短步数(重点模板)

C++ 实例
#include <iostream>
#include <queue>
#include <string>
using namespace std;
const int N = 105;
int n, m;
char mp[N][N];       // 地图
int step[N][N];      // step[i][j]:从起点到 (i,j) 的最少步数
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
const int INF = 1e9;

// BFS 求最短步数(边权为 1 的网格)
int bfs(int sx, int sy, int ex, int ey) {
    for (int i = 0; i < n; i++)
        for (int j = 0; j < m; j++) step[i][j] = INF;   // 初始全不可达
    queue<pair<int, int>> q;
    q.push({sx, sy});
    step[sx][sy] = 0;                // 起点步数 0
    while (!q.empty()) {
        pair<int, int> cur = q.front(); q.pop();   // C++11 兼容写法
        int x = cur.first, y = cur.second;
        if (x == ex && y == ey) return step[x][y];   // 到终点即最短
        for (int d = 0; d < 4; d++) {   // 向四个方向扩展
            int nx = x + dx[d], ny = y + dy[d];
            if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;  // 越界
            if (mp[nx][ny] == '#') continue;   // 墙
            if (step[nx][ny] != INF) continue; // 已访问过(第一次才最短)
            step[nx][ny] = step[x][y] + 1;     // 步数 +1
            q.push({nx, ny});
        }
    }
    return -1;   // 队列空仍未到终点:不可达
}
int main() {
    n = 4; m = 4;
    string raw[4] = {"S..#", "..#.", "....", "#..E"};
    for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) mp[i][j] = raw[i][j];
    cout << bfs(0, 0, 3, 3) << endl;   // 最短步数
    return 0;
}

BFS 要点:入队时即标记 dist(不要出队才标记,否则会重复入队)。若需要输出路径,用 pre[x][y] 记录前驱。

形象理解BFS 求最短路像涟漪一圈圈扩散:起点是丢石子的地方,第一圈是走 1 步能到的格子,第二圈是 2 步…第一个碰到终点 E 的圈数就是最短步数。
易错点BFS 必须入队时立刻标记步数(step[nx][ny] = step[x][y] + 1 后马上 push)。如果等出队才标记,同一个格子会被多个邻居重复入队,轻则 TLE,重则死循环。

五、剪枝(Pruning)——搜索提速的核心

剪枝 = 提前发现"这条路不可能有解",尽早砍掉。

常用剪枝手法: 1. 可行性剪枝:当前状态已不满足约束(如已超时/超步数)→ 剪。 2. 最优性剪枝:当前累计代价已 ≥ 已知最好解 → 剪。 3. 对称性剪枝:重复状态的等价性去重。 4. 记忆化:同一个状态只搜一次(memo 数组),避免重复计算。

C++ 实例
// 记忆化搜索:用 memo 记录已算过的子问题,避免重复递归
#include <iostream>
#include <string>
using namespace std;
const int N = 20;
int n, m;
char mp[N][N];
long long memo[N][N];    // memo[x][y]:从 (x,y) 出发的路径数(-1 未算)
bool vis[N][N];          // 是否已计算过

long long dfsPath(int x, int y) {
    if (x == n - 1 && y == m - 1) return 1;      // 到终点:1 条路
    if (x >= n || y >= m) return 0;              // 越界:0 条
    if (mp[x][y] == '#') return 0;               // 墙:0 条
    if (vis[x][y]) return memo[x][y];            // 已记忆:直接返回
    vis[x][y] = true;                            // 标记已算
    // 只能向右、向下走:路径数 = 右 + 下
    memo[x][y] = dfsPath(x + 1, y) + dfsPath(x, y + 1);
    return memo[x][y];
}
int main() {
    n = 3; m = 3;
    string raw[3] = {"...", ".#.", "..."};
    for (int i = 0; i < n; i++) for (int j = 0; j < m; j++) mp[i][j] = raw[i][j];
    cout << dfsPath(0, 0) << endl;
    return 0;
}
形象理解剪枝像逛超市提前看标签:发现这箱水果超过预算就直接跳过,不用一个个翻(可行性剪枝);已经找到 100 元方案,超过 100 的组合直接放弃(最优性剪枝)。
考级视角记忆化搜索与 DP 是同一件事的两种写法:memo[x][y] 记下“从 (x,y) 出发的答案”,下次直接取。GESP/CSP 常考“记忆化把指数级搜索优化成多项式”,理解“状态只算一次”是核心。

六、经典搜索题变式

  • 八皇后:DFS + 列/对角线标记。
  • 数独:DFS + 剪枝(行/列/九宫格标记)。
  • 单词接龙、连连看、连连跳:状态转移建模。
  • 双向 BFS / 迭代加深(IDDFS):NOIP 提高组进阶,减少状态爆炸。
  • 状态压缩搜索:把状态压缩成整型(配合 17 章位运算)。

七、易错点 / 常见坑点

  • ⚠️ BFS 忘标记或出队才标记导致重复处理、TLE/死循环。
  • ⚠️ 坐标越界判断顺序:先判越界再访问数组。
  • ⚠️ DFS 回溯必须撤销标记(每个分支独立),否则解互相污染。
  • ⚠️ 搜索顺序影响时间:先尝试更可能成功的分支可显著提速。
  • ⚠️ 递归深度过大爆栈(迷宫很大时用显式栈或 BFS)。
  • ⚠️ 记忆化的状态定义要完整(漏掉关键维度导致错误结果)。

八、考级要求 vs 竞赛进阶

  • 考级要求(GESP 6 级):DFS/BFS 基本框架、迷宫可达与最短步数、全排列/组合的枚举、简单回溯。这是 GESP 6 级重点。
  • 竞赛进阶(CSP-J/NOIP):剪枝艺术(最优性/可行性)、记忆化搜索(与 DP 等价)、状态压缩搜索、迭代加深;搜索是 T4 的常见题型,也是很多暴力分的主要来源 —— "先写暴力(搜索)拿分,再优化" 是重要应试策略。

九、课内练习建议

  1. 写 DFS + 回溯输出 1..n 全排列、组合。
  2. BFS 模板解 3 道迷宫最短路题。
  3. 八皇后问题(DFS + 三数组对角标记)。
  4. 用最优性剪枝优化一个暴力搜索题,对比时间。
  5. 将记忆化搜索与 15 章 DP 对照,体会两者的联系。
本章目录
一、搜索的本质 二、DFS 回溯模版(重点) 三、迷宫问题(DFS 求所有路径 / 连通性) 四、BFS 求迷宫最短步数(重点模板) 五、剪枝(Pruning)——搜索提速的核心 六、经典搜索题变式 七、易错点 / 常见坑点 八、考级要求 vs 竞赛进阶 九、课内练习建议