笔者每个月都会做一类算法题,最近一个月都在做回溯相关的算法题,总结出一些小套路。
什么是回溯算法
回溯算法个人理解其实就是一种暴力枚举方法,其中的回溯意思是指在当前岔路口选择一条路走出去后还可以回到当前岔路口再选择另一条路去走,和枚举很像吧,回溯算法的应用很多,常见的有全排列、数独、N皇后、0-1背包、正则表达式匹配、走迷宫等等。
回溯算法的套路
套路1
回溯算法其实主要的是需要确定如何开始与如何结束,一般都是有一个初始值,以及一个结束的条件,把整个流程分为多个阶段,每个阶段[1]都选择一个解[a]放入当前集合,并且记录下当前集合的状态,之后进入下一阶段[2],下一阶段[2]结束后,回溯到阶段[1]的上一状态,即将解[a]移除当前集合,回退集合的状态,再选择另一个解进行下一步。伪代码如下:
void back_trace(已选择的集合,当前解的状态,可选择的解的集合) {
if (当前解满足条件) {
结果集.add(当前解);
return;
}
for (可选解 : 可选择的解的集合) {
if (可选解在已选择的集合中) { continue; }
已选择集合.add(可选解);
当前解的状态.add(可选解);
back_trace(已选择的集合,当前解状态,下个可选择的集合);
已选择的集合.remove(可选解);
当前解的状态.remove(可选解);
}
}
这里使用此套路做全排列题目
全排列题目
给定一个没有重复数字的序列,返回其所有可能的全排列。
示例:
输入: [1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
这里使用之前的套路:
class Solution {
public:
vector<vector<int>> ret;
vector<int> path;
vector<bool> visited;
void back_trace(vector<int>& nums) {
// 当前解满足条件
if (path.size() == nums.size()) {
// 结果集.add(当前解);
ret.push_back(path);
return;
}
for (int i = 0, size = nums.size(); i < size; ++i) {
// if (可选解在已选择的集合中) { continue; }
if (visited[i]) continue;
// 已选择集合.add(可选解);
visited[i] = true;
// 当前解的状态.add(可选解);
path.push_back(nums.at(i));
// back_trace(已选择的集合,当前解状态,下个可选择的集合);
back_trace(nums);
// 已选择的集合.remove(可选解);
visited[i] = false;
// 当前解的状态.remove(可选解);
path.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
visited.resize(nums.size(), false);
back_trace(nums);
return ret;
}
};
关于排列问题也可以看这篇文章:无重复字符串的排列组合
套路2
然而上一类是指无重复的输入的排列,然而还有那种有重复的输入,这样使用套路1就有一点点小问题,对于这种,可以使用这种套路2。
void main() {
首先对输入的数组排序,使相同的元素在相邻的位置
back_trace();
}
void back_trace(已选择的集合,当前解的状态,可选择的解的集合) {
if (当前解满足条件) {
结果集.add(当前解);
return;
}
for (可选解 : 可选择的解的集合) {
if (当前可选解和上一可选解相同&&上一可选解未在已选择集合里) { continue; }
if (可选解在已选择的集合中) { continue; }
已选择集合.add(可选解);
当前解的状态.add(可选解);
back_trace(已选择的集合,当前解状态,下个可选择的集合);
已选择的集合.remove(可选解);
当前解的状态.remove(可选解);
}
}
具体实例可以看这篇文章:有重复字符串的排列组合
套路3
使用套路1和套路2还是不能解决全部的问题,因为回溯的时间复杂度比较高,是n的阶乘,做很多hard类题目容易超时,所以针对回溯要尽量使用一些优化剪枝的策略,一个是尽量剪枝,一个是尽量避免重复的递归。
void main() {
在输入处尽量做到一些剪枝
back_trace();
}
void back_trace(已选择的集合,当前解的状态,可选择的解的集合) {
if (之前针对此可选择的集合已经递归过) { // 此处灵活添加
返回之前保存的状态;
return;
}
if (当前解满足条件) {
结果集.add(当前解);
return;
}
for (可选解 : 可选择的解的集合) {
// 此处灵活添加剪枝策略
if (当前可选解和上一可选解相同&&上一可选解未在已选择集合里) { // 灵活使用
continue;
}
if (可选解在已选择的集合中) { continue; }
已选择集合.add(可选解);
当前解的状态.add(可选解);
state = back_trace(已选择的集合,当前解状态,下个可选择的集合);
【保存针对下个可选择的结合的递归的状态state】
已选择的集合.remove(可选解);
当前解的状态.remove(可选解);
}
}
关于此类问题可以参考这类文章:
附加
更多关于回溯的题目练习可以看往期的文章
每日一道算法题