【LeetCode】组合问题——1863.找出所有子集的异或总和再求和(回溯)
组合问题
- 1863.找出所有子集的异或总和再求和
 - 示例 1
 - 示例 2
 - 示例 3
 - 提示
 
- 解题思路
 - 算法选择
 - 决策树
 - 剪枝
 - 回溯
 
- 编写代码
 - 决策树的构建
 - 剪枝
 - 回溯
 - 完整代码展示
 
- 代码测试
 

1863.找出所有子集的异或总和再求和
相关标签:位运算、数组、数学、回溯、枚举、组合数学、第241场周赛
 题目难度:简单
 题目描述
 一个数组的 异或总和 定义为数组中所有元素按位 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<=121 <= nums.length <= 121<=nums.length<=12
 1<=nums[i]<=201 <= nums[i] <= 201<=nums[i]<=20
解题思路
算法选择
本题实质上是一个组合问题,涉及的标签有很多,今天我们主要介绍的解法是——回溯。
回溯 是以DFS为框架的一整套算法思想。通过回退与剪枝操作来实现试错与撤销选择这一核心步骤。
对于决策问题、组合问题、排列问题……我们都可以通过构建该问题的决策树,从而来解决该问题。
决策树
这道题的核心问题是——如何获取一个数组的全部子集。
为了解决该问题,我们可以通过构建该数组的决策树来进一步探讨;
首先我们需要明确——一个数组的子集可以为空集,那么我们就可以通过构建一棵从空集出发的子集树,来进一步构建其决策树;
对于一个长度为 n 的数组,其子集我们可以按元素数量划分为:
- 元素数量为 
0的空集 - 元素数量为 
1的子集 - 元素数量为 
2的子集 - ⋯\cdots⋯
 - 元素数量为 
n的子集 
其对应的子集树为:
可以看到,树中个结点的层高代表着该结点对应的子集所含有的元素数量,并且树的结点数量 == 数组的子集数量。像这样的一棵树,我们就称其为数组 [1, 2, 3, ..., n] 的子集树。
那么我们应该如何通过子集树构建该数组的决策树呢?
以数组 [5, 1, 6] 为例,接下来我们就来画出其子集树:
在这棵子集树中,我们是如何对每一层结点中的元素进行筛选的呢?
这个问题就是我们构建这个数组的决策树的关键。在决策树中,我们以 开始 这个结点作为树的根结点:
树的第一层,其对应的结点为——元素格式为1的子集,在这个数组中,我们总共有 333 种选择:5, 1, 6 ,每一种选择分别对应一个结点:
树的第二层,对于第一层的结点 5 而言,同样有 333 种选择,每一种选择对应一个结点:
同理,在决策树中,不管对应哪一个结点,我们在进行选择时都会有 333 种选择,因此该数组对应的完整决策树为:
那此时我们得到的决策树是否有效呢?
答案是无效的,因为我们在这个过程中,只是给出了具体的选项,但还没有完成最重要的工作选,那我们应该如何选呢?这里就需要我们通过回溯和剪枝来实现:
对于第一层的元素而言,每一个元素都是必须要选择的,因此我们保留第一层:
从第二层开始,第一层中已经选择过的元素,在第二层中我们就无法重新选择,即:
上图中标红的结点是我们需要进行剪枝操作的结点:
不仅如此,在查找子集的过程中,我们是从左往右进行查找,即,对于第一层中的结点 111 而言,在它前面的结点 555 是已经被查找过的结点,因此,在查找该元素对应子集的第二个元素时,我们也无法查找元素 555,同理,第一层的结点 666 ,其对应的第二层结点中,我们同样无法查找 555 和 111 :
第三层的元素选择也满足:
- 第三层结点对应的父结点无法在第三层再一次被选择
 - 第三层结点对应的父结点的左兄弟结点无法再一次被选择
 
因此第三层的元素最终能够被选择的只有一个结点:
可以看到,最终我们得到的决策树与其子集树是一致的,只不过得到该树的过程就是我们决策的过程,因此我们需要将这一过程通过代码的形式展示出来;
剪枝
在构建决策树的过程中,当我们遇到以下情况时,我们需要将该情况对应的结点给剪掉:
- 该结点为其父结点
 - 该结点为其父结点的左兄弟结点
 
这里我们如何来判断当前选择的结点是否为其父结点或者左兄弟结点呢?
很简单,我们可以通过数组下标实现:
- 当该结点的数组下标 == 父结点的数组下标时,该结点为其父结点
 - 当该结点的数组下标 < 父结点的数组下标时,该结点为其父结点的左兄弟结点
 
现在我们就很明确了,当我们通过数组下标进行选择时,我们需要选择的是数组下标大于当前元素数组下标的所有元素中的一个;
回溯
在本题中,获取子集是第一步,我们最终要获取的是各个子集的异或值,以 [5, 1, 6] 为例,当我们将该数组对应的决策树中的结点元素替换为子集的异或值时,我们就能得到一棵新的决策树:
可以看到,当前结点的异或值 = 当前选择的结点元素与其父结点的异或值。
那现在问题来了,在第三层中,当我记录了结点 111 的异或值 515 ^ 151 之后,我们应该如何记录结点 666 的异或值呢?
这里就需要我们通过异或的运算规则实现:
a ^ b = c
c ^ b = a
 
即当我们对一个值 a 异或同一个数两次时,最终的结果还是为 a。
通过这个规则,我们就可以在获取其他元素的异或值时,回溯到其父结点所对应的异或值;
编写代码
根据上述的解题思路,我们可以需要完成三步操作:
- 决策树的构建
 - 剪枝
 - 回溯
 
在整个过程中,我们需要记录两个值:父结点的异或值,以及子集异或值的总和,因此这里我们可以定义两个变量来进行记录:
ans记录数组的所有子集的总和mid记录当前元素所对应的子集的异或值
int subsetXORSum(int* nums, int numsSize) {int ans = 0, mid = 0;
}
 
这里我们将其均初始化为 0;
决策树的构建
接下来我们就需要构建该数组所对应的决策树,具体的构建思路我们是采用的 dfs 完成,因此对应的函数名我们可以直接使用 dfs ,不过为了更加清楚的描述该问题,这里我选择的是Subset,函数的参数至少有4个:
nums—— 原数组numsSize—— 原数组大小ans—— 记录子集的异或总和mid—— 记录子集的异或值
void Subset(int* nums, int numsSize, int* ans, int* mid) {
}
 
在构建决策树的过程中,我们每一层的结点的孩子结点都是当前结点的右侧元素,因此我们还需要一个变量来记录下一层的起始位置:
void Subset(int* nums, int numsSize, int* ans, int* mid, int start) {
}
 
现在我们就确定了函数头,下面我们就要开始实现该函数的具体细节了;
首先我们的目的是记录数组所有子集的异或值之和,因此我们在获取当前子集对应的下一层子集之前,我们需要先将当前子集的异或值给记录下来,这里我们可以通过:*ans += *mid; 这一累加操作实现;
在选择的过程中,我们实际上就是在完成数组的遍历操作,这里我们可以通过 for 循环来实现,for 循环的起始位置就是我们传入的参数 start:
for (int i = start; i < numsSize; i++) {
}
 
在循环中,我们首先要记录当前元素所对应子集的异或值,这里我们可以通过:*mid ^= nums[i] 实现;
完成记录后,我们需要构建当前子集的孩子子集,这里我们通过递归实现:Subset(nums, numsSize, ans, mid, i + 1);
完成其孩子子集的构建后,我们需要通过回溯操作来找到当前子集的父子集,这里我们通过异或规则实现:*mid ^= nums[i];
之后我们就可以继续通过循环找到该子集的兄弟子集。完整的代码如下所示:
void Subset(int* nums, int numsSize, int* ans, int* mid, int start) {*ans += *mid;for (int i = start; i < numsSize; i++) {*mid ^= nums[i];Subset(nums, numsSize, ans, mid, i + 1);*mid ^= nums[i];}
}
 
剪枝
在前面的思路中我们已经确认了我们在查找一个元素所对应的子集是,我们需要将当前元素及其左侧元素全部给裁剪掉,因此我们通过参数start 记录子集的起始点这个操作就完成了剪枝;
回溯
这个算法中,回溯的目的是为了找到当前子集的父子集所对应的异或值,因此我们通过异或规则实现了回溯;
完整代码展示
完成了函数的主体后,此时的 Subset 函数在运行的过程中,就可以完成记录所有子集的异或值的总和,因此我们在完成函数调用后,直接返回 ans 即可。该题的解题算法完整C语言代码如下:
void Subset(int* nums, int numsSize, int* ans, int* mid, int start) {*ans += *mid;for (int i = start; i < numsSize; i++) {*mid ^= nums[i];Subset(nums, numsSize, ans, mid, i + 1);*mid ^= nums[i];}
}int subsetXORSum(int* nums, int numsSize) {int ans = 0, mid = 0;Subset(nums, numsSize, &ans, &mid, 0);return ans;
}
 
其该思路对应的Python 2.x 代码如下:
class Solution(object):def subsetXORSum(self, nums):""":type nums: List[int]:rtype: int"""ans, mid = [0], [0]numsSize = len(nums)def dfs(numsSize, pos):ans[0] += mid[0]for i in range(pos, numsSize):mid[0] ^= nums[i]dfs(numsSize, i + 1)mid[0] ^= nums[i]dfs(numsSize, 0)return ans[0]
 
代码测试
接下来我们就在 leetcode 中测试一下这两份代码:

可以看到此时我们很好的解决了本题。
