lucifer

在算法题目的世界里,限制条件(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 List

class Solution:
def maxBalancedSubarray(self, nums: List[int]) -> int:
# 初始化前缀数组和 seen 映射,包含初始状态
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) # 更新差分:奇数 +1,偶数 -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) # 长度 = i - j(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];
// seen 映射用于(差分, 异或) -> 最早索引
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); // +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);
// seen 映射用于状态
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);
// seen 映射
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 中,连续性、最长追求和规模限制,共同指向前缀 + 双遍历哈希的方案。掌握此道,解题将事半功倍。


 评论


博客内容遵循 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议

本站使用 Material X 作为主题 。
载入天数...载入时分秒...