关于力扣3721. 最长平衡子数组 II线段树解法的反思
学习的灵神的代码
这题有几点观察:
1.由于计算奇偶的平衡,那么用两数之和思想,寻找前面第一个前缀和等值的位置,维护区间长度最大值即可。
2.从左到右扫的时候,对于确定该区间是否存在某数,只需要左边的边界大于等于该数 在扫描区间的最后一次出现的位置。
根据这个特性,我们维护在这个扫描区间中这个数最后一次出现的位置。
(1)假如这个数第一次出现,也就是维护的最后一次出现的位置在更新前不存在时,那么对于idx
的区段,都应该计算这个数的贡献。这时候因为当到达之前,这个贡献都合法,而我们在扫描到当前位置的当下还不知道下一个出现位置在哪里或者会不会出现下一个位置,所以我们大胆地将
的区间都加上这个贡献,并在到达
时对贡献进行校正即可。
(2)假如这个数不是第一次出现,因为我们在他第上一次出现的时候,已经把他的位置到这个数组最后的位置加上了该数的贡献,那么我们需要根据我们维护的上一个最后一次出现的位置减去贡献,具体操作是减去区间的贡献,然后加上
区间的贡献。
根据这两个操作,我们就能正确地计算前缀区间的贡献和。
(3)贪心思想和离散介值定理:我们要得到最长的奇偶等量的数组长度,当前位置已经确定了,我们只需要知道等值的最前面的点。所以我们用线段树二分的思想进行寻找。
然后线段树为了优化时间复杂度,需要在顶部就能知道在某一区间是否具有目标信息。这时候灵神使用的是离散介值定理,因为可以通过观察得到,相邻前缀区间的差值小于等于一,那么在一个区间中的最大值和最小值包含了目标值,那么就可以断定这个目标值必然在其中,反之则可以知道没有。这个观察相当巧妙。然后通过这个观察我能学到的是,0 1的连续变化,可以根据离散介值定理实现线段树的搜索。
min = lambda a,b: b if b < a else a
max = lambda a,b:b if b > a else a
class Node:__slots__ = 'min','max','todo'def __init__(self):self.min = self.max = self.todo = 0
class LazySegmentTree:def __init__(self,n):self._n = n self._tree = [Node() for _ in range(2<<(n -1).bit_length())]# def _apply(self,node,todo):# cur = self._tree[node]# cur.min += todo # cur.max += todo # cur.todo += todo # 把当前节点的懒标记下传给左右儿子# def _spread(self, node: int) -> None:# todo = self._tree[node].todo# if todo == 0: # 没有需要下传的信息# return# self._apply(node * 2, todo)# self._apply(node * 2 + 1, todo)# self._tree[node].todo = 0 # 下传完毕# 把懒标记作用到 node 子树def _apply(self, node: int, todo: int) -> None:cur = self._tree[node]cur.min += todocur.max += todocur.todo += todo# 把当前节点的懒标记下传给左右儿子def _spread(self, node: int) -> None:todo = self._tree[node].todoif todo == 0: # 没有需要下传的信息returnself._apply(node * 2, todo)self._apply(node * 2 + 1, todo)self._tree[node].todo = 0 # 下传完毕def _maintain(self,node):l_node = self._tree[node*2]r_node = self._tree[node*2 +1]self._tree[node].min = min(l_node.min ,r_node.min)self._tree[node].max = max(l_node.max,r_node.max)def _update(self, node: int, l: int, r: int, ql: int, qr: int, f: int) -> None:if ql <= l and r <= qr: # 当前子树完全在 [ql, qr] 内self._apply(node, f)returnself._spread(node)m = (l + r) // 2if ql <= m: # 更新左子树self._update(node * 2, l, m, ql, qr, f)if qr > m: # 更新右子树self._update(node * 2 + 1, m + 1, r, ql, qr, f)self._maintain(node)def _find_first(self, node: int, l: int, r: int, ql: int, qr: int, target: int) -> int:if l > qr or r < ql or not self._tree[node].min <= target <= self._tree[node].max:return -1if l == r:return lself._spread(node)m = (l + r) // 2idx = self._find_first(node * 2, l, m, ql, qr, target)if idx < 0:# 去右子树找idx = self._find_first(node * 2 + 1, m + 1, r, ql, qr, target)return idxdef update(self, ql: int, qr: int, f: int) -> None:self._update(1, 0, self._n - 1, ql, qr, f)# 查询 [ql, qr] 内第一个等于 target 的元素下标# 找不到返回 -1# 0 <= ql <= qr <= n-1# 时间复杂度 O(log n)def find_first(self, ql: int, qr: int, target: int) -> int:return self._find_first(1, 0, self._n - 1, ql, qr, target)class Solution:def longestBalanced(self, nums: List[int]) -> int:n = len(nums)t = LazySegmentTree(n + 1)last = {} # nums 的元素上一次出现的位置ans = cur_sum = 0for i, x in enumerate(nums, 1):v = 1 if x % 2 else -1j = last.get(x, 0)if j == 0: # 首次遇到 xcur_sum += vt.update(i, n, v) # sum[i:] 增加 velse: # 再次遇到 xt.update(j, i - 1, -v) # 撤销之前对 sum[j:i] 的增加last[x] = i# 把 i-1 优化成 i-1-ans,因为在下标 > i-1-ans 中搜索是没有意义的,不会把答案变大j = t.find_first(0, i - 1 - ans, cur_sum)if j >= 0:ans = i - j # 如果找到了,那么答案肯定会变大return ans