「力扣」第 78、90 题:子集、子集 II(回溯算法)题解

「力扣」第 78 题:“子集”问题描述

给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例:

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

思路 1

  • 画出树形图:按照“一个数可以选,也可以不选”的思路,画出如下树形图;

「力扣」第 78 题:子集(回溯算法)题解-1

  • 结果出现在哪里?所有符合条件的结果出现在叶子结点中。
  • 使用深度优先遍历需要的状态变量:1、当前考虑的是第几个数 index;2、从根结点到叶子结点的路径 path ,不难分析出它是一个栈。

Java 代码:

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class Solution {

    public List<List<Integer>> subsets(int[] nums) {
        int len = nums.length;

        List<List<Integer>> res = new ArrayList<>();
        if (len == 0){
            return res;
        }

        Deque<Integer> path = new ArrayDeque<>();
        dfs(nums, len, 0, path, res);
        return res;
    }

    private void dfs(int[] nums, int len, int index, Deque<Integer> path, List<List<Integer>> res) {
        if (index == len){
            res.add(new ArrayList<>(path));
            return;
        }

        path.addLast(nums[index]);
        dfs(nums, len, index + 1, path, res);
        path.removeLast();

        dfs(nums, len, index + 1, path, res);
    }
}

使用示例 [1, 2, 3] 写一段测试代码运行一下,输出结果:

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

符合我们画出的树形图。

复杂度分析

  • 时间复杂度: O ( N × 2 N ) O(N \times 2^N) ,这里 N N 为数组的长度,叶子结点一共有 2 N 2^N 个,树的高度为 N N
  • 空间复杂度: O ( N × 2 N ) O(N \times 2^N) ,理由同时间复杂度。保存子集需要长度为 2 N 2^N 的列表,每一个子集的元素最多长度为 N N

思路 2

  • 画出树形图:按照“按照每一层选出一个数产生分支”的思路,可以画出如下树形图;

「力扣」第 78 题:子集(回溯算法)题解-2

  • 结果出现在哪里?所有的结点都是符合条件的结果。
  • 使用深度优先遍历需要的状态变量:1、从候选数组的哪一个下标开始搜索 start;2、从根结点到叶子结点的路径 path ,这个变量我们多次遇到了。

Java 代码:

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

public class Solution {

    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        int len = nums.length;
        if (len == 0) {
            return res;
        }

        Deque<Integer> path = new ArrayDeque<>();
        dfs(nums, 0, len, path, res);
        return res;
    }

    private void dfs(int[] nums, int begin, int len, Deque<Integer> path, List<List<Integer>> res) {
        // 在遍历的过程中,收集符合条件的结果
        res.add(new ArrayList<>(path));
        for (int i = begin; i < len; i++) {
            path.addLast(nums[i]);
            dfs(nums, i + 1, len, path, res);
            path.removeLast();
        }
    }
}

复杂度分析

  • 时间复杂度: O ( 2 N ) O(2^N) ,整棵树的结点个数一共是 2 N 2^N 个。
  • 空间复杂度: O ( N × 2 N ) O(N \times 2^N) ,保存子集需要长度为 2 N 2^N 的列表,每一个子集的元素最多长度为 N N

思路 3

  • 每一个候选数选与不选,这恰恰好是计算机世界里二进制数能够表示的含义,1 表示选择,0 表示不选;
  • 因此,我们可以枚举数组长度的二进制数的所有可能十进制值,按照每一个数位的值枚举所有的可能性(这句话没有说得很准确,大家领会意思即可)。

「力扣」第 78 题:子集(回溯算法)题解-3

Java 代码:

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

public class Solution {

    public List<List<Integer>> subsets(int[] nums) {
        int size = nums.length;
        int n = 1 << size;
        List<List<Integer>> res = new ArrayList<>();

        for (int i = 0; i < n; i++) {
            List<Integer> cur = new ArrayList<>();
            for (int j = 0; j < size; j++) {
                if (((i >> j) & 1) == 1) {
                    cur.add(nums[j]);
                }
            }
            res.add(cur);
        }
        return res;
    }
}

复杂度分析

  • 时间复杂度: O ( N × 2 N ) O(N \times 2^N) ,这里 N N 为数组的长度,叶子结点一共有 2 N 2^N 个子集,遍历每一个子集所代表的二进制数有 N N 位。
  • 空间复杂度: O ( N × 2 N ) O(N \times 2^N) ,保存子集需要长度为 2 N 2^N 的列表,每一个子集的元素最多长度为 N N

「力扣」第 90 题:“子集 II”问题描述

给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

说明:解集不能包含重复的子集。

示例

输入: [1, 2, 2]
输出:
[
  [2],
  [1],
  [1, 2, 2],
  [2, 2],
  [1, 2],
  []
]

思路

  • 借用「力扣」第 47 题的剪枝思想,先对数组排序,只要搜索起点一样,必然发生重复,此时剪枝即可。

「力扣」第 90 题:子集 II(回溯算法)题解-1

复习一下为什么要先排序:

1、根据题目意思,子集的每个元素其实是不考虑次序的,因此 [1, 2][2, 1] 在结果集里只能保留一个;

2、有重复元素的时候,得到的子集也有重复列表,如何对列表去重呢?排个序以后,再逐个比对,这样做很麻烦,但其实可以在搜索的时候,就排序,然后绕过会产生重复结果的分支。

  • 这里因为是不考虑顺序的,因此搜索的时候按顺序搜索即可,需要设置状态变量 start(正是因为按顺序搜索,因此不用设置布尔数组 used ) 和路径变量 path

Java 代码:

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
import java.util.Stack;

public class Solution {

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        int len = nums.length;
        if (len == 0) {
            return res;
        }
        // 排序是为了后面剪枝去重
        Arrays.sort(nums);

        Deque<Integer> path = new ArrayDeque<>();
        dfs(nums, 0, len, path, res);
        return res;
    }

    private void dfs(int[] nums, int start, int len, Deque<Integer> path, List<List<Integer>> res) {
        res.add(new ArrayList<>(path));

        for (int i = start; i < len; i++) {
            // 常见的剪枝操作
            if (i > start && nums[i] == nums[i - 1]) {
                continue;
            }

            path.addLast(nums[i]);

            // 从 i + 1 开始继续枚举,按顺序枚举,所以不会重复
            dfs(nums, i + 1, len, path, res);
            path.removeLast();
        }
    }
}

复杂度分析:(理由同「力扣」第 78 题思路 2)

  • 时间复杂度: O ( 2 N ) O(2^N) ,整棵树的结点个数最多 2 N 2^N 个。
  • 空间复杂度: O ( N × 2 N ) O(N \times 2^N) ,保存子集需要长度为 2 N 2^N 的列表,每一个子集的元素最多长度为 N N

总结

  • 找出所有可能的解,这样的问题一般用回溯算法解决;
  • 回溯算法能解决的问题的特点是:解决一个问题有多种办法,每一个办法又分为多个步骤,我们使用遍历的方法得到所有的结果;
  • 解决回溯算法首先要画出树形图,以打开思路,进而考虑编码,考虑剪枝条件怎么写;
  • 一般画树形图的时候都是一层一层画,这是人的思维,但是编码的时候,用深度优先遍历,这是计算机的思维,好处在《全排列》这个专辑里已经介绍了(借用系统栈、全程可以使用一份状态变量,节约空间)。

(如果有写得不详细的地方和错误的地方,欢迎朋友们指导。)

发布了455 篇原创文章 · 获赞 348 · 访问量 126万+

猜你喜欢

转载自blog.csdn.net/lw_power/article/details/104398118
今日推荐