算法-回溯算法思想
1. 回溯算法的基本概述
什么是回溯算法?回溯算法,本质上就是
回溯 = 递归 + 状态恢复 + 剪枝
它是一种暴力搜索的优化形式,通过“尝试 -> 撤销”来穷举所有可能解,并在过程中剪枝无效分枝
2. 回溯适用的算法题目
| 场景 | 典型关键词 | 示例题目 |
|---|---|---|
| 组合/子集/排列 | “所有可能的组合”、“返回所有子集”、“全排列” | 78. Subsets, 46. Permutations |
| 路径搜索 | “是否存在路径”、“从起点到终点”、“相邻单元格” | 79. Word Search, 212. Word Search II |
| 约束满足问题 | “和为 target”、“不重复使用”、“每个元素最多用一次” | 39. Combination Sum, 40. Combination Sum II |
| 构造类问题 | “生成所有有效括号”、“解数独”、“N皇后” | 22. Generate Parentheses, 51. N-Queens |
核心标志:
- 需要枚举所有的可行解
- 解具有 阶段性决策结构
- 不能重复使用状态(需标记+恢复)
- 有明确的终止条件
3. 回溯算法的五大要素(分析 Checklist)
在遇到回溯算法题目时,首先要分析如下的5个问题:
| 要素 | 问题 | 示例(Combination Sum) |
|---|---|---|
| 1. 路径(Path) | 当前已经做出的选择是什么? | 已选的数字列表 path |
| 2. 选择列表(Choices) | 当前可以做哪些选择? | candidates[i](从 index 开始) |
| 3. 结束条件(Base Case) | 什么时候算找到一个解? | target == 0 |
| 4. 约束条件(Constraints) | 什么选择是非法的? | target < 0 或重复组合 |
| 5. 状态管理(State) | 如何避免重复使用?如何恢复? | path.add() / path.remove();排序+同层去重 |
4. 通用算法模板
4.1 存在性问题(返回boolean)
boolean backtrack(状态,参数...) {if (满足结束条件) return true;if (不合法) return false;for (每个选择 in 选择列表) {if (backtrack(新状态)) return true;}// 不满足条件,恢复状态return false;
}
4.2 收集所有解(返回 List<List<...>>)
void backtrack(List<解> res, 路径 path, int startIndex, ...) {if (满足结束条件) {res.add(new ArrayList<解>(path));return;}if (不合法) {return;}for (int i = startIndex; i < 选择总数; i++) {// 可选】剪枝:跳过无效或重复选择if(需要跳过) {continue;}path.add(选择[i]);backtrack(res, path, i/i+1, ...); // 如果题目要求选课选择重复的,从i开始;如果要求不允许重复,则从 i+1 开始进行递归path.remove(path.size() - 1); // 不符合条件进行回溯时,移除的是最近一次新添加入的元素,也就是撤销最近一次的选择}
}
5. 去重的一些核心技巧(重点!)
场景:数组包含重复元素,但是解不能重复
正确的做法是:
-
先排序:
Arrays.sort(nums) -
在 for 循环中判断:
for(参数) {if(i > startIndex && nums[i] == nums[i - 1]) {continue; // 当前遍历的索引位置元素不是当前遍历的开头元素,并且和上一个元素的值相同,则跳过当前元素的处理} }
一些比较没有意义的做法
- 用
Set存结果再转 List,这种做法没有任何意义! - 在递归开头判断是否重复,也没有意义,因为这样无法阻止无效递归
💡 为什么
i > startIndex?表示当前不是这一层的第一个选择。如果是第一个(
i == startIndex),即使和前面相同,也是新路径的开始,应保留。
6. 一些性能优化的小技巧
| 技巧 | 说明 |
|---|---|
| 提前排序 | 便于剪枝和去重 |
| 剪枝 | 如 if (nums[i] > target) break;(排序后有效) |
| 使用 StringBuilder / LinkedList | 减少字符串/列表操作开销 |
| 避免深拷贝过大对象 | 只在必要时复制路径 |
7. 总结:回溯解题四步法
- 确定递归函数参数:路径、起始位置、目标状态等
- 写终止条件:何时将路径加入结果?
- 写 for 循环:遍历当前可选集合
- 做选择 → 递归 → 撤销选择
- 子集/组合问题:用
startIndex控制起点- 排列问题:用
used[]标记是否使用- 网格问题:用原地标记(如
#)防重复- 有重复元素:先排序,再同层去重