十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

LeetCode 1787 题解:使所有区间的异或结果为零 —— 异或分组 + 值域动态规划双解法详解

LeetCode 1787 题解:使所有区间的异或结果为零 —— 异或分组 + 值域动态规划双解法详解 LeetCode 1787 题解使所有区间的异或结果为零 —— 异或分组 值域动态规划双解法详解【免费下载链接】leetcodeLeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解记录自己的leetcode解题之路。)项目地址: https://gitcode.com/gh_mirrors/le/leetcode导读本文以 problems/1787.make-the-xor-of-all-segments-equal-to-zero.md 为核心系统拆解 LeetCode 1787「使所有区间的异或结果为零」这道位运算与动态规划结合的中等偏难题。文章会依次推导出两个关键结论解的上限与周期分组结构给出常规三维 DP、滚动数组空间优化以及可 AC 的 O(k × upper) 优化 DP三版可运行代码与复杂度分析并结合本仓库的位运算与动态规划专题文档补充底层原理。读完你将掌握对值域做 DP 而非对索引做 DP这一重要套路以及如何利用异或自反性将 O(k × upper²) 的三层枚举优化为 O(k × upper)。题目回顾与题意分析给你一个整数数组nums和一个整数k。区间[left, right]left right的异或结果是对下标位于left和right包括两端之间所有元素进行 XOR 运算的结果nums[left] XOR nums[left1] XOR ... XOR nums[right]。返回数组中要更改的最小元素数以使所有长度为k的区间异或结果等于零。示例示例 1输入nums [1,2,0,3,0], k 1输出3解释将数组[1,2,0,3,0]修改为[0,0,0,0,0]。当k 1时长度为 1 的区间即单个元素所有元素都必须为 0。示例 2输入nums [3,4,5,2,1,7,3,4,7], k 3输出3解释将数组[3,4,5,2,1,7,3,4,7]修改为[3,4,7,3,4,7,3,4,7]。注意修改后nums[i] nums[i3]成立周期为 k。示例 3输入nums [1,2,4,1,2,5,1,2,6], k 3输出3解释将数组[1,2,4,1,2,5,1,2,6]修改为[1,2,3,1,2,3,1,2,3]。数据范围1 k nums.length 20000 nums[i] 2^10即值域上限upper 1024这是后面值域 DP 与总价值域规模的关键前置知识异或运算与动态规划基础异或运算的三个核心性质本题的整个推导都建立在异或运算性质之上本仓库的 位运算专题文档 对此有专门总结任何数和本身异或为 0a ^ a 0任何数和 0 异或是本身a ^ 0 a交换律与结合律a ^ b ^ c a ^ c ^ b由第 1 条可推出本题最关键的自反性a ^ b ^ b a ^ (b ^ b) a ^ 0 a即一个数异或两次同一个数等于它自己。原题解把这条性质称为异或的自反性它是后面推导nums[i] nums[ik]以及状态转移方程的理论基石。值域 DP 与滚动数组本题的状态设计对值域upper做 DP而不是对数组索引做 DP是该题最大的思维跳跃点。关于动态规划的状态定义原则本仓库的 动态规划专题文档 强调状态定义是动态规划的核心定义好状态才能画出递归树、聚焦最优子结构写转移方程。同时该文档还专门讲解了滚动数组优化技巧当转移方程中当前状态只依赖前一层状态时可以用两个数组滚动复用将空间从二维压缩到一维。本文第 5 节的优化正是这一技巧的实战应用。两个关键观察从区间异或到周期分组观察一解的上限是 n因为你可以把每个位置都改成 00 ^ 0 0所以无论如何答案不会超过数组长度n。这个平凡上界为后面 DP 数组的初值提供了依据——所有状态的初始代价都可以设为n。观察二修改后的数组满足 nums[i] nums[ik]设两个相邻的长度为 k 的窗口[i, ik-1]与[i1, ik]它们的异或结果都为 0。将两式异或(nums[i] ^ ... ^ nums[ik-1]) ^ (nums[i1] ^ ... ^ nums[ik]) 0 ^ 0 0利用异或的结合律中间项nums[i1] ... nums[ik-1]各出现两次恰好抵消为 0于是得到nums[i] ^ nums[ik] 0 nums[i] nums[ik]这正是异或的自反性的直接推论。由此修改后的数组以 k 为周期所有下标满足i % k r的位置必须被改成同一个值。分组定义与等价条件将i % k相同的下标划为一组组编号即i % k一共恰好k组。此时所有长度为 k 的区间异或为 0与下面两个条件完全等价可以逐条验证同一组内所有元素相等即周期结构nums[i] nums[ik]每组选取的代表值异或起来为 0因为第一个长度为 k 的窗口恰好由每组的一个代表组成且任意长度 k 窗口的异或值相同。组i的大小为size_i n // k int(n % k i)前n % k组比其余组多一个元素。验证示例 2n 9, k 3n % k 0每组大小都是 3组 0 为[3,2,3,3]组 1 为[4,1,4,4]组 2 为[5,7,7,7]代表值3 ^ 4 ^ 7 0。常规 DP三层枚举O(k × upper²)状态定义定义dp[i][j]为处理到第 i 组0 基且前 i1 组代表值的异或和为 j 时所需的最小修改次数。答案即dp[k-1][0]。注意这里 j 是异或和本身取值范围是[0, upper)upper 2^10 1024。这就是原题解强调的对值域upper做 dp而不是数组索引。转移方程推导由于我们可以把第 i 组整体改成任意值val0 val upper若希望修改后异或和为j那么修改前前 i 组的异或和必须是p val ^ j仍然是自反性p ^ val j。把第 i 组所有元素改成val的代价为size_i - counter[(i, val)]其中counter[(i, val)]是第 i 组中原本就等于val的元素个数——本来就等于val的不需要改。将val p ^ j代入dp[i][j] min(dp[i-1][p] size_i - counter[(i, p ^ j)])对 p 取遍 [0, upper)边界i 0时没有前一状态直接把第 0 组全部改成j即可即dp[0][j] size_0 - counter[(0, j)]。于是需要枚举 ik 组、jupper、pupper三层循环。Python3 代码来自原题解完整保留class Solution: def minChanges(self, nums: List[int], k: int) - int: counter collections.defaultdict(int) UPPER 2 ** 10 n len(nums) for i, num in enumerate(nums): counter[(i % k, num)] 1 dp [[n] * UPPER for _ in range(k)] for i in range(k): size_i n // k int(n % k i) for j in range(UPPER): for p in range(UPPER): if i 0: dp[i][j] size_i - counter[(i, j)] else: dp[i][j] min( dp[i][j], dp[i - 1][p] size_i - counter[(i, p ^ j)], ) return dp[-1][0]复杂度分析令 n 为数组长度。时间复杂度O(n * (k upper²))其中O(n)来自统计counterO(k * upper²)来自三层 DP 枚举统计项可并入。空间复杂度O(k * upper)。代入本题数据范围n k 2000upper 1024k * upper² ≈ 2000 × 1024² ≈ 2 × 10⁹。原题解明确指出这个量级远远大于 10⁷在力扣单测例 12 秒的时间限制下Python 大约只能稳定承受 10⁷ 量级的操作几乎必然超时因此必须优化。滚动数组空间压缩到 O(upper)观察转移方程dp[i][j]只依赖dp[i-1][...]这提示我们可以使用滚动数组只保留两层。原题解特别提醒由于 p 可能大于 jp 和 j 的枚举顺序无关但写入与读取发生在不同数组上因此至少需要两个 dp 数组而不是一个——即不能在同一数组上原地更新否则新写入的值会污染本轮后续状态。Python3 代码滚动数组版counter collections.defaultdict(int) UPPER 2 ** 10 n len(nums) for i, num in enumerate(nums): counter[(i % k, num)] 1 dp [n] * UPPER for i in range(k): size_i n // k int(n % k i) nxt_dp [n] * UPPER for j in range(UPPER): for p in range(UPPER): if i 0: nxt_dp[j] size_i - counter[(i, j)] else: nxt_dp[j] min( nxt_dp[j], dp[p] size_i - counter[(i, p ^ j)], ) dp nxt_dp return dp[0]复杂度分析令 n 为数组长度。时间复杂度O(n * (k upper²))与未优化版一致。空间复杂度O(upper)从二维降为一维。滚动数组只解决了空间问题时间瓶颈三层枚举依旧存在需要进一步优化。优化 DP从改新数与改已有数两个角度切入O(k × upper)思路为什么能砍掉 p 这一层原解法的瓶颈在于枚举p时做了upper次全值域扫描。优化的核心洞察是对第 i 组来说我们真正关心的只有两种选择情况一把这一组改成分组中不存在的数新数情况二把这一组改成分组中已经出现过的数已有数情况一改成分组中没有的数如果选择新数那么该组可以贡献任意异或和 j因为异或的自反性只要前一状态存在某个值q对应前 i 组异或和为 q我们就可以把第 i 组全部改成q ^ j从而总异或和变成 j。此时代价是dp[q] size_i其中0 q upper。由于我们求最小值自然取min(dp) size_i。这一步把原来枚举 p 的upper次操作降为一次求min(dp)。情况二改成分组中已有的数如果选择已有数那么该组的值val必须来自第 i 组的候选集合且改为 val 的代价就是size_i - count其中count为val在该组中的出现次数预处理自counter。枚举该组所有出现过的(val, count)若前 i 组异或和为j则新异或和为j ^ val转移为for val, count in counter[i].items(): # 改成这一列已有的数 nxt_dp[j ^ val] min(nxt_dp[j ^ val], dp[j] size_i - count)注意这里的枚举范围不再是全值域upper而是该组实际出现的不同数值。所有组的候选数总和不超过n因此整层枚举是线性的。两种情况下取较小者填入nxt_dp即完成了从dp到nxt_dp的一层滚动。关键点异或的自反性从改成新的数和改成已有的数两个角度考虑避免了全值域枚举 p。Python3 代码优化版完整保留class Solution: def minChanges(self, nums: List[int], k: int) - int: counter collections.defaultdict(lambda: collections.defaultdict(int)) UPPER 2 ** 10 n len(nums) for i, num in enumerate(nums): counter[i % k][num] 1 dp [n] * UPPER dp[0] 0 for i in range(k): size_i n // k int(n % k i) nxt_dp [min(dp) size_i] * UPPER # 改成新的数 for j in range(UPPER): for val, count in counter[i].items(): # 改成这一列已有的数 nxt_dp[j ^ val] min(nxt_dp[j ^ val], dp[j] size_i - count) dp nxt_dp return dp[0]补充说明两点实现细节初始化dp[0] 0、其余为n表示尚未处理任何组时异或和为 0 的代价是 0其余状态不可达用上界 n 兜底。nxt_dp [min(dp) size_i] * UPPER一步就把改新数的所有状态初始化完毕之后再逐个用改已有数的方案取 min。复杂度分析令 n 为数组长度。时间复杂度O(n * (k upper))O(n)统计 counterDP 部分对每组做一次min(dp)O(upper)并枚举该组候选值总和 O(n)整体远小于常规解法。空间复杂度O(upper)两个滚动数组各占upper长度。代入数据范围k 2000、upper 1024计算量约为k * upper 2 × 10⁶量级配合 O(n) 的统计完全在 10⁷ 安全线之内可以放心通过。两种解法复杂度对比解法时间复杂度空间复杂度是否可 AC按本题数据范围常规 DP三维枚举O(n * (k upper²))O(k * upper)约 2 × 10⁹ 次操作可能超时滚动数组版O(n * (k upper²))O(upper)同上仅省空间优化 DP改新数/改已有数O(n * (k upper))O(upper)约 2 × 10⁶ 次操作可 AC小结与延伸回顾整道题的解题链条由所有长度为 k 的区间异或为 0推出周期结构nums[i] nums[ik]将问题转化为k 个分组各自取一个代表值、代表值异或为 0的组合优化问题状态设计对值域做 DPdp[i][j]表示前 i 组异或和为 j 的最小代价配合counter预处理每组各数值的出现次数得到三维转移方程用滚动数组压缩空间用改新数 / 改已有数二分类思维把全值域枚举 p 优化掉得到 O(k × upper) 的可 AC 解法。本题是异或自反性 值域 DP套路的经典样本与仓库内其他题目可对照学习thinkings/bit.md异或性质总览及 136. 只出现一次的数字、137、260、645 等位运算套路题problems/1835.find-xor-sum-of-all-pairs-bitwise-and.md同样围绕异或结果逐位确定思路的姊妹题thinkings/dynamic-programming.md状态定义、滚动数组等动态规划方法论完整题解目录见 README.md 与 SUMMARY.md本题收录于 problems 章节。【免费下载链接】leetcodeLeetCode Solutions: A Record of My Problem Solving Journey.( leetcode题解记录自己的leetcode解题之路。)项目地址: https://gitcode.com/gh_mirrors/le/leetcode创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表