LeetCode算法日记 - Day 55: 子集、找出所有子集的异或总和再求和

目录

[1. 子集](#1. 子集)

[1.1 题目解析](#1.1 题目解析)

[1.2 解法](#1.2 解法)

[1.3 代码实现](#1.3 代码实现)

[2. 找出所有子集的异或总和再求和](#2. 找出所有子集的异或总和再求和)

[2.1 题目解析](#2.1 题目解析)

[2.2 解法](#2.2 解法)

[2.3 代码实现](#2.3 代码实现)


1. 子集

https://leetcode.cn/problems/subsets/description/

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

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

示例 1:

复制代码
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

复制代码
输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

1.1 题目解析

题目本质

生成给定集合的幂集(所有子集)。对一个包含 n 个互不相同元素的集合,子集总数必然是 2^n。
常规解法

枚举"每个元素要/不要",把做出的选择拼成子集。
问题分析

朴素地"列举所有组合"如果没有系统性,会漏解或重复;而且需要一套机制来有序地遍历所有选择状态。复杂度下界就是输出规模 2^n量级,时间至少 O(2^n)。
思路转折

想系统地覆盖所有"要/不要"的选择树 → 用回溯 DFS,每层决定当前位置是否选入,天然避免重复;

1.2 解法

算法思想

回溯:从索引 k 开始,先将当前 path 加入 result;对 i = k 到 n-1,尝试加入 nums[i],递归到 i+1,再回退。

**i)**维护全局 result(答案)与 path(当前子集)。

**ii)**定义 dfs(nums, k):先把 path 的拷贝加入 result。

**iii)**从 i = k 到 n-1:

  • 把 nums[i] 加入 path;

  • 递归 dfs(nums, i+1);

  • 回退:移除 path 尾元素。。

**iv)**入口调用 dfs(nums, 0),返回 result。

易错点

  • 加入答案时必须拷贝当前路径(new ArrayList<>(path)),否则后续回退会改动已存结果。

  • 回溯后记得弹栈(remove(path.size()-1)),否则路径会"越堆越长"。

  • 题目无序要求,不必排序;若要字典序输出,可先 Arrays.sort(nums)。

1.3 代码实现

java 复制代码
import java.util.*;

class Solution {
    List<List<Integer>> result;
    List<Integer> path;

    public List<List<Integer>> subsets(int[] nums) {
        result = new ArrayList<>();
        path = new ArrayList<>();
        dfs(nums, 0);
        return result;
    }

    private void dfs(int[] nums, int k) {
        // 1) 任何时刻的路径都是一个合法子集
        result.add(new ArrayList<>(path));

        // 2) 从当前位置开始,依次尝试选择一个元素加入
        for (int i = k; i < nums.length; i++) {
            path.add(nums[i]);           // 选择
            dfs(nums, i + 1);            // 递归到下一个起点
            path.remove(path.size() - 1);// 回退
        }
    }
}

复杂度分析

  • 时间复杂度:O(n⋅2^n)

  • 空间复杂度:O(n)

2. 找出所有子集的异或总和再求和

https://leetcode.cn/problems/sum-of-all-subset-xor-totals/description/

一个数组的异或总和 定义为数组中所有元素按位 XOR 的结果;如果数组为 ,则异或总和为 0

  • 例如,数组 [2,5,6]异或总和2 XOR 5 XOR 6 = 1

给你一个数组 nums ,请你求出 nums 中每个 子集异或总和 ,计算并返回这些值相加之

注意: 在本题中,元素 相同 的不同子集应 多次 计数。

数组 a 是数组 b 的一个 子集 的前提条件是:从 b 删除几个(也可能不删除)元素能够得到 a

示例 1:

复制代码
输入:nums = [1,3]
输出:6
解释:[1,3] 共有 4 个子集:
- 空子集的异或总和是 0 。
- [1] 的异或总和为 1 。
- [3] 的异或总和为 3 。
- [1,3] 的异或总和为 1 XOR 3 = 2 。
0 + 1 + 3 + 2 = 6

示例 2:

复制代码
输入:nums = [5,1,6]
输出:28
解释:[5,1,6] 共有 8 个子集:
- 空子集的异或总和是 0 。
- [5] 的异或总和为 5 。
- [1] 的异或总和为 1 。
- [6] 的异或总和为 6 。
- [5,1] 的异或总和为 5 XOR 1 = 4 。
- [5,6] 的异或总和为 5 XOR 6 = 3 。
- [1,6] 的异或总和为 1 XOR 6 = 7 。
- [5,1,6] 的异或总和为 5 XOR 1 XOR 6 = 2 。
0 + 5 + 1 + 6 + 4 + 3 + 7 + 2 = 28

示例 3:

复制代码
输入:nums = [3,4,5,6,7,8]
输出:480
解释:每个子集的全部异或总和值之和为 480 。

提示:

  • 1 <= nums.length <= 12
  • 1 <= nums[i] <= 20

2.1 题目解析

题目本质

计算"所有子集的按位异或值之和"。
常规解法

先生成全部子集,再逐个子集把元素异或起来累加到答案。
问题分析

子集数是2^n。暴力遍历并对每个子集做一次线性异或,时间下界 O(n⋅2^n)。若把子集都存到内存中(如 ret),还需要 O( O(n⋅2^n) 空间。
思路转折

用回溯生成所有子集,计算 XOR 并累加;

2.2 解法

算法思想

回溯:维护全局 ret(收集子集)与 path(当前路径)。dfs(nums, k) 先把 path 拷贝进 ret,再枚举 i=k..n-1:加入 nums[i] → 递归到 i+1 → 回退。最终遍历 ret,对子集元素做异或并累加。
**i)**准备全局容器:ret = new ArrayList<>(); path = new ArrayList<>();

**ii)**调用 dfs(nums, 0) 生成所有子集:

  • 把 path 的拷贝放入 ret;

  • 从 i = k 到 n-1:path.add(nums[i]) → dfs(nums, i+1) → path.remove(...) 回退。

**iii)**遍历 ret:对每个子集线性求异或 xor ^= x,并做 result += xor。

**iv)**返回 result。

易错点

  • 加入答案时要拷贝 path:new ArrayList<>(path),避免回溯改动已存结果。

2.3 代码实现

java 复制代码
import java.util.*;

class Solution {
    List<List<Integer>> ret;
    List<Integer> path;

    public int subsetXORSum(int[] nums) {
        int result = 0;
        ret = new ArrayList<>();
        path = new ArrayList<>();

        // 可选:排序对异或结果无影响,通常无需
        // Arrays.sort(nums);

        dfs(nums, 0);

        // 遍历每个子集,计算其异或值并累加
        for (List<Integer> cur : ret) {
            int xor = 0;
            for (int x : cur) {
                xor ^= x;
            }
            result += xor;
        }
        return result;
    }

    // 回溯生成所有子集:从索引 k 开始做选择
    private void dfs(int[] nums, int k) {
        // 当前路径就是一个合法子集
        ret.add(new ArrayList<>(path));

        // 从 k 开始依次选择一个元素加入
        for (int i = k; i < nums.length; i++) {
            path.add(nums[i]);         // 选择
            dfs(nums, i + 1);          // 递归到下一个起点
            path.remove(path.size() - 1); // 回退
        }
    }
}

复杂度分析

  • 时间复杂度:生成子集 O(2^n)

  • 空间复杂度:回溯栈与路径O(n),若计入 ret 存储子集,空间 O(n⋅2^n)

相关推荐
熬了夜的程序员2 小时前
【LeetCode】48. 旋转图像
算法·leetcode·链表·职场和发展·深度优先
Q741_1472 小时前
C++ 位运算 高频面试考点 力扣 268. 丢失的数字 题解 每日一题
c++·算法·leetcode·面试·位运算
未知陨落2 小时前
LeetCode:79.跳跃游戏Ⅱ
算法·leetcode
未知陨落2 小时前
LeetCode:74.数组中的第K个最大元素
算法·leetcode
电子_咸鱼2 小时前
LeetCode-hot100——验证二叉搜索树
开发语言·数据结构·c++·算法·leetcode·深度优先
Miraitowa_cheems2 小时前
LeetCode算法日记 - Day 56: 全排列II、话号码的字母组合
数据结构·算法·leetcode·决策树·链表·职场和发展·深度优先
潼心1412o2 小时前
数据结构(长期更新)第1讲:算法复杂度
数据结构
Imxyk2 小时前
Codeforces Round 1052 (Div. 2) C. Wrong Binary Searchong Binary Search
c语言·c++·算法
Yunfeng Peng2 小时前
1- 十大排序算法(选择排序、冒泡排序、插入排序)
java·算法·排序算法