来源:https://www.cnblogs.com/labuladong/p/12320463.html
我是看这个大佬的博客学习到的,下面我自己总结一下,并且记录一下遇到的使用回溯的题目,验证一下这个套路的可行性。一直比较讨厌这种题,因为没有总结出一套框架来。
一.需要思考的问题
解决一个回溯算法题,只需要以下三点:
- 路径:也就是已经做出的选择。
- 选择列表:也就是你当前可以做的选择
- 结束条件:也就是到达决策树底层,无法再做选择的条件
其实我看到这个也是一脸懵逼的,但是后来看例子就明白了,下面是算法的框架:
result = []
def backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择
简单解释:首先result是在这个函数之外,属于一个全局的变量,是保存结果的,这个函数比较重要。
二.例题
1.全排列
给定一个 没有重复 数字的序列,返回其所有可能的全排列。这是LeetCode的46题,非常常见,我做了几次还是经常写不对,这太难受了,原理文章中已经讲得很清楚,下面我自己实现一下:
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> permute(int[] nums) {
// 记录「路径」
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums, track);
return res;
}
private void backtrack(int[] nums, LinkedList<Integer> track){
//写结束条件,就是路径长度跟选择列表一样的时候
if(track.size() == nums.length){
res.add(new LinkedList(track));
return;
}
for (int i = 0; i < nums.length; i++) {
//如果已经选了就过滤掉
if (track.contains(nums[i]))
continue;
//做选择
track.add(nums[i]);
//进入下一层决策树
backtrack(nums, track);
//取消选择
track.removeLast();
}
}
}
解释一下:
这是决策数结构:
- 首先建立一个成员变量res,是一个嵌套的List,对应上面框架的result,这就是结果
- 确定什么是路径,什么是路径呢,其实就是我这个结果其中一个List,这个List保存着一条完整的路径,也就是一个排列,比如【1,2,3】这三个数其中一个排列是3,2,1,这个【3,2,1】就是一条路径,它是结果的一部分
- 重点说一下选择和选择列表这块。在这个题里头,选择是什么呢,其实就是选不选这个数,比如我现在刚选完1,下面可以选择2和3,【2,3】就是选择列表,1已经不能选了,因为已经选过了,所以通过continue过滤掉。
2.子集问题
我现在感觉做这种题要先把决策树画出来:
然后看代码:
class Solution {
//这是保存结果的嵌套List
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
//路径
LinkedList<Integer> track = new LinkedList<>();
backtrack(nums,track,0);
return res;
}
private void backtrack(int[] nums, LinkedList<Integer> track, int start){
//结束条件
res.add(new LinkedList<>(track));
//for循环 遍历选择列表
for(int i = start; i< nums.length; i++){
track.add(nums[i]);
backtrack(nums, track, i+1);
track.removeLast();
}
}
}
代码跟刚才的题非常相似 下面说一下解释:
- 路径:路径是什么,这题的路径就是一个子集,因为答案是求子集总合,其中一个路径就是一个子集
- 选择列表:也就是当前可以做的选择,假设我们走在第一步,也就是空的时候,可以直接加到结果里头(空集也是子集),也可以选择1 ,2,3,所以选择列表是从start=0开始遍历到结束。在方法里把start不停累加,这样能选的就会越来越少,符合预期。
- 结束条件:这道题没有判断,每次调用backtrack就相当于到了一个新的树节点,可以直接加进去
3.全排列 II
LeetCode地址
这个题和全排列类似,都是找出全排列的结果,但是加上了一个条件:不重复的
代码:
class Solution {
//跟上面一样
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
//一样的track
LinkedList<Integer> track = new LinkedList<>();
//先排序方便判断
Arrays.sort(nums);
backtrack(nums, new boolean[nums.length], track);
return res;
}
private void backtrack(int[] nums, boolean[] visited, LinkedList<Integer> track){
if(track.size() == nums.length){
res.add(new LinkedList<>(track));
return;
}
//选择列表
for(int i = 0; i<nums.length; i++){
//已经选择过的不需要再放进去了
if(visited[i]) continue;
//接下来,如果当前节点与他的前一个节点一样,并其他的前一个节点已经被遍历过了,那我们也就不需要了。
if(i>0 && nums[i] == nums[i-1] && visited[i-1]) continue;
//做出选择
track.add(nums[i]);
visited[i] = true;
backtrack(nums,visited,track);
//撤销选择
track.removeLast();
visited[i] = false;
}
}
}
本题和第一题大致一样,关键是 for 选择 in 选择列表这块有点难。怎么做呢,就是要知道当前位于的节点有多少选择,应不应该选,所以需要维护一个visited[]数组,已经选过了的就不要再选了(全排列),并且如果当前节点与他的前一个节点一样,并其他的前一个节点已经被遍历过了,那我们也就不需要了。实际上就是加了两个判断,过滤掉 然后选择 / 回溯/ 撤销
4.子集 II
这题就是原题加了个条件:不能重复
什么样的情况下会出现重复呢,比如数组是【1,2,2】,假如我选了【1】,【2】,这时候再选【2】就重复了,假如我选了【1,2】,下次本来要回溯去选第三个数,发现这个数也是2,跟前一个一样,那么就不选了
结论:当这个数和前一个数一样那么就不选了,注意这里要判断,就是当前的i下标一定要大于开始的索引,才过滤,否则就选不出【1,2,2】这个子集了。
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> subsetsWithDup(int[] nums) {
LinkedList<Integer> track = new LinkedList<>();
Arrays.sort(nums);
backtrack(track, nums, 0);
return res;
}
private void backtrack(LinkedList<Integer> track, int[] nums,int start){
res.add(new LinkedList<>(track));
for(int i = start; i< nums.length; i++){
if(i > start && nums[i]==nums[i-1]){//这里最重要
continue;
}
track.add(nums[i]);
backtrack(track, nums, i + 1);
track.removeLast();
}
}
}
5.组合
同样的,先画出决策树
代码如下:
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> combine(int n, int k) {
LinkedList<Integer> track = new LinkedList<>();
backtrack(n, k, track, 1);
return res;
}
private void backtrack(int n, int k, LinkedList<Integer> track,int start){
if(track.size() == k){
res.add(new LinkedList<>(track));
}
for(int i = start; i <= n; i++){
track.add(i);
backtrack(n, k, track, i+1);
track.removeLast();
}
}
}
这个和前几道题非常的相似,基本上一样
6.路径综合 II
关键点:
- 这题最大的特点就是输入参数是一个TreeNode root 而不再是一个数组
- 写完代码我属实有点疑惑,为啥这次就不用for循环了?实际上for循环是为了通过backtrack(n, k, track, i+1),i的变化来遍历所有可能,而root本身是一个TreeNode,同时具备了路径+选择列表两种特征,所以不需要再for循环了
代码如下:
class Solution {
List<List<Integer>> res = new LinkedList<>();
public List<List<Integer>> pathSum(TreeNode root, int sum) {
LinkedList<Integer> track = new LinkedList<>();
backtrack(root, sum, track);
return res;
}
private void backtrack(TreeNode root, int sum, LinkedList<Integer> track){
if(root == null) return ;
sum = sum - root.val;
if(sum == 0 && root.left == null && root.right == null){
track.add(root.val);
res.add(new LinkedList<>(track));
track.removeLast();
return;
}
track.add(root.val);
backtrack(root.left,sum,track);
backtrack(root.right,sum,track);
track.removeLast();
return;
}
}