回溯法的本质
回溯法是一种深度优先搜索算法,该方法试图穷举问题所有的解。所谓递归回溯,回溯一般出现在递归函数中。通过递归来实现深度优先遍历。常用来解决组合问题、排列问题、分割问题等问题。
深度优先搜索(DFS)
在回溯算法中,“深度”指的是递归调用的层数,或者说是在解决问题时探索路径的长度。每深入一层,就选择一个可能的选项继续向下探索。检查当前路径是否满足问题的要求。如果满足,则记录这个解;如果不满足,则回溯到上一层,尝试其他可能的选择。
如何搜索?
-
做出选择: 在当前状态下,选择一个可行的选项。
-
递归深入: 基于当前选择,进入下一个状态,继续做出新的选择。
-
撤销选择: 如果发现当前路径不能到达目标或者已经找到一个解,回溯到上一步,撤销之前的决定,尝试其他选择。
-
终止条件: 当所有选择都被尝试过或者找到了足够的解后,结束搜索过程。
-
剪枝策略: 可以设置不同的剪枝策略,在枚举搜索节点(即路径节点)时使用剪枝避免不必要的搜索,提高算法性能。
通过在递归过程中提前排除不可能产生有效解的路径,剪枝可以帮助我们更快地找到所有可能的解决方案!
- 问题约束规则(约束函数):视具体情况而定。
- 限界选优(剪枝函数),常出现在求解最优问题时。
回溯算法的最简洁模版
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
e.g:组合总和问题
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
void backtrack(vector<int>& candidates, int target, int start, vector<int>& path, vector<vector<int>>& result) {
if (target == 0) { // 终止条件:如果目标值为0,说明找到了一个有效组合
result.push_back(path);
return;
}
for (int i = start; i < candidates.size(); ++i) {
if (candidates[i] > target) break; // 剪枝:如果当前候选数大于目标值,后面的更大数也不需要考虑
// 选择:将当前候选数加入路径
path.push_back(candidates[i]);
// 递归调用,深入下一层
backtrack(candidates, target - candidates[i], i, path, result); // 注意这里的start保持不变,允许重复使用当前候选数
// 撤销选择:移除最后一个元素
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> result;
vector<int> path;
// 首先对候选数组进行排序,方便后续剪枝操作
sort(candidates.begin(), candidates.end());
backtrack(candidates, target, 0, path, result);
return result;
}
int main() {
vector<int> candidates = {2, 3, 6, 7};
int target = 7;
vector<vector<int>> combinations = combinationSum(candidates, target);
return 0;
}
回溯算法中只有剪枝策略一种优化方式
优化方案
-
在for循环(单层遍历节点)上做剪枝操作,映射到解空间树的结构上就是层方面的剪枝
-
在递归函数中做剪枝或者更优解的更新,映射到解空间树的结构就是递归树垂直上的剪枝
本文非常简陋,是在ai刷题过程中慢慢总结的,目前还有很多地方不足,个人建议初学者(虽然我也是)最好一开始刷题先问ai题目的分类和推荐方法,然后具体的方法应该参考课本,搭配专门的一些算法总结和例题,然后自己抽象总结,常备纸笔自己手写程序,感觉是很有效的。