ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OI-wiki 回溯法(Backtracking)详解:从 DFS/BFS 应用到剪枝优化实战

OI-wiki 回溯法(Backtracking)详解:从 DFS/BFS 应用到剪枝优化实战 OI-wiki 回溯法Backtracking详解从 DFS/BFS 应用到剪枝优化实战【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki回溯法Backtracking是 OI / ICPC 竞赛中搜索算法的重要技巧其核心思想是走不通就回头在状态空间中构造解并逐步试探一旦发现当前路径不可能达到目标就立即回退到上一个状态改走另一条分支。本文以 OI-wiki 的 backtracking.md 为主体结合仓库中的完整参考代码与测试样例系统讲解回溯法的概念、执行过程并通过八皇后Checker Challenge与迷宫计数两道经典例题深入剖析其在 DFS 与 BFS 两种搜索框架下的落地方式与剪枝优化要点。回溯法是什么一种搜索技巧而非独立算法回溯法不是一种独立的算法而是经常被用在 深度优先搜索DFS 和 广度优先搜索BFS 中的一种技巧。它的本质可以浓缩为一句话走不通就回头。与一次性枚举所有可能不同回溯法在搜索过程中维护当前路径这一状态向前试探性地扩展一步当发现当前路径已经违反约束条件到达边界条件时不再继续向下搜索而是撤销刚才的决策回退到上一层的状态转而搜索另一条分支。这种试探—回退—再试探的循环使得回溯法能够在有限的状态空间中系统性地遍历所有可行解同时通过约束条件提前终止大量无望的分支。在 OI 竞赛中纯粹的搜索往往是获得部分分的手段而回溯法则是在状态空间上实现暴力枚举的高效组织方式——它既是 DFS 搜索 的天然载体也可借助队列以 BFS 搜索 的形式实现尽管 BFS 缺乏天然的回溯过程需要额外记录状态。回溯法的四步过程OI-wiki 将回溯法的执行过程概括为四个步骤构造空间树把问题的所有可能解组织成一棵解空间树。树的每一层对应决策的一个阶段每个结点代表当前的部分解状态从根到叶的一条路径就是一个完整的候选方案。进行遍历按照某种顺序通常是 DFS 的深度优先顺序也可以是 BFS 的逐层顺序遍历这棵空间树。遇到边界条件即剪枝当当前结点不满足约束到达边界条件时不再向下搜索转而搜索另一条链。这一步是回溯法效率的关键——边界条件越早触发被剪掉的无望分支越多。达到目标条件输出结果当遍历到达叶子结点且满足目标条件时就得到了一组可行解将其输出或统计。用一句话串起整个过程沿着一条分支一路试探到不能走为止不行就退回来换一条路直到穷尽整棵空间树。例题一八皇后 Checker Challenge——回溯法的 DFS 实现题目背景USACO 1.5.4 Checker Challenge在一个 $6 \times 6$ 的跳棋棋盘上放置六个棋子使得每行、每列、每条对角线包括两条主对角线在内的所有对角线上都至多有一个棋子。下面是一种合法布局O表示棋子0 1 2 3 4 5 6 ------------------------- 1 | | O | | | | | ------------------------- 2 | | | | O | | | ------------------------- 3 | | | | | | O | ------------------------- 4 | O | | | | | | ------------------------- 5 | | | O | | | | ------------------------- 6 | | | | O | | | -------------------------由于每行只能放一个棋子上述布局可以用序列 ${2,4,6,1,3,5}$ 描述第 $i$ 个数字表示第 $i$ 行的棋子所在列号。对应地行号 $i$ 为 ${1,2,3,4,5,6}$列号 $a_i$ 为 ${2,4,6,1,3,5}$。任务要求找出所有放置方案并按字典顺序输出只需输出前 $3$ 个解并在最后一行输出解的总个数同时必须优化程序以保证在更大棋盘尺寸下仍然高效。空间树建模与合法条件判定把放置过程视为逐行决策第 $i$ 层决定第 $i$ 行棋子放在哪一列。这样解空间树共有 $n$ 层、每层 $n$ 个分支暴力枚举的总状态数为 $n^n$而加上列不重复、对角线不重复的约束后合法分支会被大幅裁剪。判定在某位置放置是否合法需要三个检查列冲突当前列 $i$ 是否已被占用主对角线冲突位于同一主对角线左上到右下的格子满足 $line - i$ 为常数检查 $line i$ 或 $line - i$ 的对应标记副对角线冲突位于同一副对角线左下到右上的格子满足 $line i$ 为常数。仓库中的参考实现仓库提供了完整的参考代码 backtracking_1.cpp注释明确标注该代码为回溯法的 DFS 实现// 该代码为回溯法的 DFS 实现 #include iostream using namespace std; int ans[14], check[3][28] {0}, sum 0, n; void eq(int line) { if (line n) { // 如果已经搜索完n行 sum; if (sum 3) return; else { for (int i 1; i n; i) cout ans[i] ; cout \n; return; } } for (int i 1; i n; i) { if ((!check[0][i]) (!check[1][line i]) (!check[2][line - i n])) { // 判断在某位置放置是否合法 ans[line] i; check[0][i] 1; check[1][line i] 1; check[2][line - i n] 1; eq(line 1); // 向下递归后进行回溯方便下一轮递归 check[0][i] 0; check[1][line i] 0; check[2][line - i n] 0; } } } int main() { cin.tie(nullptr)-sync_with_stdio(false); cin n; eq(1); cout sum; return 0; }这段代码是回溯法 DFS 实现的教科书范例包含三个值得细读的设计check[3][28]标记数组check[0][i]标记第 $i$ 列是否被占用check[1][line i]标记主对角线$line i$ 恒为常数check[2][line - i n]标记副对角线$line - i$ 为常数通过偏移 $n$ 将下标归正到非负区间。三个条件同时满足才允许放置这是空间树剪枝的核心一旦某列或某条对角线已被占用整条分支立即放弃。递归与回溯的配对在递归调用eq(line 1)之前置位标记递归返回之后立即清除标记三行置零这正是走不通就回头在代码层面的体现。若不清除标记下一轮循环将无法复用这些列和对角线导致漏解。前 3 个解的特殊处理if (sum 3) return;保证只输出前 3 个解而sum持续累加最终输出解的总数——用最小的改动满足题目输出前 3 个解并在最后一行输出总数的要求。测试样例验证仓库在 examples/backtracking/ 中提供了与代码配套的输入输出样例。输入 backtracking_1.in 为6对应输出 backtracking_1.ans 为2 4 6 1 3 5 3 6 2 5 1 4 4 1 5 2 6 3 4即 $6 \times 6$ 棋盘共有 $4$ 组解前 3 组按字典序依次为 ${2,4,6,1,3,5}$、${3,6,2,5,1,4}$、${4,1,5,2,6,3}$。这与题目描述中的示例布局 ${2,4,6,1,3,5}$ 完全吻合可直接用此样例验证代码正确性也可自行扩展 $n$如 $n8$ 时经典八皇后问题观察解数与运行效率。例题二迷宫计数——回溯法的 BFS 实现题目背景迷宫现有一个尺寸为 $N \times M$ 的迷宫迷宫中有 $T$ 处障碍障碍处不可通过。给定起点坐标和终点坐标每个方格最多经过一次问有多少种从起点到终点的方案。移动方式为上、下、左、右四种每次只能移动一个方格数据保证起点上没有障碍。注意这道题与常规求最短路径的迷宫题不同它要求统计所有可行路径的方案数且每个方格最多经过一次因此需要穷举状态空间而非求最短路。仓库中的参考实现仓库提供的 backtracking_2.cpp 注释标注该代码为回溯法的 BFS 实现它用队列存储路径状态来模拟回溯// 该代码为回溯法的 BFS 实现 #include cstring #include iostream #include queue using namespace std; int n, m, k, x, y, a, b, ans; int dx[4] {0, 0, 1, -1}, dy[4] {1, -1, 0, 0}; // 四个方向 bool vis[6][6]; struct oo { int x, y, used[6][6]; }; oo sa; void bfs() { queueoo q; sa.x x; sa.y y; sa.used[x][y] 1; q.push(sa); while (!q.empty()) { // BFS队列 oo now q.front(); q.pop(); for (int i 0; i 4; i) { // 枚举向四个方向走 int sx now.x dx[i]; int sy now.y dy[i]; if (now.used[sx][sy] || vis[sx][sy] || sx 0 || sy 0 || sx n || sy m) continue; if (sx a sy b) { ans; continue; } sa.x sx; sa.y sy; memcpy(sa.used, now.used, sizeof(now.used)); sa.used[sx][sy] 1; q.push(sa); // 假设向此方向走放入BFS队列 } } } int main() { cin.tie(nullptr)-sync_with_stdio(false); cin n m k; cin x y a b; for (int i 1, aa, bb; i k; i) { cin aa bb; vis[aa][bb] true; // 障碍位置不可通过 } bfs(); cout ans; return 0; }这段 BFS 实现有两点值得注意状态中包含访问历史struct oo除了坐标x, y外还携带一个used[6][6]访问标记数组。这与常规 BFS 的全局 visited不同——由于要统计不同路径的方案数且每个方格最多经过一次每条路径必须拥有独立的访问历史。扩展新结点时用memcpy复制当前状态的used数组再在新位置置位从而保证不同分支互不干扰。这是 BFS 中回溯的等价实现状态随队列保存天然支持从旧状态重新分叉。越界与障碍判定合并now.used[sx][sy] || vis[sx][sy] || sx 0 || sy 0 || sx n || sy m一次性完成走过、障碍、越界三类非法条件的过滤任一条件为真即continue跳过该方向。测试样例验证输入 backtracking_2.in 为2 2 1 1 1 2 2 1 2表示 $2 \times 2$ 迷宫、$1$ 处障碍位于 $(1,2)$起点 $(1,1)$终点 $(2,2)$。对应输出 backtracking_2.ans 为1即只有 $(1,1) \to (2,1) \to (2,2)$ 这一条路径另一条直行路径被障碍挡住方案数为 $1$。可以用这个最小规模的样例理解 BFS 状态队列的扩展过程再替换为更大的N、M数据验证统计正确性。回溯法与剪枝效率来自哪里回溯法之所以能在竞赛中实用关键在于边界条件触发得越早被剪掉的分支越多。从仓库两份代码中可以提炼出三类通用的剪枝手段约束前置判定即剪枝八皇后问题中check标记数组在尝试放置的瞬间就完成列与对角线的冲突检测一旦冲突eq(line 1)根本不会被调用整棵子树被整体跳过。这是最典型、最有效的剪枝。非法状态过滤迷宫问题中越界、障碍、已访问三类条件在状态入队前统一过滤continue避免无效状态进入队列占用空间与时间。决策顺序优化启发式在保证正确性的前提下优先尝试更可能通向解的分支可以更快找到解、更早触发剪枝。这类技巧的深入内容可参考仓库中的 搜索优化 opt.md 与 启发式搜索 heuristic.md。另外需要指出 BFS 实现回溯的代价由于每个状态都要复制一份used访问数组memcpy状态数与路径数成正比内存开销显著高于 DFS 的共享数组 撤销模式这也是 BFS 搜索文档 中所指出的BFS 需要更大的内存、缺乏天然的回溯过程的具体体现。因此当问题对内存敏感时DFS 形式的回溯通常是更自然的选择当问题本身具有层次性或需要配合其他逐层算法时BFS 形式的回溯也是一种可行方案。总结回溯法在 OI-wiki 的搜索章节中被定位为 DFS 与 BFS 中常用的一种技巧其走不通就回头的本质决定了它适用于穷举可行解、统计合法解个数的一类问题。通过 backtracking.md 的两道例题可以看到八皇后Checker Challenge展示了回溯法在 DFS 框架下的经典形态逐层决策 标记数组剪枝 递归返回后撤销标记配合 backtracking_1.cpp 与 测试样例 可完整验证迷宫计数展示了回溯思想在 BFS 框架下的另一种实现用携带独立访问历史的状态入队以memcpy复制状态模拟回溯见 backtracking_2.cpp 与 对应样例。熟练掌握构造空间树—遍历—边界剪枝—输出目标这一四步流程并理解剪枝与状态管理对效率的决定性影响是写出高效搜索程序的基础。在此基础上可继续研读仓库中的 DFS、BFS、迭代加深 iterative.md 以及 双向搜索 bidirectional.md 等页面构建完整的搜索算法体系。【免费下载链接】OI-wiki:star2: Wiki of OI / ICPC for everyone. 某大型游戏线上攻略内含炫酷算术魔法项目地址: https://gitcode.com/GitHub_Trending/oi/OI-wiki创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表