连续数组:把 0 变成 −1,问题立刻翻转
0 与 1 一样多,把 0 换成 −1 就是和为 0。同一前缀和再次出现,中间那段最长即可。
把 0 当成 −1、1 当成 +1。区间里两种数个数相等 ⇔ 区间和为 0 ⇔ 两端前缀和相等。哈希表只记录每个前缀和第一次出现的位置(空前缀 0 记在位置 0)。再次见到同一个前缀和,用当前长度减去首次位置,更新最长。时间 O(n)。
这是 LeetCode 525. Contiguous Array。给你只含 0 和 1 的数组 nums,找出 0 和 1 个数相同的最长连续子数组,返回它的长度。没有这样的子数组就返回 0。
主例 nums = [0, 1, 0]。[0,1] 和 [1,0] 都是长度 2,整段三个数里 0 比 1 多一个,不是答案。答案 2。更长的对照 [0,1,0,0,1,1]:整段 0、1 各三个,长度 6。
枚举左右端点再分别数 0 和 1,平方。缺的是把「两个计数相等」收成一个数。本文先做 0→−1,再在前缀和上找同一值的最远两次出现。
0 看成 −1,个数相等就是和为 0
设一段里有 a 个 1、b 个 0。要 a=b。把每个 0 换成 −1,这段的和变成 a×1 + b×(−1) = a−b。于是 a=b 当且仅当这段和为 0。不必再维护两本计数。
主例 [0,1,0] 变成 [−1, 1, −1]。前两个 −1+1=0,后两个 1+(−1)=0,演示两帧就是这两段。整段 −1+1−1=−1,不是 0,所以长度 3 不合法。
这步只改观察,不改扫描次数。和为 0 的最长子数组,仍然不能对每个右端点再往左扫。下一步用前缀和把「和为 0」变成「两个位置的值相同」。
一个和代替两个计数要两种东西一样多,给它们相反的权重,和为零就是相等。LC525 的 0/1、括号匹配里的 +1/−1,用的是同一句。
同一前缀和再次出现,中间就是合法段
到下标 j(不含)的前缀和记为 pre[j],空前缀 pre[0]=0。区间 [i, j) 的和是 pre[j]−pre[i]。和为 0 就是 pre[j]=pre[i]。题目变成:同一前缀和出现在 i 和 j,j−i 最大。
要最长,每个前缀和只需要最早的那个位置。更晚的同值再出现,减去最早位置才最长;中间那些同值只会得到更短的段。哈希表 first 在第一次见到 pre 时记下位置,之后只用来减,不再覆盖。
代码用「已经处理的元素个数」当位置:first[0]=0 表示一个数都还没读。读完下标 j 的数之后,当前位置是 j+1。若 pre 见过,长度 = (j+1) − first[pre];否则 first[pre]=j+1。
用手走主例。读 0:pre=−1,第一次见,first[−1]=1。读 1:pre=0,0 在位置 0 见过,长度 2−0=2,对应 [0,1]。读最后一个 0:pre=−1,−1 在位置 1 见过,长度 3−1=2,对应 [1,0]。best 停在 2。演示两帧就是这两次「前缀和撞车」。若把 first 改成记录最后一次,两次撞车得到的仍是 2,但更长数组会丢掉从更早位置拉出来的整段。
Go:0→−1 + 首次出现
func findMaxLength(nums []int) int {first := map[int]int{0: 0}pre, best := 0, 0for j, x := range nums {if x == 0 { pre-- } else { pre++ }if i, ok := first[pre]; ok {if j+1-i > best { best = j + 1 - i }} else {first[pre] = j + 1}}return best}
1first[0]=0 是空前缀。整段 0、1 一样多时,最后 pre 回到 0,长度就是 n。
20 减一、1 加一,不当场建新数组。主例三次之后 pre 分别是 −1、0、−1。
3见过就用 j+1−i 更新最长。主例两次都得到 2。
4第一次见才登记。覆盖成最后一次,更长的合法前缀会被缩短。
总结
0 当 −1,前缀和再次出现就量一段。主例 0 在位置 0 与 2 各出现,长度 2。
- 两个计数相等先收成一个和为零,再收成两个前缀和相等。
- 要最长,只记每个前缀和最早出现的位置,不要记最后一次。
- 空前缀 0 必须先放进表,否则从下标 0 开始的合法整段会漏掉。