算法Day21|回溯专题三 78.子集,90.子集II,491.递增子序列

 78.子集

1.题目描述

  • 给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

    解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

2.解题思路 

  • 如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!
  • 其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。
  • 以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下:

78.子集

 从图中红线部分,可以看出遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合

3.代码实现

1.确定递归函数的参数和返回值:

  • 全局变量数组path为子集收集元素,二维数组res存放子集组合。
  • 递归函数定义:从index位置开始寻找数组nums的所有子集,并把子集中放到结果集res中。
    //78.子集
    List<List<Integer>> res= new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    private void backTracking(int[] nums, int startIndex) {}

2.确定终止条件:

  • 从图中可以看出:剩余集合为空的时候,就是叶子节点。那么什么时候剩余集合为空呢?
  • 就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下:
        //递归终止条件
        if (startIndex == nums.length) {
            return;
        }

78.子集

 3.确定单层递归的逻辑:

  • 子集收集元素
  • 递归:注意从i+1开始,元素不重复取
  • 回溯
        //单层递归逻辑
         for (int i = startIndex; i < nums.length; i++) {
            path.add(nums[i]);// 子集收集元素
            backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
            path.removeLast(); // 回溯
        }

 完整代码如下:

class Solution {
    //78.子集
    List<List<Integer>> res= new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

   public List<List<Integer>> subsets(int[] nums) {
        backTracking(nums, 0);
        return res;
    }

    private void backTracking(int[] nums, int startIndex) {
        // 收集子集,要放在终止添加的上面,否则会漏掉自己
        res.add(new ArrayList<>(path));
        // 终止条件可以不加
        if (startIndex == nums.length) {
            return;
        }
        for (int i = startIndex; i < nums.length; i++) {
            path.add(nums[i]);// 子集收集元素
            backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
            path.removeLast(); // 回溯
        }
    }
}

90.子集II

1.题目描述

  • 给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。
  • 解集不能包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

2.解题思路 

  • 这道题目和78.子集 (opens new window)区别就是集合里有重复元素了,而且求取的子集要去重。
  • 那么关于回溯算法中的去重问题,40.组合总和II (opens new window)中已经详细讲解过了,和本题是一个套路
  • 所以理解“树层去重”和“树枝去重”非常重要

    用示例中的[1, 2, 2] 来举例,如图所示: (注意去重需要先对集合排序

90.子集II

从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集! 

3.代码实现

1.确定递归函数的参数和返回值:

  • 全局变量数组path为子集收集元素,二维数组res存放子集组合。
  • 递归函数定义:从index位置开始寻找数组nums的所有子集,并把子集中放到结果集res中。
    List<List<Integer>> res = new ArrayList<>();;
    LinkedList<Integer> path = new LinkedList<>();;
    //90. 子集 II
   private void backTracking(int[] nums, int startIndex) {}

2.确定终止条件:

  • 剩余集合为空的时候,就是叶子节点。那么什么时候剩余集合为空呢?
  • 就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下:
        //递归终止条件
        if (startIndex == nums.length) {
            return;
        }

 3.确定单层递归的逻辑:

       利用二叉搜索树的特性:

  • 相对于78.子集来说只需要多添加树层上了剪枝逻辑就好了。

  • 注意在去重时要对原数组进行排序。

        //单层递归逻辑(一定采用中序遍历,左中右)
         for (int i = startIndex; i < nums.length; i++) {
            // 我们要对同一树层使用过的元素进行跳过
            // 注意这里使用i > startIndex
            if (i > startIndex && nums[i] == nums[i - 1]) {
                continue;
            }
            path.add(nums[i]);// 子集收集元素
            backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
            path.removeLast();// 回溯
        }

完整代码如下:

class Solution {
    
    List<List<Integer>> res = new ArrayList<>();;
    LinkedList<Integer> path = new LinkedList<>();;
    //90. 子集 II
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);// 去重需要排序
        backTracking(nums, 0);
        return res;
    }

    private void backTracking(int[] nums, int startIndex) {
        res.add(new ArrayList<>(path));
        if (startIndex == nums.length) {
            return;
        }
        for (int i = startIndex; i < nums.length; i++) {
            // 我们要对同一树层使用过的元素进行跳过
            // 注意这里使用i > startIndex
            if (i > startIndex && nums[i] == nums[i - 1]) {
                continue;
            }
            path.add(nums[i]);// 子集收集元素
            backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
            path.removeLast();// 回溯
        }
    }
}

491.递增子序列

1.题目描述

  • 给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
  • 数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

 2.解题思路 

  • 这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。
  • 这又是子集,又是去重,是不是不由自主的想起了刚刚讲过的90.子集II (opens new window)
  • 就是因为太像了,更要注意差别所在,要不就掉坑里了!
  • 90.子集II (opens new window)中我们是通过排序达到去重的目的。
  • 而本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。
  • 所以不能使用之前的去重逻辑!

为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图: 

491. 递增子序列1

 3.代码实现

3.1使用map

1.确定递归函数的参数和返回值:

        递归函数定义:本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。

    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    //491.递增子序列 使用map
    private void backTracking(int[] nums, int startIndex) {}

2.确定终止条件:

  • 本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和回溯算法:求子集问题! (opens new window)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。
  • 但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以代码如下:
        //递归终止条件
        if (path.size() > 1) {
            res.add(new ArrayList<>(path));
            // 注意这里不要加return,要取树上的节点
        }

 3.确定单层递归的逻辑:

        在图中可以看出,同一父节点下的同层上使用过的元素就不能在使用了。

491. 递增子序列1 

 那么单层搜索代码如下:

        //单层递归逻辑
        //说明:map是记录本层元素是否重复使用,新的一层map都会重新定义(清空),
        //所以要知道map只负责本层!map与path无关,所以不用做回溯处理
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = startIndex; i < nums.length; i++) {
            // 不满足递增或者当前层使用了重复元素
            if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)
                    || map.getOrDefault(nums[i], 0) == 1) {
                continue;
            }
            // 使用过了当前数字就把对应数字的value设置1,没有使用就为0
            map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
            path.add(nums[i]);// 子集收集元素
            backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
            path.removeLast();// 回溯
        }
  • 对于已经习惯写回溯的同学,看到递归函数上面的 map.put();,下面却没有对应的回溯之类的操作,应该很不习惯吧!
  • 这也是需要注意的点,map 是记录本层元素是否重复使用,新的一层map都会重新定义(清空),所以要知道map只负责本层! 

完整代码如下:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

     public List<List<Integer>> findSubsequences(int[] nums) {
        backTracking(nums, 0);
        return res;
    }

   //491.递增子序列  方法一:使用map
    private void backTracking(int[] nums, int startIndex) {
        if (path.size() > 1) {
            res.add(new ArrayList<>(path));
            // 注意这里不要加return,要取树上的节点
        }
        //说明:map是记录本层元素是否重复使用,新的一层map都会重新定义(清空),
        //所以要知道map只负责本层!map与path无关,所以不用做回溯处理
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = startIndex; i < nums.length; i++) {
            // 不满足递增或者当前层使用了重复元素
            if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)
                    || map.getOrDefault(nums[i], 0) == 1) {
                continue;
            }
            // 使用过了当前数字就把对应数字的value设置1,没有使用就为0
            map.put(nums[i], map.getOrDefault(nums[i], 0) + 1);
            path.add(nums[i]);// 子集收集元素
            backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
            path.removeLast();// 回溯
        }
    }
}

3.2使用数组

完整代码如下:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

     public List<List<Integer>> findSubsequences(int[] nums) {
        backTracking(nums, 0);
        return res;
    }

  //491.递增子序列 方法二:使用int[]
    private void backTracking(int[] nums, int startIndex) {
        if (path.size() > 1) {
            res.add(new ArrayList<>(path));
            // 注意这里不要加return,要取树上的节点
        }
        //这里使用数组来进行去重操作,题目说数值范围[-100, 100]
        //说明:isOrUse是记录本层元素是否重复使用,新的一层isOrUse都会重新定义(清空),
        //所以要知道isOrUse只负责本层!isOrUse与path无关,所以不用做回溯处理
        int[] isOrUse = new int[201];
        for (int i = startIndex; i < nums.length; i++) {
            // 不满足递增或者当前层使用了重复元素
            if (!path.isEmpty() && nums[i] < path.get(path.size() - 1)
                    || (isOrUse[nums[i] + 100] == 1)) {
                continue;
            }
            // 记录这个元素在本层用过了,本层后面不能再用了
            isOrUse[nums[i] + 100] = 1;
            path.add(nums[i]);// 子集收集元素
            backTracking(nums, i + 1);// 注意从i+1开始,元素不重复取
            path.removeLast();// 回溯
        }
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值