“统计美丽子数组的数目(位运算 + 区间DP)”
字数 4066 2025-12-19 12:00:25

好的,我看了一下列表,发现**“统计美丽子数组的数目(位运算 + 区间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 = 0prefixXor[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

但请注意,ij+1 是前缀异或数组的下标,i 是子数组的起始索引,j+1 是子数组结束索引的下一个位置。它们必须满足 i <= j,这等价于在 prefixXor 数组中,我们找的两个相等值,其对应的原数组下标中,第一个下标要小于第二个下标。

因此,原问题转化为:在 prefixXor 数组中,统计有多少对索引 (a, b),满足 a < bprefixXor[a] == prefixXor[b]

步骤4:暴力枚举与初步优化
最直接的方法是两层循环枚举所有子数组 [i, j],计算其异或和。时间复杂度 O(n²),对于 n=1000 是可行的(1e6次操作)。
但我们可以用前缀异或优化内层循环:

  1. 预处理 prefixXor 数组,长度 n+1
  2. 用两层循环枚举 ij (i <= j)。
  3. 检查 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. 初始化:对于长度为1的子数组 (i == j),xor[i][i] = nums[i]。如果 nums[i] == 0,那么它是美丽子数组 (dp[i][i]=1),否则 dp[i][i]=0
  2. 状态转移:对于子数组 [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,我们就计数。
  • 算法流程
    1. 初始化 count = 0
    2. 外层循环枚举子数组起点 i (从 0 到 n-1):
      • currentXor = 0
      • 内层循环枚举子数组终点 j (从 i 到 n-1):
        • currentXor ^= nums[j]。 // 这就是 xor[i][j]
        • 如果 currentXor == 0,则 count++
    3. 返回 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,与示例一致。

步骤8:总结
这道题的核心在于:

  1. 问题转化:利用前缀异或,将“子数组异或和为0”转化为“前缀异或数组中两个相等的值”。
  2. 区间DP视角:将原数组的每个子数组视为一个状态,xor[i][j] 可以通过 xor[i][j-1] 递推得到。这本质上是一种动态规划,其“状态”是区间 [i, j],“决策”是将区间向右扩展一位。
  3. 优化实现:通过从每个起点 i 开始,逐步向右扩展并计算异或和,我们可以在 O(n²) 时间内优雅地枚举并检查所有子数组,这正是区间DP中常见的“枚举起点,然后扩展终点”的循环模式。

这道题巧妙地将位运算的特性与区间DP的框架结合在一起,是理解区间DP如何应用于非传统“合并”、“分割”类问题的优秀例题。

好的,我看了一下列表,发现** “统计美丽子数组的数目(位运算 + 区间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,与示例一致。 步骤8:总结 这道题的核心在于: 问题转化 :利用前缀异或,将“子数组异或和为0”转化为“前缀异或数组中两个相等的值”。 区间DP视角 :将原数组的每个子数组视为一个状态, xor[i][j] 可以通过 xor[i][j-1] 递推得到。这本质上是一种动态规划,其“状态”是区间 [i, j] ,“决策”是将区间向右扩展一位。 优化实现 :通过从每个起点 i 开始,逐步向右扩展并计算异或和,我们可以在 O(n²) 时间内优雅地枚举并检查所有子数组,这正是区间DP中常见的“枚举起点,然后扩展终点”的循环模式。 这道题巧妙地将位运算的特性与区间DP的框架结合在一起,是理解区间DP如何应用于非传统“合并”、“分割”类问题的优秀例题。