看完你一定会有收获的,相信我!
回溯法
递归算法中非常经典的思想:回溯法。这样的算法思想通常都应用在一类问题上,这类问题叫做树型问题。
用回溯算法解决问题的一般步骤:
1、 针对所给问题,定义问题的解空间,它至少包含问题的一个(最优)解。
2 、确定易于搜索的解空间结构,使得能用
回溯法
方便地搜索整个解空间 。3 、以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索。
简单来说,回溯法可以理解成为通过选择不同的岔路口,来寻找目的地,一个岔路口一个岔路口的去尝试找到目的地,如果走错了路的话,继续返回到上一个岔路口的另外一条路,直到找到目的地。
算法模板
可以按照3个步骤来思考这类的问题:
- 「路径」:记录做出的选择。
- 「选择列表/状态变量」:通常而言,用数组存储可以选择的操作,或用状态变量记录当前操作位置。
- 「结束条件」:一般而言,就是递归的结束点,也就是搜索的结束点。
result = []
function backtrack(路径, 选择列表) {
if('满足结束条件') {
// 这里就是对答案做更新,依据实际题目出发
result.push(路径)
return
} else {
for(let i = 0; i < 选择列表.length; i++) {
// 对一个选择列表做相应的选择
做选择
backtrack(路径, 选择列表)
// 既然是回溯算法,那么在一次分岔路做完选择后
// 需要回退我们之前做的操作
撤销选择
}
}
}
回溯算法题的解题思路
使用刻意练习的方法训练回溯算法:
1、画出递归树,找到状态变量。即函数中的变量。
2、找出递归出口。
3、找出选择列表 / 表达式
4、进行剪枝操作
5、根据选择,递归调用。
6、撤销选择
案例分析
字母大小写全排列
给定一个字符串S,通过将字符串S中的每个字母转变大小写,我们可以获得一个新的字符串。返回所有可能得到的字符串集合。
输入:S = “a1b2”
输出:[“a1b2”, “a1B2”, “A1b2”, “A1B2”]
输入:S = “3z4”
输出:[“3z4”, “3Z4”]
输入:S = “12345”
输出:[“12345”]
解题思路:
每个字母有状态,大写小写;对于数字,直接跳过。
- 遇到数字,没有产生新分支,直接往后搜索。
- 遇到字母,需要搜索两次,1、大写 2、小写
- 递归出口:搜索到最后,则加入结果数组中。我们需要维护一个Index,指向当前的字符。
按照刻意练习的整理:
1、递归树,状态变量:index 和 当前合成字符串。
2、递归出口:index === len
3、选择列表:数字直接往下搜索;字母则分大小写搜索两次
4、剪枝:暂时不需要
5、撤销操作:不用撤销。
var letterCasePermutation = function (S) {
const result = [];
var backfind = function (str, index) {
if (index === S.length) {
return result.push(str);
}
let current = S[index];
if ((current >= 'A' && current <= 'Z') || (current >= 'a' && current <= "z")) {
//字母
let low = current.toLowerCase();
let upper = current.toUpperCase();
backfind(str + low, index + 1);
backfind(str + upper, index + 1);
} else {
//数字 直接添加
backfind(str + current, index + 1);
}
}
backfind("", 0);
return result;
};
子集
给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。
说明:解集不能包含重复的子集。
示例:
输入: nums = [1,2,3]
输出: [ [3], [1], [2], [1,2,3], [1,3], [2,3], [1,2], [] ]
解题思路:
每个数字有两种状态,选与不选。我们可以从头进行搜素,记录状态变量index和当前新数组。
当index === len,递归结束,存入结果数组中。
对每个数字进行两次操作:
1、选择该数字,进行搜索。
2、不选择该数字,进行搜索。
注意需要在选择后,撤销操作。
从刻意练习的思路捋一遍:
1、递归树,状态变量,当前数字下标、当前新数组。
2、递归出口:index===len
3、选择列表:每个数字要或不要
- 要,数字存入新数组,进行搜索。
- 不要,直接index+1搜索。
注意,选择数字后需要撤销操作。
4、剪枝:暂无。
5、撤销操作,pop
var subsets = function (nums) {
const result = [];
var dfs = function (index, list) {
if (index === nums.length) {
return result.push(list.slice());
}
list.push(nums[index]);
dfs(index + 1, list);
//撤销操作
list.pop();
dfs(index + 1, list);//不加入当前数字。
}
dfs(0, []);
return result;
};
以下是我在网上看到一套不错的回溯算法题集,如果你还在刻意找的话,可以看看这里。
类型 | 题目链接 |
---|---|
子集,组合 | 子集、子集 II、组合、组合总和、组合总和 II |
全排列 | 全排列、全排列 II、字符串的全排列、字母大小写全排列 |
搜索 | 解数独、单词搜索、N皇后、分割回文串、二进制手表 |
附上每道题 使用刻意练习思路的题解:
子集2 题解
组合 题解
组合总和 题解
组合总和2 题解
全排列 题解
全排列2 题解
字符串的全排列 题解
分割回文串 题解