在算法题目的世界里,限制条件(Constraints)往往被视为障碍,但实际上,它们是解题过程中的宝贵提示。这些限制不仅定义了问题的边界,还暗示了高效算法的方向,帮助我们避开低效路径。例如,一个“有序数组”的限制往往提示二分搜索的可能性;“无环图”限制建议树遍历如 DFS;“值域有限”限制指向计数排序或桶排序,而不是通用排序。又如,在“两数之和”中,n=10^4 提示哈希 O(n)而非 O(n^2);在“最大子数组和”中,连续+n=10^5 提示 Kadane DP。本文以 LeetCode 3755「最大平衡异或子数组的长度」为例,剖析如何将限制转化为解题利器。
题目描述 题目编号 :LeetCode 3755题目名称 :Find Maximum Balanced XOR Subarray Length(最大平衡异或子数组的长度)
给定一个整数数组 nums,返回满足以下条件的最长子数组 的长度:
该子数组的异或和 (bitwise XOR of all elements)为 0。
该子数组中偶数 和奇数 的个数相等 。
如果不存在这样的子数组,返回 0。
示例 :
输入:nums = [1,2,3,4] 输出:4 解释:整个数组 [1,2,3,4] 的异或和为 0,且奇数(1,3)和偶数(2,4)各有两个。
输入:nums = [1,2,3] 输出:0 解释:无满足条件的子数组。
约束条件 :
1 ≤ nums.length ≤ 10^5
0 ≤ nums[i] ≤ 10^9
这些约束表明,我们需要高效的解决方案,可能为 O(n) 或 O(n log n),并建议使用前缀相关的技巧来处理子数组。
解题思路 暴力枚举所有子数组会是 O(n^2),在 n=10^5 下超时。这正是限制作为提示 的精髓所在——让我们逐一拆解。
限制 1:子数组的连续性(隐含的前缀技巧提示) 题目指定“子数组”(subarray),而非“子序列”,这隐含连续性,第一眼应想到滑动窗口或前缀技巧,避免离散组合的复杂。
限制 2:最长长度(双遍历优化的转向) “最长”结合平衡条件(偶奇相等),提示不是简单窗口,而是状态跟踪,如用哈希记录最早匹配,实现最大距离。
限制 3:n ≤ 10^5(O(n) 或 O(n log n) 的铁律) 数组规模提示需线性或准线性时间,排除暴力,转向哈希或排序优化。若涉及排序,可转向 O(n log n),详见 刷题思路 3:复杂度分析 深入探讨。 这些限制并非壁垒,而是灯塔:连续性启发前缀,平衡+最长导向状态哈希,规模锁定效率。
详细思路剖析:一步步思考过程 为了更清晰地展示如何从限制中逐步推导出解决方案,让我们模拟一个典型的解题思考流程。这个过程从问题初读开始,逐步精炼思路,并结合本题的具体条件。
步骤 1:理解核心需求,识别暴力解的陷阱 初读题目,我们看到需要“最长子数组”,满足“异或和=0”和“奇偶个数相等”。直觉上,可以用两层循环枚举所有子数组 [l, r],计算子数组的异或和(O(n) 时间内用前缀优化为 O(1))和奇偶计数(同样用前缀)。但总时间仍是 O(n^2),而限制 3(n≤10^5)明确提示:这会超时,必须优化。
步骤 2:从连续性限制启发前缀技巧 限制 1 强调“子数组”连续,这提示使用前缀数组。定义 prefix_xor[i+1] = nums[0..i]的异或,则子数组[l..r]异或=prefix_xor[r+1] ^ prefix_xor[l]。要=0,即 prefix_xor[r+1] == prefix_xor[l]。 类似地,为奇偶平衡,定义 diff[i+1] = diff[i] + (1 if nums[i]奇 else -1),则平衡 iff diff[r+1] == diff[l](奇-偶=0⇒ 奇=偶)。子数组长度必偶数。 前缀让条件检查 O(1)。
步骤 3:结合最长限制,转向状态跟踪 限制 2“最长”提示不是找任意满足,而是最大距离的 l,r where prefix_xor[r+1]==prefix_xor[l] AND diff[r+1]==diff[l],且 r-l 最大。暴力仍 O(n^2)。提示:用哈希存( diff, xor ) -> 最早索引(最长=当前 i - 最早 j)。 为什么双遍历?第一遍只记录首次(最早),第二遍查询,确保使用历史最早值。
代码实现 多语言实现遵循此逻辑:前缀计算、状态映射记录最早索引,然后查询遍历求最大长度。添加注释以便理解。
Python 3 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from typing import Listclass Solution : def maxBalancedSubarray (self, nums: List[int]) -> int: x_or_pre = [0 ] diff_pre = [0 ] seen = {(0 , 0 ): -1 } for i, num in enumerate(nums): new_diff = diff_pre[-1 ] + (1 if num & 1 else -1 ) diff_pre.append(new_diff) new_xor = x_or_pre[-1 ] ^ num x_or_pre.append(new_xor) pair = (new_diff, new_xor) if pair not in seen: seen[pair] = i ans = 0 n = len(nums) for i in range(n): cnt = diff_pre[i + 1 ] curr_xor = x_or_pre[i + 1 ] pair = (cnt, curr_xor) if pair in seen: j = seen[pair] ans = max(ans, i - j) return ans
Java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import java.util.HashMap;import java.util.Map;class Solution { public int maxBalancedSubarray (int [] nums) { int n = nums.length; int [] xOrPre = new int [n + 1 ]; int [] diffPre = new int [n + 1 ]; Map<String, Integer> seen = new HashMap<>(); seen.put("0,0" , -1 ); for (int i = 0 ; i < n; i++) { int num = nums[i]; diffPre[i + 1 ] = diffPre[i] + ((num & 1 ) == 1 ? 1 : -1 ); xOrPre[i + 1 ] = xOrPre[i] ^ num; String pair = diffPre[i + 1 ] + "," + xOrPre[i + 1 ]; if (!seen.containsKey(pair)) { seen.put(pair, i); } } int ans = 0 ; for (int i = 0 ; i < n; i++) { String pair = diffPre[i + 1 ] + "," + xOrPre[i + 1 ]; if (seen.containsKey(pair)) { int j = seen.get(pair); ans = Math.max(ans, i - j); } } return ans; } }
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 #include <unordered_map> #include <string> #include <vector> class Solution {public : int maxBalancedSubarray (std ::vector <int >& nums) { int n = nums.size(); std ::vector <int > xOrPre (n + 1 , 0 ) ; std ::vector <int > diffPre (n + 1 , 0 ) ; std ::unordered_map <std ::string , int > seen; seen["0,0" ] = -1 ; for (int i = 0 ; i < n; ++i) { int num = nums[i]; diffPre[i + 1 ] = diffPre[i] + ((num & 1 ) ? 1 : -1 ); xOrPre[i + 1 ] = xOrPre[i] ^ num; std ::string pair = std ::to_string(diffPre[i + 1 ]) + "," + std ::to_string(xOrPre[i + 1 ]); if (seen.find(pair) == seen.end()) { seen[pair] = i; } } int ans = 0 ; for (int i = 0 ; i < n; ++i) { std ::string pair = std ::to_string(diffPre[i + 1 ]) + "," + std ::to_string(xOrPre[i + 1 ]); if (seen.find(pair) != seen.end()) { int j = seen[pair]; ans = std ::max(ans, i - j); } } return ans; } };
JavaScript 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 var maxBalancedSubarray = function (nums ) { const n = nums.length; const xOrPre = new Array (n + 1 ).fill(0 ); const diffPre = new Array (n + 1 ).fill(0 ); const seen = new Map (); seen.set("0,0" , -1 ); for (let i = 0 ; i < n; i++) { const num = nums[i]; diffPre[i + 1 ] = diffPre[i] + (num & 1 ? 1 : -1 ); xOrPre[i + 1 ] = xOrPre[i] ^ num; const pair = `${diffPre[i + 1 ]} ,${xOrPre[i + 1 ]} ` ; if (!seen.has(pair)) { seen.set(pair, i); } } let ans = 0 ; for (let i = 0 ; i < n; i++) { const pair = `${diffPre[i + 1 ]} ,${xOrPre[i + 1 ]} ` ; if (seen.has(pair)) { const j = seen.get(pair); ans = Math .max(ans, i - j); } } return ans; };
Go 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 import ( "fmt" "strconv" ) func maxBalancedSubarray (nums []int ) int { n := len (nums) xOrPre := make ([]int , n+1 ) diffPre := make ([]int , n+1 ) seen := make (map [string ]int ) seen["0,0" ] = -1 for i := 0 ; i < n; i++ { num := nums[i] if num&1 == 1 { diffPre[i+1 ] = diffPre[i] + 1 } else { diffPre[i+1 ] = diffPre[i] - 1 } xOrPre[i+1 ] = xOrPre[i] ^ num pair := strconv.Itoa(diffPre[i+1 ]) + "," + strconv.Itoa(xOrPre[i+1 ]) if _, exists := seen[pair]; !exists { seen[pair] = i } } ans := 0 for i := 0 ; i < n; i++ { pair := strconv.Itoa(diffPre[i+1 ]) + "," + strconv.Itoa(xOrPre[i+1 ]) if j, exists := seen[pair]; exists { if i-j > ans { ans = i - j } } } return ans }
复杂度分析
时间复杂度 :O(n),两次遍历 + 哈希操作 O(1) 均摊。
空间复杂度 :O(n),前缀数组 O(n) + 哈希表最坏 O(n) 独特状态。
在 n=10^5 下,高效通过。注:n≤10^5 也允许 O(n log n),如需排序场景,可参考上述链接深入探讨。
总结 算法解题的关键在于解读限制,将其转化为思路指引。在 LeetCode 3755 中,连续性、最长追求和规模限制,共同指向前缀 + 双遍历哈希的方案。掌握此道,解题将事半功倍。