5639 字
约 18 分钟
10
078.子集
无标签

078.子集

作者:沉默王二
发布时间:2023-09-14T16:26:55.140+0800


本题解同时收录在星球专栏《二哥的 LeetCode 刷题笔记》中,专栏托管在语雀平台上(更方便你沉浸式阅读和做笔记,数学表达公式也能显示出来),访问地址和密码:https://t.zsxq.com/6iuzn6I

题意

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

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

难度

中等

示例

示例 1:

输入:nums = [1,2,3]

示例 2:

输入:nums = [0]

分析1

这题很容易想到的一个解法,就是先枚举子集的大小,再按照大小找出所有的子集,但是这样子太麻烦了,代码也会非常冗长。我们来观察子集的数量公式。

按照刚刚的想法,我们可以得出子集数量tot和nums的长度n的一个关系式:

再来观察一下,按照这个公式计算出来的tot

n = 0  :  tot = 1

n = 2  :  tot = 1 + 2 + 1 = 4n = 3  :  tot = 1 + 3 + 3 + 1 = 8n = 4  :  tot = 1 + 4 + 6 + 4 + 1 = 16...

咦,怎么有点杨辉三角的影子?

是的,没错,我们得出了这样一个结论:

那既然子集的总数是 2 的 n 次方,很容易让我们联想到一种新的枚举方式——二进制枚举!

我们知道,里面,每个数字都不一样,自然,每个数字的二进制表示也是不一样的,如果我们能把每个数字的二进制位上的0和1分别表示该位上的数字是取还是不取,就能得到种互不相同的取法。

然后只需要按照上述的二进制来选择,就可以快速获取所有的子集了。具体我们来看代码实现:

class Solution {

    List<List> res = new ArrayList<>();
    public List<List> subsets(int[] nums) {        // 遍历所有可能的子集组合(从0到2^nums.length-1)        // 例如,如果nums长度为3,则有2^3=8种可能的子集        for(int i = 0; i < (1 << nums.length); i++){            List list = new ArrayList<>();            // 遍历nums的每一个数字,判断它是否应该出现在当前子集中            for(int j = 0; j < nums.length; j++){                // 使用位运算判断j位置的数字是否应该加入到子集中                // 如果i的第j位是1,那么nums[j]就应该在当前子集中                if((i & (1 << j)) != 0){                    list.add(nums[j]);                }            }            // 添加当前子集到结果中            res.add(list);        }        return res;    }}

我们在外层用一个循环,枚举了次,然后再依次遍历0-(n-1),如果当前位是1那么我们就将其加入list中,最后返回结果。

在上述的 LeetCode 题解中,我们正在检查数字i的第j位。为了创建一个只在第j位为 1 的掩码,我们使用1 << j。例如,如果j = 2,那么1 << j为100,即数字4。

现在,为了检查i的第j位是否为 1,我们可以使用与操作:i & (1 << j)。

  • 如果结果是 0,那么i的第j位是 0。
  • 如果结果不是 0,那么i的第j位是 1。

这就是为什么在题解中,我们使用if ((i & (1 << j)) != 0)来判断i的第j位是否为 1,进而确定nums[j]是否应该包含在当前子集中的原因。

最后来看一下运行效率:

分析2

可能涉及到位运算对一些同学来说还是比较陌生的,我们再来看一种更加直观的解法——回溯算法。

回溯算法是一个试错的方法,用于寻找所有(或某些)解决特定问题的方案。假如你尝试构建一个解决方案,但你发现当前的解决方案无法成功,那就撤销最后一步(或多步),然后在做其他尝试。简而言之,它涉及选择,执行和撤销。

回溯算法的主要特点包括:

  • 深度优先:回溯算法通常使用递归来解决问题,这意味着它使用深度优先策略来遍历解决方案的空间。
  • 试错:如果某一路径不是正确的解决方案(或它不可能导致正确的解决方案),那么算法会退回一步或多步,并尝试其他路径。
  • 方案空间树:你可以将可能的解决方案视为一棵树,其中每个节点是基于前一个选择做出的一个选择。回溯算法遍历这棵树来找到解决方案,从根到叶子,尝试所有可能的路径。

常见的问题,可以使用回溯算法来解决,包括但不限于:

  • 组合问题(如从n个元素中选择k个元素的所有可能组合)。
  • 排列问题(如所有可能的字母排列)。
  • N皇后问题。
  • 图的着色问题。
  • 解决数独。

回溯算法的基本结构通常如下:

function backtrack(path, choices):
    save path    else:        for choice in choices:            make choice            backtrack(path with choice, updated choices)            undo choice

其中:

  • path 是当前尝试的解决方案或部分解决方案。
  • choices 是你还可以选择的选项或步骤。

回溯算法可以非常强大,但也可能是低效的,因为它可能需要探索解决方案空间的所有路径。在实践中,许多优化策略(如剪枝)可以用来提高回溯算法的效率。

下面我们就用回溯算法来试一下:

  • 初始化一个空列表来存储所有的子集。
  • 使用递归方法从数组的第一个元素开始考虑每个元素:要么选择它,要么不选择它。
  • 如果达到数组的末尾,我们就把当前的子集添加到结果列表中。
  • 如果没有达到数组的末尾,我们先添加当前元素,然后进行递归,再回溯(即移除刚才添加的元素),然后再进行递归。

下面是实现这个思路的代码:

class Solution {

        public List<List> subsets(int[] nums) {        backtrack(nums, new ArrayList<>(), 0);        return res;    }        public void backtrack(int[] nums, List current, int start) {        // 添加当前子集到结果        res.add(new ArrayList<>(current));                for (int i = start; i < nums.length; i++) {            // 选择当前数字            current.add(nums[i]);                        // 向前探索            backtrack(nums, current, i + 1);                        // 回溯:撤回刚才的选择,以考虑不选择当前数字的情况            current.remove(current.size() - 1);        }    }}

解释:

  • backtrack 方法开始时,首先将 current 当前子集添加到结果 res 中。
  • 然后,我们考虑数组中的每一个数字。对于每一个数字,我们有两种选择:要么选择它,要么跳过它。如果选择它,我们就把它添加到当前子集中,并继续向前递归;完成递归后,我们需要回溯,即移除这个数字,并再次递归。
  • 这样,我们能确保结果中的每一个子集都被考虑到。

来看一下运行效率:

为了方便大家理解,我这里把代码加完整,直接以 ACM 的模式(带输入输出)来看一下。

import java.util.ArrayList;

public class SubsetsSolution {
    public static void main(String[] args) {        // 从命令行参数中解析输入        int[] nums = new int[args.length];        for (int i = 0; i < args.length; i++) {            nums[i] = Integer.parseInt(args[i]);        }
        SubsetsSolution solution = new SubsetsSolution();        List<List> result = solution.subsets(nums);
        // 打印结果        for (List subset : result) {            System.out.println(subset);        }    }
    public List<List> subsets(int[] nums) {        List<List> res = new ArrayList<>();        backtrack(nums, new ArrayList<>(), 0, res);        return res;    }
    public void backtrack(int[] nums, List current, int start, List<List> res) {        res.add(new ArrayList<>(current));        for (int i = start; i < nums.length; i++) {            current.add(nums[i]);            backtrack(nums, current, i + 1, res);            current.remove(current.size() - 1);        }    }}

OK,我们在 run configuration 中添加 123 参数,然后来看一下运行结果。

结果如下:

[]

[1, 2][1, 2, 3][1, 3][2][2, 3][3]

总结

二进制枚举是我们在一些排列组合中需要统计确切的集合情况的时候经常遇到的一种解题方法,通常能够使我们的代码更加简洁,并且能够节省一些没有必要的去重的问题。

力扣链接:https://leetcode.cn/problems/subsets/

本题解同时收录在星球专栏《二哥的 LeetCode 刷题笔记》中,专栏托管在语雀平台上(更方便你沉浸式阅读和做笔记,数学表达公式也能显示出来),访问地址和密码:https://t.zsxq.com/6iuzn6I

题意

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

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

难度

中等

示例

示例 1:

输入:nums = [1,2,3]

示例 2:

输入:nums = [0]

分析1

这题很容易想到的一个解法,就是先枚举子集的大小,再按照大小找出所有的子集,但是这样子太麻烦了,代码也会非常冗长。我们来观察子集的数量公式。

按照刚刚的想法,我们可以得出子集数量tot和nums的长度n的一个关系式:

再来观察一下,按照这个公式计算出来的tot

n = 0  :  tot = 1

n = 2  :  tot = 1 + 2 + 1 = 4n = 3  :  tot = 1 + 3 + 3 + 1 = 8n = 4  :  tot = 1 + 4 + 6 + 4 + 1 = 16...

咦,怎么有点杨辉三角的影子?

是的,没错,我们得出了这样一个结论:

那既然子集的总数是 2 的 n 次方,很容易让我们联想到一种新的枚举方式——二进制枚举!

我们知道,里面,每个数字都不一样,自然,每个数字的二进制表示也是不一样的,如果我们能把每个数字的二进制位上的0和1分别表示该位上的数字是取还是不取,就能得到种互不相同的取法。

然后只需要按照上述的二进制来选择,就可以快速获取所有的子集了。具体我们来看代码实现:

class Solution {

    List<List> res = new ArrayList<>();
    public List<List> subsets(int[] nums) {        // 遍历所有可能的子集组合(从0到2^nums.length-1)        // 例如,如果nums长度为3,则有2^3=8种可能的子集        for(int i = 0; i < (1 << nums.length); i++){            List list = new ArrayList<>();            // 遍历nums的每一个数字,判断它是否应该出现在当前子集中            for(int j = 0; j < nums.length; j++){                // 使用位运算判断j位置的数字是否应该加入到子集中                // 如果i的第j位是1,那么nums[j]就应该在当前子集中                if((i & (1 << j)) != 0){                    list.add(nums[j]);                }            }            // 添加当前子集到结果中            res.add(list);        }        return res;    }}

我们在外层用一个循环,枚举了次,然后再依次遍历0-(n-1),如果当前位是1那么我们就将其加入list中,最后返回结果。

在上述的 LeetCode 题解中,我们正在检查数字i的第j位。为了创建一个只在第j位为 1 的掩码,我们使用1 << j。例如,如果j = 2,那么1 << j为100,即数字4。

现在,为了检查i的第j位是否为 1,我们可以使用与操作:i & (1 << j)。

  • 如果结果是 0,那么i的第j位是 0。
  • 如果结果不是 0,那么i的第j位是 1。

这就是为什么在题解中,我们使用if ((i & (1 << j)) != 0)来判断i的第j位是否为 1,进而确定nums[j]是否应该包含在当前子集中的原因。

最后来看一下运行效率:

分析2

可能涉及到位运算对一些同学来说还是比较陌生的,我们再来看一种更加直观的解法——回溯算法。

回溯算法是一个试错的方法,用于寻找所有(或某些)解决特定问题的方案。假如你尝试构建一个解决方案,但你发现当前的解决方案无法成功,那就撤销最后一步(或多步),然后在做其他尝试。简而言之,它涉及选择,执行和撤销。

回溯算法的主要特点包括:

  • 深度优先:回溯算法通常使用递归来解决问题,这意味着它使用深度优先策略来遍历解决方案的空间。
  • 试错:如果某一路径不是正确的解决方案(或它不可能导致正确的解决方案),那么算法会退回一步或多步,并尝试其他路径。
  • 方案空间树:你可以将可能的解决方案视为一棵树,其中每个节点是基于前一个选择做出的一个选择。回溯算法遍历这棵树来找到解决方案,从根到叶子,尝试所有可能的路径。

常见的问题,可以使用回溯算法来解决,包括但不限于:

  • 组合问题(如从n个元素中选择k个元素的所有可能组合)。
  • 排列问题(如所有可能的字母排列)。
  • N皇后问题。
  • 图的着色问题。
  • 解决数独。

回溯算法的基本结构通常如下:

function backtrack(path, choices):
    save path    else:        for choice in choices:            make choice            backtrack(path with choice, updated choices)            undo choice

其中:

  • path 是当前尝试的解决方案或部分解决方案。
  • choices 是你还可以选择的选项或步骤。

回溯算法可以非常强大,但也可能是低效的,因为它可能需要探索解决方案空间的所有路径。在实践中,许多优化策略(如剪枝)可以用来提高回溯算法的效率。

下面我们就用回溯算法来试一下:

  • 初始化一个空列表来存储所有的子集。
  • 使用递归方法从数组的第一个元素开始考虑每个元素:要么选择它,要么不选择它。
  • 如果达到数组的末尾,我们就把当前的子集添加到结果列表中。
  • 如果没有达到数组的末尾,我们先添加当前元素,然后进行递归,再回溯(即移除刚才添加的元素),然后再进行递归。

下面是实现这个思路的代码:

class Solution {

        public List<List> subsets(int[] nums) {        backtrack(nums, new ArrayList<>(), 0);        return res;    }        public void backtrack(int[] nums, List current, int start) {        // 添加当前子集到结果        res.add(new ArrayList<>(current));                for (int i = start; i < nums.length; i++) {            // 选择当前数字            current.add(nums[i]);                        // 向前探索            backtrack(nums, current, i + 1);                        // 回溯:撤回刚才的选择,以考虑不选择当前数字的情况            current.remove(current.size() - 1);        }    }}

解释:

  • backtrack 方法开始时,首先将 current 当前子集添加到结果 res 中。
  • 然后,我们考虑数组中的每一个数字。对于每一个数字,我们有两种选择:要么选择它,要么跳过它。如果选择它,我们就把它添加到当前子集中,并继续向前递归;完成递归后,我们需要回溯,即移除这个数字,并再次递归。
  • 这样,我们能确保结果中的每一个子集都被考虑到。

来看一下运行效率:

为了方便大家理解,我这里把代码加完整,直接以 ACM 的模式(带输入输出)来看一下。

import java.util.ArrayList;

public class SubsetsSolution {
    public static void main(String[] args) {        // 从命令行参数中解析输入        int[] nums = new int[args.length];        for (int i = 0; i < args.length; i++) {            nums[i] = Integer.parseInt(args[i]);        }
        SubsetsSolution solution = new SubsetsSolution();        List<List> result = solution.subsets(nums);
        // 打印结果        for (List subset : result) {            System.out.println(subset);        }    }
    public List<List> subsets(int[] nums) {        List<List> res = new ArrayList<>();        backtrack(nums, new ArrayList<>(), 0, res);        return res;    }
    public void backtrack(int[] nums, List current, int start, List<List> res) {        res.add(new ArrayList<>(current));        for (int i = start; i < nums.length; i++) {            current.add(nums[i]);            backtrack(nums, current, i + 1, res);            current.remove(current.size() - 1);        }    }}

OK,我们在 run configuration 中添加 123 参数,然后来看一下运行结果。

结果如下:

[]

[1, 2][1, 2, 3][1, 3][2][2, 3][3]

总结

二进制枚举是我们在一些排列组合中需要统计确切的集合情况的时候经常遇到的一种解题方法,通常能够使我们的代码更加简洁,并且能够节省一些没有必要的去重的问题。

力扣链接:https://leetcode.cn/problems/subsets/

本题解同时收录在星球专栏《二哥的 LeetCode 刷题笔记》中,专栏托管在语雀平台上(更方便你沉浸式阅读和做笔记,数学表达公式也能显示出来),访问地址和密码:https://t.zsxq.com/6iuzn6I

题意

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

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

难度

中等

示例

示例 1:

输入:nums = [1,2,3]

示例 2:

输入:nums = [0]

分析1

这题很容易想到的一个解法,就是先枚举子集的大小,再按照大小找出所有的子集,但是这样子太麻烦了,代码也会非常冗长。我们来观察子集的数量公式。

按照刚刚的想法,我们可以得出子集数量tot和nums的长度n的一个关系式:

再来观察一下,按照这个公式计算出来的tot

n = 0  :  tot = 1

n = 2  :  tot = 1 + 2 + 1 = 4n = 3  :  tot = 1 + 3 + 3 + 1 = 8n = 4  :  tot = 1 + 4 + 6 + 4 + 1 = 16...

咦,怎么有点杨辉三角的影子?

是的,没错,我们得出了这样一个结论:

那既然子集的总数是 2 的 n 次方,很容易让我们联想到一种新的枚举方式——二进制枚举!

我们知道,里面,每个数字都不一样,自然,每个数字的二进制表示也是不一样的,如果我们能把每个数字的二进制位上的0和1分别表示该位上的数字是取还是不取,就能得到种互不相同的取法。

然后只需要按照上述的二进制来选择,就可以快速获取所有的子集了。具体我们来看代码实现:

class Solution {

    List<List> res = new ArrayList<>();
    public List<List> subsets(int[] nums) {        // 遍历所有可能的子集组合(从0到2^nums.length-1)        // 例如,如果nums长度为3,则有2^3=8种可能的子集        for(int i = 0; i < (1 << nums.length); i++){            List list = new ArrayList<>();            // 遍历nums的每一个数字,判断它是否应该出现在当前子集中            for(int j = 0; j < nums.length; j++){                // 使用位运算判断j位置的数字是否应该加入到子集中                // 如果i的第j位是1,那么nums[j]就应该在当前子集中                if((i & (1 << j)) != 0){                    list.add(nums[j]);                }            }            // 添加当前子集到结果中            res.add(list);        }        return res;    }}

我们在外层用一个循环,枚举了次,然后再依次遍历0-(n-1),如果当前位是1那么我们就将其加入list中,最后返回结果。

在上述的 LeetCode 题解中,我们正在检查数字i的第j位。为了创建一个只在第j位为 1 的掩码,我们使用1 << j。例如,如果j = 2,那么1 << j为100,即数字4。

现在,为了检查i的第j位是否为 1,我们可以使用与操作:i & (1 << j)。

  • 如果结果是 0,那么i的第j位是 0。
  • 如果结果不是 0,那么i的第j位是 1。

这就是为什么在题解中,我们使用if ((i & (1 << j)) != 0)来判断i的第j位是否为 1,进而确定nums[j]是否应该包含在当前子集中的原因。

最后来看一下运行效率:

分析2

可能涉及到位运算对一些同学来说还是比较陌生的,我们再来看一种更加直观的解法——回溯算法。

回溯算法是一个试错的方法,用于寻找所有(或某些)解决特定问题的方案。假如你尝试构建一个解决方案,但你发现当前的解决方案无法成功,那就撤销最后一步(或多步),然后在做其他尝试。简而言之,它涉及选择,执行和撤销。

回溯算法的主要特点包括:

  • 深度优先:回溯算法通常使用递归来解决问题,这意味着它使用深度优先策略来遍历解决方案的空间。
  • 试错:如果某一路径不是正确的解决方案(或它不可能导致正确的解决方案),那么算法会退回一步或多步,并尝试其他路径。
  • 方案空间树:你可以将可能的解决方案视为一棵树,其中每个节点是基于前一个选择做出的一个选择。回溯算法遍历这棵树来找到解决方案,从根到叶子,尝试所有可能的路径。

常见的问题,可以使用回溯算法来解决,包括但不限于:

  • 组合问题(如从n个元素中选择k个元素的所有可能组合)。
  • 排列问题(如所有可能的字母排列)。
  • N皇后问题。
  • 图的着色问题。
  • 解决数独。

回溯算法的基本结构通常如下:

function backtrack(path, choices):
    save path    else:        for choice in choices:            make choice            backtrack(path with choice, updated choices)            undo choice

其中:

  • path 是当前尝试的解决方案或部分解决方案。
  • choices 是你还可以选择的选项或步骤。

回溯算法可以非常强大,但也可能是低效的,因为它可能需要探索解决方案空间的所有路径。在实践中,许多优化策略(如剪枝)可以用来提高回溯算法的效率。

下面我们就用回溯算法来试一下:

  • 初始化一个空列表来存储所有的子集。
  • 使用递归方法从数组的第一个元素开始考虑每个元素:要么选择它,要么不选择它。
  • 如果达到数组的末尾,我们就把当前的子集添加到结果列表中。
  • 如果没有达到数组的末尾,我们先添加当前元素,然后进行递归,再回溯(即移除刚才添加的元素),然后再进行递归。

下面是实现这个思路的代码:

class Solution {

        public List<List> subsets(int[] nums) {        backtrack(nums, new ArrayList<>(), 0);        return res;    }        public void backtrack(int[] nums, List current, int start) {        // 添加当前子集到结果        res.add(new ArrayList<>(current));                for (int i = start; i < nums.length; i++) {            // 选择当前数字            current.add(nums[i]);                        // 向前探索            backtrack(nums, current, i + 1);                        // 回溯:撤回刚才的选择,以考虑不选择当前数字的情况            current.remove(current.size() - 1);        }    }}

解释:

  • backtrack 方法开始时,首先将 current 当前子集添加到结果 res 中。
  • 然后,我们考虑数组中的每一个数字。对于每一个数字,我们有两种选择:要么选择它,要么跳过它。如果选择它,我们就把它添加到当前子集中,并继续向前递归;完成递归后,我们需要回溯,即移除这个数字,并再次递归。
  • 这样,我们能确保结果中的每一个子集都被考虑到。

来看一下运行效率:

为了方便大家理解,我这里把代码加完整,直接以 ACM 的模式(带输入输出)来看一下。

import java.util.ArrayList;

public class SubsetsSolution {
    public static void main(String[] args) {        // 从命令行参数中解析输入        int[] nums = new int[args.length];        for (int i = 0; i < args.length; i++) {            nums[i] = Integer.parseInt(args[i]);        }
        SubsetsSolution solution = new SubsetsSolution();        List<List> result = solution.subsets(nums);
        // 打印结果        for (List subset : result) {            System.out.println(subset);        }    }
    public List<List> subsets(int[] nums) {        List<List> res = new ArrayList<>();        backtrack(nums, new ArrayList<>(), 0, res);        return res;    }
    public void backtrack(int[] nums, List current, int start, List<List> res) {        res.add(new ArrayList<>(current));        for (int i = start; i < nums.length; i++) {            current.add(nums[i]);            backtrack(nums, current, i + 1, res);            current.remove(current.size() - 1);        }    }}

OK,我们在 run configuration 中添加 123 参数,然后来看一下运行结果。

结果如下:

[]

[1, 2][1, 2, 3][1, 3][2][2, 3][3]

总结

二进制枚举是我们在一些排列组合中需要统计确切的集合情况的时候经常遇到的一种解题方法,通常能够使我们的代码更加简洁,并且能够节省一些没有必要的去重的问题。

力扣链接:https://leetcode.cn/problems/subsets/

            知识星球
            
    
    
        
        扫码加入星球
        查看更多优质内容
    
    https://wx.zsxq.com/mweb/views/joingroup/join_group.html?group_id=15522885221412
078.子集
https://clxhxhhr.top/posts/2532/
作者
clxstart
发布于
2026-09-19
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。
☰ 文章目录