好的,我看了一下列表,发现**“统计美丽子数组的数目(位运算 + 区间DP)”** 这个题目虽然被提及,但没有详细展开过。这是一个非常有趣且能体现区间DP与位运算结合的题目。我来为你详细讲解。
统计美丽子数组的数目(位运算 + 区间DP)
题目描述:
给你一个下标从 0 开始的整数数组 nums,数组的长度为 n。
我们定义 美丽子数组 为:一个数组的 所有 元素做 按位异或(XOR) 运算后,结果等于 0。
请你返回数组 nums 中 美丽子数组 的数目。
注意:
- 子数组是数组中的一个连续非空元素序列。
- 按位异或运算用符号
^表示,其规则为:0^0=0, 1^0=1, 0^1=1, 1^1=0。对多个数连续做异或,顺序不影响最终结果。 - 1 <= n <= 1000 (这是一个典型的使用O(n²)区间DP可以解决的约束范围)
示例 1:
输入:nums = [4, 3, 1, 2, 4]
输出:2
解释:美丽子数组有 [4, 3, 1] (4^3^1 = 6^1 = 7? 等等,这里我算错了,我们重新计算) 和 [2, 4] (2^4 = 6? 也不对)。
让我们重新计算:
[4, 3, 1]: 4 ^ 3 = 7, 7 ^ 1 = 6。结果不是0。[3, 1, 2]: 3 ^ 1 = 2, 2 ^ 2 = 0。这是一个美丽子数组。[1, 2, 4, 3]: 1 ^ 2 = 3, 3 ^ 4 = 7, 7 ^ 3 = 4。不是0。- 实际上,美丽子数组应该是
[3, 1, 2]和[4, 3, 1, 2, 4]整个数组 (因为4^3^1^2^4 = (4^4)^(3^1^2) = 0^(0) = 0)。
所以正确输出是2。这个示例告诉我们,美丽子数组可能出现在任何位置。
示例 2:
输入:nums = [1, 1, 1, 1, 1]
输出:6
解释:任何长度为偶数的子数组,其所有元素的异或和都是0(因为1^1=0)。长度为2的子数组有4个,长度为4的子数组有2个,共6个。注意,长度为1的子数组(如[1])异或和是1,不是美丽子数组。
解题过程循序渐进讲解:
步骤1:理解问题的核心
我们要找的是 连续子数组,其所有元素的 异或和 等于 0。
异或运算有一个非常重要的性质:a ^ a = 0,以及 a ^ 0 = a。并且,异或运算是可结合和可交换的。
设子数组 nums[i...j] 的异或和为 xor(i, j)。
那么 xor(i, j) = nums[i] ^ nums[i+1] ^ ... ^ nums[j]。
步骤2:关键的前缀异或思想
为了快速计算任意子数组的异或和,我们引入 前缀异或数组 prefixXor。
- 定义
prefixXor[k]为nums[0] ^ nums[1] ^ ... ^ nums[k-1]。特别地,prefixXor[0] = 0。 - 那么对于子数组
nums[i...j],它的异或和可以表示为:
xor(i, j) = prefixXor[i] ^ prefixXor[j+1]
为什么?
因为prefixXor[j+1]是前j+1个数的异或,prefixXor[i]是前i个数的异或。根据异或性质a ^ a = 0,prefixXor[i]会被抵消掉,剩下的就是nums[i] ^ ... ^ nums[j]。
所以,判断子数组 [i, j] 是否为美丽子数组,就等价于判断:
prefixXor[i] ^ prefixXor[j+1] == 0
步骤3:转化问题
根据异或性质,prefixXor[i] ^ prefixXor[j+1] == 0 等价于:
prefixXor[i] == prefixXor[j+1]
也就是说,一个美丽子数组对应着一对相等的 prefixXor 值。
但请注意,i 和 j+1 是前缀异或数组的下标,i 是子数组的起始索引,j+1 是子数组结束索引的下一个位置。它们必须满足 i <= j,这等价于在 prefixXor 数组中,我们找的两个相等值,其对应的原数组下标中,第一个下标要小于第二个下标。
因此,原问题转化为:在 prefixXor 数组中,统计有多少对索引 (a, b),满足 a < b 且 prefixXor[a] == prefixXor[b]。
步骤4:暴力枚举与初步优化
最直接的方法是两层循环枚举所有子数组 [i, j],计算其异或和。时间复杂度 O(n²),对于 n=1000 是可行的(1e6次操作)。
但我们可以用前缀异或优化内层循环:
- 预处理
prefixXor数组,长度n+1。 - 用两层循环枚举
i和j(i <= j)。 - 检查
prefixXor[i] == prefixXor[j+1]。
这就是 O(n²) 的解法。
步骤5:引入区间DP的思路
虽然上述转化后的统计问题可以用哈希表在 O(n) 内解决(记录每个 prefixXor 值出现的次数,然后组合计数),但题目要求我们练习区间DP。
区间DP的核心定义:dp[i][j] 表示子数组 nums[i...j] 是否是美丽子数组(1表示是,0表示不是)。
但这样定义,我们需要统计的是所有 dp[i][j] == 1 的个数。
状态转移:
一个子数组 nums[i...j] 的异或和怎么算?我们可以利用小区间的结果。
设 xor[i][j] 为子数组 nums[i...j] 的异或值。
我们可以这样计算:xor[i][j] = xor[i][j-1] ^ nums[j]。
也就是说,我们可以通过延长一个较短的子数组来得到当前子数组的异或和。
区间DP的递推关系:
- 初始化:对于长度为1的子数组 (
i == j),xor[i][i] = nums[i]。如果nums[i] == 0,那么它是美丽子数组 (dp[i][i]=1),否则dp[i][i]=0。 - 状态转移:对于子数组
[i, j](j > i):- 首先计算
xor[i][j] = xor[i][j-1] ^ nums[j]。(这里xor[i][j-1]在上一步迭代中已经计算好) - 然后判断:如果
xor[i][j] == 0,那么dp[i][j] = 1,否则dp[i][j] = 0。
- 首先计算
最后,答案就是所有 dp[i][j] 的和。
步骤6:算法实现与复杂度分析
- 我们使用一个二维数组
xorDP来存储xor[i][j]。 - 同时,我们可以直接用一个变量
count来累加美丽子数组的个数,而不需要显式的dp[i][j]数组来存储0/1,因为只要xor[i][j] == 0,我们就计数。 - 算法流程:
- 初始化
count = 0。 - 外层循环枚举子数组起点
i(从 0 到 n-1):- 令
currentXor = 0。 - 内层循环枚举子数组终点
j(从i到 n-1):currentXor ^= nums[j]。 // 这就是xor[i][j]- 如果
currentXor == 0,则count++。
- 令
- 返回
count。
- 初始化
时间复杂度:O(n²),因为嵌套循环枚举了所有 n(n+1)/2 个子数组。
空间复杂度:O(1),只用了常数个额外变量。
步骤7:举例验证
以 nums = [4, 3, 1, 2, 4] 为例:
- i=0:
- j=0: currentXor=4 !=0
- j=1: currentXor=4^3=7 !=0
- j=2: currentXor=7^1=6 !=0
- j=3: currentXor=6^2=4 !=0
- j=4: currentXor=4^4=0 -> count=1 (对应子数组[4,3,1,2,4])
- i=1:
- j=1: currentXor=3 !=0
- j=2: currentXor=3^1=2 !=0
- j=3: currentXor=2^2=0 -> count=2 (对应子数组[3,1,2])
- j=4: currentXor=0^4=4 !=0
- i=2:
- j=2: currentXor=1 !=0
- j=3: currentXor=1^2=3 !=0
- j=4: currentXor=3^4=7 !=0
- i=3:
- j=3: currentXor=2 !=0
- j=4: currentXor=2^4=6 !=0
- i=4:
- j=4: currentXor=4 !=0
最终 count = 2,与示例一致。
- j=4: currentXor=4 !=0
步骤8:总结
这道题的核心在于:
- 问题转化:利用前缀异或,将“子数组异或和为0”转化为“前缀异或数组中两个相等的值”。
- 区间DP视角:将原数组的每个子数组视为一个状态,
xor[i][j]可以通过xor[i][j-1]递推得到。这本质上是一种动态规划,其“状态”是区间[i, j],“决策”是将区间向右扩展一位。 - 优化实现:通过从每个起点
i开始,逐步向右扩展并计算异或和,我们可以在 O(n²) 时间内优雅地枚举并检查所有子数组,这正是区间DP中常见的“枚举起点,然后扩展终点”的循环模式。
这道题巧妙地将位运算的特性与区间DP的框架结合在一起,是理解区间DP如何应用于非传统“合并”、“分割”类问题的优秀例题。