文章

[LeetCode] 每日一题 1863. 找出所有子集的异或总和再求和(递归法 | 数学推导法)

题目描述

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

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

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

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

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

题目链接

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

示例输入

示例 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

解题思路

今天这题虽然是标注的简单题,但我觉得非常值得深入研究。

一开始我是从暴力的角度出发,想到用回溯 + DFS枚举所有子集,每一层递归都表示当前是否选择某个元素,这样自然能遍历出所有子集。在每个子集的末尾(即遍历结束时)计算当前的异或总和并累加到全局变量上。这种方法虽然直观易懂,但时间复杂度为 2^n,递归深度是 O(n),在数据量较小时完全能接受。

不过,后来看题解时,发现还有一种更加巧妙的思路,是通过数学推导 + 位运算来简化计算,时间复杂度从指数级降到了线性级。这让我收获挺大。

这类异或题的关键点在于:异或是按位独立运算,所以我们也可以从每一位单独考虑。对于数组中每一位上至少有一个元素为 1 的情况,最终所有子集异或和在这一位上为 1 的子集数恰好是所有子集数量的一半。结合二项式展开的知识,我们能推导出每个 bit 在所有子集中出现为 1 的次数正好是 2^(n - 1),而该位的结果正好由所有数的“按位或”来决定。

换句话说,所有子集异或和的总和就是(所有元素的按位或) × 2^(n - 1)

这种简化令人惊艳,也让我意识到在某些场景下,从数学角度去分析会更高效、也更优雅😍

代码实现

方法一

class Solution {
    int sum = 0;

    public int subsetXORSum(int[] nums) {
        dfs(nums, 0, 0);
        return sum;
    }

    private void dfs(int[] nums, int index, int value) {
        if (index == nums.length) {
            sum += value;
            return;
        }
        dfs(nums, index + 1, value ^ nums[index]);
        dfs(nums, index + 1, value);
    }
}

方法二

class Solution {
    public int subsetXORSum(int[] nums) {
        int ans = 0;
        int n = nums.length;
        for (int num : nums) {
            ans |= num;
        }
        return ans << (n - 1);
    }
}

复杂度分析

解法一(递归法)

  • 时间复杂度:O(2^n),每个元素都有选和不选两种可能

  • 空间复杂度:O(n),递归栈深度最多为 n

解法二(数学推导)

  • 时间复杂度:O(n),只遍历一遍数组即可

  • 空间复杂度:O(1),只使用了常数级变量

总结

这道题让我对“异或”和“子集枚举”的结合方式有了更深理解。虽然暴力法是最直接的,但通过数学思维分析按位贡献并结合组合数学进行优化,是我今天最大的收获。以后在做位运算相关场景时,我会优先考虑是否可以按位进行分析,这样也许能快速找到更优解法 🤗

希望这篇分享能为你带来启发!如果你有任何问题或建议,欢迎在评论区留言,与我共同交流探讨。

License:  CC BY 4.0