稀疏表、树状数组和线段树
区间问题,例如区间求和、区间最值,有很多数据结构可以选择。稀疏表、树状数组和线段树就是解决区间问题比较套路的方法,只要理解了这些数据结构的特征并且掌握了代码模版,遇到可建模成区间求和和区间最值的问题,就可以轻松解决了。
1. 稀疏表(Sparse Table)
稀疏表经常用来处理离线多轮RMQ(区间最值)问题。之所以强调多轮,因为多轮的情况下稀疏表的预处理时间$(nlgn)$才能被摊还出去,试想一下只为一次的区间最值查询建立一个稀疏表,未免给人雷声大雨点小的感觉。
1.1 结构特征
先从最简单暴力的思路开始。因为是离线的问题,可以用一个矩阵来保存答案,$matrix[m][n]$为区间$[m, n]$的最值,这样用起来直接查询就行了。预处理的时间为$O(n^2)$,查询时间为$O(1)$,空间复杂度为$O(n^2)$。预处理比较慢且浪费空间,是不是有其他的手段降低一下这两块的开销?
区间最值问题是不是可以通过求解子区间的最值然后整合出结果?显然可以,这样我们就没必要直接给出区间的最值,给出子区间的就可以了,可以压缩状态。以区间极小值为例,我们定义$d(A)$表示区间$A$上的最小值,假设有n个区间$A_1,A_2,.....A_n$,满足对$\forall i:1\leq i\leq n, A_i\cup A=A$,并且$\bigcap_{1}^{n}A_i = A$,那么$d(A)$可以通过$min(d(A_i),1\leq i\leq n)$得到。可以看出:在最值问题中,子区间的并集要为当前区间,但是对子区间之间的交集并无要求。
最直观的想法当然是将区间二分,查询的时候分别求左右区间的最值,就可以得到了这个区间的最值,用$d(l, r)$表示区间$[l, r)$的最值,$min(l, r)=min\{d(l, m), d(m, r)\}, m=(l+r)/2$。发现这一通操作只是稍微改善了一下复杂度系数,数量级并没有改变。既然对子区间之间的交集没有要求,那状态是不是可以继续压缩,比如说$d(1,5)=min\{d(1,4), d(2,5)\};d(1,6)=min\{d(1,4),d(3,6)\}$,这样$d(1,4)$就可以被复用多次了。怎么设置才合理呢?既然$O(n^2)$的复杂度不行,那就往$O(nlgn)$考虑一下?一个节点平均保存$O(lgn)$个的区间,到这里差不多大家心里已经有答案了吧:只需要维护2的幂次长度的区间就可以了。
定义$st(i,j)$表示以$i$开始的,长度为$2^j$的区间的最值,$st(i,j)$可以通过一下公式得到:
$$st(i,j)=min\{d(i, j-1),d(i+2^{j-1}, j-1)\}$$
用上面的递推关系进行预处理,查询的时候用下面的式子:
$$d(l,r)=min\{st(l,k),st(r-2^k+1, k)\};k=\left \lfloor log(r-l+1) \right \rfloor$$
1.2 代码模版
1 class SparseTable(object): 2 def __init__(self, nums: List): 3 size, row, col = len(nums), len(nums), 0 4 while (1 << (col + 1)) <= size: 5 col += 1 6 self.st = [[0] * col for _ in range(row)] 7 for i in range(row): 8 self.st[i][0] = nums[0] 9 for j in range(1, col): 10 for i in range(row): 11 if i + (1 << j) > size: 12 break 13 self.st[i][j] = min(self.st[i][j - 1], self.st[i + (1 << (j - 1))][j - 1]) 14 15 def query(self, left: int, right: int): 16 k = 0 17 while (1 << (k + 1)) <= right - left + 1: 18 k += 1 19 return min(self.st[left][k], self.st[right - (1 << k) + 1][k])
1.3 小结
当问题满足和RMQ问题一样的区间特性的时候,就可以考虑用稀疏表求解:
- 子区间并集为当前区间,对交集没有要求(必要条件),例如区间最大公约数问题(RGQ)。
- 离线多轮查询用稀疏表才有意义
2. 树状数组(Binary Indexed Tree, BIT)
树状数组也是一个区间长度为2的幂次的一种数据结构,和稀疏表不同的是,位置为$i$的节点只维护一段长度为$lowbit(i)$,范围为$[i - lowbit(i) + 1, i]$的一段区间。翻译成树状数组好像并不是很贴切,可能只是把这个东西抽象成树更好理解一点。
2.1 结构特征
2.2 代码模版
树状数组的代码比较定式,理解记忆都比较简单,这里以求和为例,单独用一小节整理一下代码,下面的例子均可套用。
1 class BIT: 2 def __init__(self, arr: List[int]): 3 self.arr = arr 4 self.BITree = [0] * (len(arr) + 1) # 下标为0的位置没有意义 5 # 创建树状数组 6 for index, value in enumerate(arr): 7 self.update(index, value) 8 9 def update(self, index: int, delta: int): 10 """ 11 index对应的是arr的下标,delta是增量值 12 """ 13 self.arr[index] += delta 14 pos = index + 1 15 while pos < len(self.BITree): 16 self.BITree[pos] += delta 17 pos += self._lowbit(pos) 18 19 def sum(self, start: int, end: int) -> int: 20 """ 21 计算arr数组中[start, end)区间的和 22 """ 23 return self._sum(end) - self._sum(start) 24 25 def _sum(self, pos: int) -> int: 26 """ 27 计算arr数组中[0, pos)区间的和 28 """ 29 res = 0 30 while pos > 0: 31 res += self.BITree[pos] 32 pos -= self._lowbit(pos) 33 return res 34 35 def _lowbit(self, pos: int) -> int: 36 return pos & (-pos)
2.3 常见问题
2.3.1 单点更新,区间查询
2.3.2 区间更新,单点查询
2.3.3 区间更新,区间查询
2.4 树状数组不能用来查询区间最值?
3. 线段树(Segment Tree
3.1 结构特征
介绍Lazy思想:lazy-tag思想,记录每一个线段树节点的变化值,当这部分线段的一致性被破坏我们就将这个变化值传递给子区间,大大增加了线段树的效率。
PushUp(rt):通过当前节点rt把值递归向上更新到根节点
PushDown(rt):通过当前节点rt递归向下去更新rt子节点的值
3.2 代码模版
3.2.1 单点更新,区间查询
3.2.1 区间更新,区间查询

浙公网安备 33010602011771号