回溯算法
解决一个回溯问题,实际上就是一个决策树的遍历过程。你只需要思考 3 个问题:
1、路径:也就是已经做出的选择。
2、选择列表:也就是你当前可以做的选择。
3、结束条件:也就是到达决策树底层,无法再做选择的条件。
基本框架
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
//类似于二叉树的前序
做选择
backtrack(路径, 选择列表)
// 类似于二叉树的后序
撤销选择
其本质类似于多叉树。
回溯的特点:不像动态规划存在重叠子问题可以优化,回溯算法就是纯暴力穷举,复杂度一般都很高。
当我们只需要一个答案而不需要全部答案时 可以修改模板
// 函数找到一个答案后就返回 true
bool backtrack(vector<string>& board, int row) {
// 触发结束条件
if (... == ...) {
.....
return true;
}
...
for (..; ..; ..) {
做选择
if (backtrack(board, row + 1))
return true;
撤销选择
}
return false;
}
与动态规划的差别:动态规划的暴力求解是回溯算法。只是动态规划包含重叠子问题性质,可以用dp table或备忘录优化,将递归树大幅剪枝。
回溯的基本问题
全排序
分为有重复与无重复以及数字和字符串的问题。
对于有重复问题进行的处理为:
1、排列,剪枝 ------》 if (i >=0 && c[i]) == c[i - 1] && !flag[i-1]) continue;(无论是数字还是字符串的重复问题都适用)
2、set(只适用于字符串的重复问题)
全排序的遍历模板为 for(int i = 0;i<length;i++) 故需要标识该值是否重复使用过
有两种方法:
1、boolean[] flag 无论字符串还是数值均适用
2、list.contains() (只适用于数值)
全排列的性质:全排列的递归树相当于是完美二叉树,故其叶子节点的值满足n!。
题目练习:
https://leetcode-cn.com/problems/permutation-ii-lcci
https://leetcode-cn.com/problems/permutation-sequence
https://leetcode-cn.com/problems/permutation-i-lcci
https://leetcode-cn.com/problems/permutations
https://leetcode-cn.com/problems/permutations-ii
子集
总的思路与全排序类似,基本回溯模板为
void backtrack(int[] nums, int index,LinkedList<Integer> list) {
res.add(new LinkedList(list));
for(int i = index; i < nums.length; i++) {
// 做选择
backtrack(nums, i + 1, list);
// 撤销选择
}
}
题目练习:
https://leetcode-cn.com/problems/subsets-ii
https://leetcode-cn.com/problems/subsets