子集问题---78. Subsets && 90. Subsets II

子集问题—78. Subsets && 90. Subsets II

一、不包含重复元素的集合,列出所有子集

1. 题目
78. Subsets

Given a set of distinct integers, nums, return all possible subsets (the power set).
Note: The solution set must not contain duplicate subsets.
Example:
Input: nums = [1,2,3]
Output:
[
[3],
[1],
[2],
[1,2,3],
[1,3],
[2,3],
[1,2],
[]
]

2. 题目分析
这是我们从初中就开始接触的列出这个大的集合的所有子集,并且大的集合的元素不存在重复的元素。

找所有子集,一个集合有n个元素,则包含2的n次方个子集(包含空集)。这是一题典型的回溯算法,

科普时间:
回溯法有通用解法的美称,对于很多问题,如迷宫等都有很好的效果。回溯算法实际上一个类似枚举的深度优先搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就一步步往前“回溯”返回(也就是递归返回),尝试别的路径(结合深度优先遍历算法进行理解)。 回溯法说白了就是穷举法,一般用递归来解决。但回溯算法与普通的递归算法的区别是什么呢?

采用回溯算法实现的基本形式是“递归+循环”,正因为循环中嵌套着递归,递归中包含循环,这才使得回溯比一般的递归和单纯的循环更难理解,其实我们熟悉了它的基本形式,就会觉得这样的算法难度也不是很大。

对于回溯法来说,每次递归调用,很重要的一点是把每次递归的不同信息传递给递归调用的函数。而这里最重要的要传递给递归调用函数的信息,就是把上一步做过的某些事情的这个选择排除,避免重复和无限递归。另外还有一个信息必须传递给递归函数,就是进行了每一步选择后,暂时还没构成完整的解,这个时候前面所有选择的汇总也要传递进去。而且一般情况下,都是能从传递给递归函数的参数处,得到结束条件的。

递归函数的参数的选择,要遵循四个原则:

  1. 必须要有一个临时变量(可以就直接传递一个字面量或者常量进去)传递不完整的解,因为每一步选择后,暂时还没构成完整的解,这个时候这个选择的不完整解,也要想办法传递给递归函数。也就是,把每次递归的不同情况传递给递归调用的函数。
  2. 可以有一个结果集变量,用来存储完整的每个解,一般是个集合容器(也不一定要有这样一个变量,因为每次符合结束条件,不完整解就是完整解了,直接打印即可)。
  3. 最重要的一点,一定要在参数设计中,可以得到结束条件。一个选择是可以传递一个量n,也许是数组的长度,也许是数量,等等。
  4. 要保证递归函数返回后,状态可以恢复到递归前,以此达到真正回溯。

想要具体了解回溯算法的同学可以参见这个博主回溯算法超通俗易懂详尽分析和例题,分析的非常详细,但如果还没做题的同学直接接触这个理论分析,可能会丈二摸不着头脑,所以建议先看题目,如:当前题目,找出所有子集问题,然后回过头来理解回溯算法的精髓所在。

3. 解题思路
整个添加的顺序为:
[]
[1]
[2]
[1 2]
[3]
[1 3]
[2 3]
[1 2 3]
下面来看递归的解法,相当于一种深度优先搜索,参见网友的博客,由于原集合每一个数字只有两种状态,要么存在,要么不存在,那么在构造子集时就有选择和不选择两种情况,所以可以构造一棵二叉树,左子树表示选择该层处理的节点,右子树表示不选择,最终的叶节点就是所有子集合,树的结构如下:

                        []        
                   /          \        
                  /            \     
                 /              \
              [1]                []
           /       \           /    \
          /         \         /      \        
       [1 2]       [1]       [2]     []
      /     \     /   \     /   \    / \
  [1 2 3] [1 2] [1 3] [1] [2 3] [2] [3] []

原数组中的每个元素有两种状态:存在和不存在。(算法组成:递归+循环)
① 外层循环(循环 )逐一往中间集合 temp (临时变量,传递不完整的解)中加入元素 nums[i],使这个元素处于存在状态。
② 开始递归(递归),递归中携带加入新元素的 temp,并且下一次循环的起始start(结束条件)是 i 元素的下一个,因而递归中更新 i 值为 i + 1,然后在每次递归中判断中间集合是否满足题目要求,满足的话把中间集合加入到最终的结果集(结果集变量)中 。
③ 将这个从中间集合 temp 中移除,使该元素处于不存在状态(保证递归函数返回后,状态可以恢复到递归前)。

4. 代码实现(java)

package com.algorithm.leetcode.backtracking;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by 凌 on 2019/2/1.
 * 注释:78. Subsets
 */
public class Subsets {
    public static void main(String[] args) {
        Subsets subsets = new Subsets();
        int[] nums={1,2,3};
        List<List<Integer>> result = subsets.subsets(nums);
        System.out.println(result.toArray());
    }

    /**
     * 求所有子集,包括空集,集合没有重复的元素
     * @param nums
     * @return
     */
    public List<List<Integer>> subsets(int[] nums) {
        if (nums == null){
            return new ArrayList<>();
        }
        List<List<Integer>> result = new ArrayList<>();//结果集变量
        List<Integer> list = new ArrayList<Integer>();//中间结果集变量
        int start = 0;
        dfs(nums,result,list,start);
        return result;
    }
    public void dfs(int[] nums,List<List<Integer>> result, List<Integer> list,int start){

        result.add(new ArrayList<Integer>(list));//用new ArrayList(list)是因为,list后面的递归还会进行修改。

        if (list.size() == nums.length || start == nums.length){
            return;
        }

        for (int i = start; i < nums.length; i++) {
            list.add(nums[i]);
            dfs(nums,result,list,i+1);
            list.remove(list.size()-1);
        }

    }
}

二、包含重复元素的集合,列出所有子集

1. 题目
90. Subsets II

Given a collection of integers that might contain duplicates, nums, return all possible subsets (the power set).

Note: The solution set must not contain duplicate subsets.
Example:
Input: [1,2,2]
Output:
[
[2],
[1],
[1,2,2],
[2,2],
[1,2],
[]
]

2. 题目分析
这题跟上面那题唯一的区别就是,集合中的元素存在重复元素(数学意义上的集合是不存在相同的元素,但为了更好翻译,就把nums叫做集合)

3. 解题思路
对于递归的解法,根据之前 Subsets 子集合 里的构建树的方法,{1,2,2}在处理到第二个2时,由于前面已经处理了一次2,这次我们只在添加过2的[2] 和 [1 2]后面添加2,其他的都不添加,代码只需在原有的基础上增加一句话,while (nums[i] == nums[i + 1]) ++i; 这句话的作用是跳过树中为X的叶节点,因为它们是重复的子集,应被抛弃。(注意:这种处理方式的前提是,元素是有序的,所以需要先进行元素排序

即,从前往后遍历,保留下当前已经计算好的组合集合。对当前i号元素的加入,就是有i和没有i的场景。没有i的场景就是已有的集合。有i的就是对已有的集合追加上i后的集合。两个的并集就是加入i之后的结果。
如果i和上一个元素是重复的,则只需要考虑i-1加入过集合的那部分子集。对于不包含i-1元素的场景来说,这部分子集加入i的结果集,和已有的加入过i-1的结果集是重复的。所以跳过这部分就可以了。

4. 代码实现(java)

package com.algorithm.leetcode.backtracking;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by 凌 on 2019/2/5.
 * 注释:90. Subsets II
 */
public class SubsetsWithDup {
    public static void main(String[] args) {
        SubsetsWithDup subsets = new SubsetsWithDup();
//        int[] nums={1,2,2};
        int[] nums={4,4,4,1,4};
        List<List<Integer>> result = subsets.subsetsWithDup(nums);
        System.out.println(result.toArray());
    }

    /**
     * 求所有子集,包括空集,集合存在重复的元素
     * 从前往后遍历,保留下当前已经计算好的组合集合。对当前i号元素的加入,就是有i和没有i的场景。没有i的场景就是已有的集合。有i的就是对已有的集合追加上i后的集合。两个的并集就是加入i之后的结果。
     如果i和上一个元素是重复的,则只需要考虑i-1加入过集合的那部分子集。对于不包含i-1元素的场景来说,这部分子集加入i的结果集,和已有的加入过i-1的结果集是重复的。所以跳过这部分就可以了。
     *
     * @param nums
     * @return
     */
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if (nums == null){
            return new ArrayList<>();
        }
        Arrays.sort(nums);
        List<List<Integer>> result = new ArrayList<>();
        List<Integer> list = new ArrayList<Integer>();
        int start = 0;
        dfs(nums,result,list,start);
        return result;
    }
    public void dfs(int[] nums,List<List<Integer>> result, List<Integer> list,int start){
        /*if (!isRepeatSequence(result,list)){
            result.add(new ArrayList<Integer>(list));
        }*/
        result.add(new ArrayList<Integer>(list));
        if (list.size() == nums.length || start == nums.length){
            return;
        }

        for (int i = start; i < nums.length; i++) {
            list.add(nums[i]);
            dfs(nums,result,list,i+1);
            list.remove(list.size()-1);
            while (i + 1 < nums.length && nums[i] == nums[i + 1]) ++i;
        }
    }

    /**
     * 是否存在重复序列
     * @param list
     * @param list2
     * @return
     */
    public boolean isRepeatSequence(List<List<Integer>> list,List<Integer> list2){
        for (List<Integer> temp : list){
            boolean flag = false;
            if (temp.size() == list2.size()){
                for (int i = 0; i < list2.size(); i++) {
                    if (!list2.get(i).equals(temp.get(i))){
                        flag = true;
                        break;
                    }
                }
                //如果跟list2的每一个元素相比较,没有发现不相同的,说明就整个元素序列是相同的
                if (!flag){
                    return true;
                }
            }
        }
        return false;
    }
}

发布了151 篇原创文章 · 获赞 104 · 访问量 27万+

猜你喜欢

转载自blog.csdn.net/qq_35923749/article/details/86767153