稀疏表、树状数组和线段树

区间问题,例如区间求和、区间最值,有很多数据结构可以选择。稀疏表、树状数组和线段树就是解决区间问题比较套路的方法,只要理解了这些数据结构的特征并且掌握了代码模版,遇到可建模成区间求和和区间最值的问题,就可以轻松解决了。

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 区间更新,区间查询

 

4. 总结

 
posted @ 2020-03-13 21:57  wory  阅读(565)  评论(0)    收藏  举报