算法套路学习之回溯算法-----子集,排列,组合等问题如何解决

来源:https://www.cnblogs.com/labuladong/p/12320463.html
我是看这个大佬的博客学习到的,下面我自己总结一下,并且记录一下遇到的使用回溯的题目,验证一下这个套路的可行性。一直比较讨厌这种题,因为没有总结出一套框架来。

一.需要思考的问题

解决一个回溯算法题,只需要以下三点:

  1. 路径:也就是已经做出的选择。
  2. 选择列表:也就是你当前可以做的选择
  3. 结束条件:也就是到达决策树底层,无法再做选择的条件

其实我看到这个也是一脸懵逼的,但是后来看例子就明白了,下面是算法的框架:

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();
    }
    }
}

解释一下:

这是决策数结构:
在这里插入图片描述

  1. 首先建立一个成员变量res,是一个嵌套的List,对应上面框架的result,这就是结果
  2. 确定什么是路径,什么是路径呢,其实就是我这个结果其中一个List,这个List保存着一条完整的路径,也就是一个排列,比如【1,2,3】这三个数其中一个排列是3,2,1,这个【3,2,1】就是一条路径,它是结果的一部分
  3. 重点说一下选择和选择列表这块。在这个题里头,选择是什么呢,其实就是选不选这个数,比如我现在刚选完1,下面可以选择2和3,【2,3】就是选择列表,1已经不能选了,因为已经选过了,所以通过continue过滤掉。

2.子集问题

LeetCode地址

我现在感觉做这种题要先把决策树画出来:
在这里插入图片描述

然后看代码:

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. 选择列表:也就是当前可以做的选择,假设我们走在第一步,也就是空的时候,可以直接加到结果里头(空集也是子集),也可以选择1 ,2,3,所以选择列表是从start=0开始遍历到结束。在方法里把start不停累加,这样能选的就会越来越少,符合预期。
  3. 结束条件:这道题没有判断,每次调用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.组合

Leetcode链接

同样的,先画出决策树

在这里插入图片描述
代码如下:

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();
        }

    }
}

这个和前几道题非常的相似,基本上一样

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章