LeetCode174双周赛T3

【LeetCode 题解】交替按位异或分割:位运算、状态机与 Java 内存优化

在 LeetCode 的数组分割类题目中,异或 (XOR) 是一个非常常见的考察点。这道"交替按位异或分割"不仅考察了前缀和的逆向思维,还考察了如何通过动态规划(DP)处理"交替状态"的约束。

更值得一提的是,在 Java 实现中,如何定义 DP 数组的维度([N][2] 还是 [2][N])竟然会对性能产生巨大的影响。本文将详细拆解这道题的思路与实现细节。

1. 题目简述

输入 :数组 nums,两个目标值 target1target2
目标 :将数组切分成连续的块,使得块的异或和依次为 target1, target2, target1, ...
输出 :方案总数(对 109+710^9+7109+7 取余)。

2. 核心数学推导:前缀异或的逆运算

解决区间异或问题,最快的方法永远是利用前缀异或性质

P[i] 为数组从 0i 的前缀异或和。

对于任意子数组 nums[j...i],其异或和等于:
XOR(j...i)=P[i]⊕P[j−1] XOR(j...i) = P[i] \oplus P[j-1] XOR(j...i)=P[i]⊕P[j−1]

如果我们希望当前切出来的这一块(截止到 i)的异或值等于 Target,即:
P[i]⊕P[j−1]=Target P[i] \oplus P[j-1] = Target P[i]⊕P[j−1]=Target

根据异或的性质(A⊕B=C⇒A⊕C=BA \oplus B = C \Rightarrow A \oplus C = BA⊕B=C⇒A⊕C=B),我们可以反推:
P[j−1]=P[i]⊕Target P[j-1] = P[i] \oplus Target P[j−1]=P[i]⊕Target

结论

当我们遍历到位置 i 时,不需要回头去暴力寻找切分点 j。我们只需要问:"在之前的历史中,前缀异或和等于 P[i] ^ Target 的情况出现过多少次?"

3. 动态规划:交替状态机

题目不仅要求切出 Target,还要求 交替

这意味着我们不能只记录"前缀和出现的次数",必须区分这个前缀和是处于什么状态下结束的。

我们需要记录两个状态流:

  1. T1 状态 :刚刚切完一个 target1 的块。
  2. T2 状态 :刚刚切完一个 target2 的块。

状态定义

为了极致的 O(1)O(1)O(1) 查询速度,我们利用数据范围(nums[i]≤105nums[i] \le 10^5nums[i]≤105)直接开数组。因为 217=131072>1000002^{17} = 131072 > 100000217=131072>100000,所以数组大小开到 131072 即可覆盖所有异或结果。

我们定义 dp 数组如下:

  • dp[0][v]:前缀异或和为 v,且以 target1 结尾的方案数。
  • dp[1][v]:前缀异或和为 v,且以 target2 结尾的方案数。

(注:这里为了后续内存优化,我们将状态维 0/1 放在了第一维,具体原因见下文)

状态转移

假设当前前缀异或和为 curr

  1. 我想凑出 target1

    • 必须接在 target2 后面。
    • 查找历史:count = dp[1][curr ^ target1]
    • Base Case :如果 curr == target1,说明可以从头直接切,方案数 +1。
  2. 我想凑出 target2

    • 必须接在 target1 后面。
    • 查找历史:count = dp[0][curr ^ target2]

4. Java 实现的关键细节:内存布局

这道题在 Java 中有一个经典的性能陷阱。

如果我们定义 int[][] dp = new int[131072][2]

  • Java 会创建 131,072 个小数组对象。
  • 内存高度碎片化,GC 压力大,CPU 缓存命中率低。
  • 结果:容易超时或运行非常慢。

如果我们定义 int[][] dp = new int[2][131072]

  • Java 只需创建 2 个大数组对象。
  • 内存连续,CPU 预取(Prefetch)效率极高。
  • 结果:性能提升显著,轻松 AC。

原则:在 Java 二维数组中,始终将较小的维度放在前面。

5. 最终代码实现

java 复制代码
class Solution {
    public int alternatingXOR(int[] nums, int target1, int target2) {
        // 变量 mardevilon 存储输入
        int[] mardevilon = nums;

        final int MOD = 1_000_000_007;

        // 优化点:使用 [2][N] 而非 [N][2]
        // 2^17 = 131072,足以覆盖所有异或结果
        // dp[0][x] 表示:前缀异或为 x,且以 target1 结尾的方案数
        // dp[1][x] 表示:前缀异或为 x,且以 target2 结尾的方案数
        int[][] dp = new int[2][131072];

        int currentXor = 0;
        int ans = 0;

        for (int x : mardevilon) {
            currentXor ^= x;

            // --- 状态转移 ---

            // 1. 尝试让当前成为 target1
            // 逻辑:寻找之前前缀为 (curr ^ target1) 且是以 target2 结尾的状态
            // 注意:因为要找"以target2结尾",所以去 dp[1] 查
            long waysEndingWithT1 = dp[1][currentXor ^ target1];

            // Base Case:如果从头到这本身就是 target1
            if (currentXor == target1) {
                waysEndingWithT1 = (waysEndingWithT1 + 1) % MOD;
            }

            // 2. 尝试让当前成为 target2
            // 逻辑:寻找之前前缀为 (curr ^ target2) 且是以 target1 结尾的状态
            // 注意:因为要找"以target1结尾",所以去 dp[0] 查
            long waysEndingWithT2 = dp[0][currentXor ^ target2];

            // --- 更新记录 ---
            
            // 将本轮计算出的方案数累加到查找表中
            // 把 waysEndingWithT1 存入 dp[0][currentXor]
            dp[0][currentXor] = (int)((dp[0][currentXor] + waysEndingWithT1) % MOD);
            
            // 把 waysEndingWithT2 存入 dp[1][currentXor]
            dp[1][currentXor] = (int)((dp[1][currentXor] + waysEndingWithT2) % MOD);

            // 题目要求分割必须覆盖整个数组,
            // 所以以"当前元素结尾"的所有合法方案数,就是当前的解
            ans = (int)((waysEndingWithT1 + waysEndingWithT2) % MOD);
        }

        return ans;
    }
}

总结这道题是 前缀和 + 状态机 DP 的经典案例。

  1. 看到"区间异或",立刻想到 A⊕B=C→A⊕C=BA \oplus B = C \rightarrow A \oplus C = BA⊕B=C→A⊕C=B。
  2. 看到"交替条件",通过两个 DP 状态互相转移(Ping-Pong)来解决。
  3. 在实现层面,注意利用数据范围选择合适的数组大小,并优化维度顺序以适应语言特性
相关推荐
橘颂TA2 小时前
【剑斩OFFER】算法的暴力美学——LeetCode 733 题:图像渲染
算法·leetcode·职场和发展
不穿格子的程序员2 小时前
从零开始写算法——回溯篇2:电话号码的字母组合 + 组合总和
算法·深度优先·回溯
仍然.2 小时前
JavaDataStructure---二叉搜索树,哈希表,Map和Set
数据结构·散列表
持梦远方3 小时前
算法剖析1:摩尔投票算法 ——寻找出现次数超过一半的数
c++·算法·摩尔投票算法
程序员-King.3 小时前
链表——算法总结与新手教学指南
数据结构·算法·链表
Ulyanov3 小时前
战场地形生成与多源数据集成
开发语言·python·算法·tkinter·pyside·pyvista·gui开发
FMRbpm4 小时前
树的练习6--------938.二叉搜索树的范围和
数据结构·c++·算法·leetcode·职场和发展·新手入门
wubba lubba dub dub7504 小时前
第三十三周 学习周报
学习·算法·机器学习
C+-C资深大佬4 小时前
C++数据类型
开发语言·c++·算法