Problem: 3721. 最长平衡子数组 II
文章目录
- [1. 整体思路](#1. 整体思路)
- [2. 完整代码](#2. 完整代码)
- [3. 时空复杂度](#3. 时空复杂度)
-
-
- [时间复杂度: O ( N N ) O(N \sqrt{N}) O(NN )](#时间复杂度: O ( N N ) O(N \sqrt{N}) O(NN ))
- [空间复杂度: O ( N ) O(N) O(N)](#空间复杂度: O ( N ) O(N) O(N))
-
1. 整体思路
核心问题
找到最长的子数组,满足 "不同奇数个数" 等于 "不同偶数个数"。
算法逻辑:分块 (Square Root Decomposition)
相比于线段树的树形结构,分块将数组分为 N \sqrt{N} N 个块,每个块维护自己的局部信息。
-
数据结构设计:
sum数组:存储每个位置的基础值(不包含懒标记)。Block类 :维护每个块的信息。l, r:块的左右边界。todo:懒标记(Lazy Tag),表示整个块都需要加上的数值。posMap :这是分块的核心优化。它存储该块内 每个数值第一次出现的索引 。- Key: 数值 v v v。
- Value: 在该块内,值等于 v v v 的最小索引 j j j(即
sum[j] == v)。 - 这允许我们在 O ( 1 ) O(1) O(1) 时间内判断一个块内是否存在某个目标值,并找到其位置。
-
操作逻辑:
- 区间更新 (
rangeAdd) :- 对于中间的完整块 :直接修改
todo标记。 O ( 1 ) O(1) O(1)。 - 对于两端的残缺块 :暴力更新
sum数组,应用之前的todo,然后重构 该块的posMap。这是最耗时的部分,耗时 O ( N ) O(\sqrt{N}) O(N )。
- 对于中间的完整块 :直接修改
- 查询 (
findFirst) :- 我们需要找到最小的索引 j j j(且 j < i − a n s j < i - ans j<i−ans),使得该位置的值等于目标值。
- 遍历每个块:
- 完整块 :检查
posMap 中是否有target - todo。如果有,直接返回。 O ( 1 ) O(1) O(1)。 - 残缺块 (通常是最后一个块的部分):暴力遍历查找。 O ( N ) O(\sqrt{N}) O(N )。
- 完整块 :检查
- 区间更新 (
-
业务逻辑:
- 与之前的线段树解法类似,维护子数组的"去重奇偶差"。
- 如果
x是奇数,贡献 + 1 +1 +1;偶数贡献 − 1 -1 −1。 - 当
x新出现时,在[i, n]加上 v v v。 - 当
x重复出现时,在[last, i-1]减去 v v v(消除旧位置对当前窗口的贡献)。 - 查询目标:找到最早的 j j j,使得
State[j] == State[i](逻辑上等价于寻找平衡子数组)。
2. 完整代码
java
import java.util.*;
class Solution {
// 分块的核心类
private static class Block {
int l, r; // 块的左右边界 [l, r)
int todo; // 懒标记:整个块统一增加的值
Map<Integer, Integer> pos; // 记录块内每个值第一次出现的下标
Block(int l, int r, Map<Integer, Integer> pos) {
this.l = l;
this.r = r;
this.pos = pos;
this.todo = 0;
}
}
private int[] sum; // 存储实际数值 (不含 todo)
private Block[] blocks; // 存储块信息
private int n;
public int longestBalanced(int[] nums) {
n = nums.length;
// 计算块大小,通常为 sqrt(N)。这里除以 2 可能是为了增加块的数量减小块内重建开销
int bSize = (int) (Math.sqrt(n + 1) / 2) + 1;
sum = new int[n + 1];
blocks = new Block[(n / bSize) + 1];
// 1. 初始化分块
for (int i = 0, idx = 0; i <= n; i += bSize) {
int r = Math.min(i + bSize, n + 1);
// 初始时 sum 全为 0,构建初始的 pos 映射
blocks[idx++] = new Block(i, r, calcPos(i, r));
}
Map<Integer, Integer> last = new HashMap<>();
int ans = 0;
// 2. 遍历数组
for (int i = 1; i <= n; i++) {
int x = nums[i - 1];
int v = (x % 2 != 0) ? 1 : -1;
// 更新贡献:维护区间去重计数
if (!last.containsKey(x)) {
// 第一次出现,影响从 i 开始到最后的所有区间起点
rangeAdd(i, n + 1, v);
} else {
// 重复出现,消除上一次出现位置对 [last, i-1] 范围的影响
rangeAdd(last.get(x), i, -v);
}
last.put(x, i);
// 获取当前 i 位置的实际值 (基础值 + 所在块的懒标记)
int s = sum[i] + blocks[i / bSize].todo;
// 查询:在 [0, i - ans) 范围内查找第一个值等于 s 的位置
// 如果找到了 idx,说明 nums[idx+1...i] 是平衡的
int firstIdx = findFirst(i - ans, s);
// 更新最大长度
// 注意:findFirst 返回的是索引,如果没找到返回 n
// 如果找到,长度为 i - firstIdx
ans = Math.max(ans, i - firstIdx);
}
return ans;
}
// 辅助:计算/重构块内的 pos Map
// key: 数值, value: 该数值在块内第一次出现的下标
private Map<Integer, Integer> calcPos(int l, int r) {
Map<Integer, Integer> pos = new HashMap<>();
// 从右向左遍历,这样 map.put 会覆盖后面的,保留最前面的索引
for (int j = r - 1; j >= l; j--) {
pos.put(sum[j], j);
}
return pos;
}
// 区间更新:[l, r) 加上 v
private void rangeAdd(int l, int r, int v) {
for (Block b : blocks) {
if (b == null || b.r <= l) continue; // 块在区间左侧,跳过
if (b.l >= r) break; // 块在区间右侧,结束
// 完整覆盖的块:直接更新 todo
if (l <= b.l && b.r <= r) {
b.todo += v;
} else {
// 部分覆盖的块:暴力重构
// 1. 先把旧的 todo 下放 (push down)
for (int j = b.l; j < b.r; j++) {
sum[j] += b.todo;
// 2. 对重叠部分应用新的 v
if (j >= l && j < r) {
sum[j] += v;
}
}
// 3. 重置 todo
b.todo = 0;
// 4. 重新计算该块的 pos Map (耗时 O(BlockSize))
b.pos = calcPos(b.l, b.r);
}
}
}
// 查询:找到小于 r 的第一个位置,其值等于 v
private int findFirst(int r, int v) {
for (Block b : blocks) {
if (b == null) break;
// 如果整个块都在查询范围内 [0, r)
if (b.r <= r) {
// 检查该块内是否有值等于 v - todo
// 利用 Map O(1) 查找
Integer j = b.pos.get(v - b.todo);
if (j != null) return j;
} else {
// 块部分在范围内(最后一个块),暴力查找
for (int j = b.l; j < r; j++) {
if (sum[j] == v - b.todo) {
return j;
}
}
break; // 超过 r,后续块无需再看
}
}
return n; // 未找到
}
}
3. 时空复杂度
假设数组长度为 N N N,块大小 B ≈ N B \approx \sqrt{N} B≈N 。
时间复杂度: O ( N N ) O(N \sqrt{N}) O(NN )
- 更新 (
rangeAdd) :- 最多有两个"部分重叠"的块,需要 O ( B ) O(B) O(B) 重构 Map。
- 中间有 O ( N / B ) O(N/B) O(N/B) 个完整块,仅需 O ( 1 ) O(1) O(1) 更新标记。
- 单次更新复杂度: O ( B + N / B ) ≈ O ( N ) O(B + N/B) \approx O(\sqrt{N}) O(B+N/B)≈O(N )。
- 查询 (
findFirst) :- 遍历 O ( N / B ) O(N/B) O(N/B) 个完整块,每个块查 Map 是 O ( 1 ) O(1) O(1)。
- 最后一个部分块遍历 O ( B ) O(B) O(B)。
- 单次查询复杂度: O ( N ) O(\sqrt{N}) O(N )。
- 总复杂度 :总共循环 N N N 次,所以是 O ( N N ) O(N \sqrt{N}) O(NN )。
空间复杂度: O ( N ) O(N) O(N)
sum数组 : O ( N ) O(N) O(N)。blocks数组 : O ( N ) O(\sqrt{N}) O(N ) 个对象。posMaps :所有块的 Map 加起来,最坏情况下存储所有位置的信息,总量为 O ( N ) O(N) O(N)。- 结论 : O ( N ) O(N) O(N)。