RMQ | ST 表 | 树状数组 学习笔记

前言

前段时间没啥空写博客,今天汇总一下这几天学的几种数据结构。

Part1. ST 表

ST 表是用于求解 RMQ(区间最值) 问题的一种数据结构,使用了倍增的思想,时间复杂度 O ( n log ⁡ n ) \mathcal{O}(n\log n) O(nlogn)

本人认为 ST 表很类似区间 dp

有一个数组 a a a,假设现在要求静态区间最大值。

I. 创建 ST 表

首先定义 ST 表 s t i , j st_{i,j} sti,j 表示 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j1] 这段区间的最大值。数学公式有点别扭,说白了就是 i i i 开始的 2 j 2^j 2j 个数中的最大值

由于 2 0 = 1 2^0=1 20=1,所以 s t i , 0 st_{i,0} sti,0 就是 a i a_i ai(差不多是 dp 的边界条件吧)。

然后,我们要由小区间推出大区间,看图:

绿色数字代表 j j j 值。可见, s t i , j st_{i,j} sti,j 是由两个小区间推出来的,很容易得到状态转移方程:

s t i , j = max ⁡ ( s t i , j − 1 , s t i + 2 j − 1 , j − 1 ) st_{i,j}=\max(st_{i,j-1},st_{i+2^{j-1},j-1}) sti,j=max(sti,j1,sti+2j1,j1)

现在就类似区间 dp 那样,先枚举区间长度 j j j,再枚举左端点 i i i

ST 表的创建代码如下:

for(int j=1;(1<<j)<=n;j++){
    for(int i=1;i+(1<<j)-1<=n;i++){
        st[i][j]=max(st[i][j-1],st[i+(1<<j-1)][j-1]);
    }
}

i i i j j j 枚举范围的解释:

  • j j j 作为区间长度,自然不能超过 n n n

  • i i i 作为左端点,我们知道 s t i , j st_{i,j} sti,j 表示的区间是 [ i , i + 2 j − 1 ] [i,i+2^j-1] [i,i+2j1],不能超过 n n n

II. 求解 RMQ

创建好 ST 表,就要充分地利用它。

对于查询的区间 [ l , r ] [l,r] [l,r],组成它的两段子区间长度的指数 k = log ⁡ 2 ( r − l + 1 ) k=\log_2 (r-l+1) k=log2(rl+1)

故答案为两段的最大值。

第一段就是 [ l , k ] [l,k] [l,k],但是第二段有点麻烦了。

由于 r − l + 1 r-l+1 rl+1 不一定满足 log ⁡ 2 ( r − l + 1 ) ∈ N \log_2(r-l+1)\in\mathbb{N} log2(rl+1)N,所以两段区间会有重合的部分,这不影响最终的结果,但是左端点就要由 r r r 推出来,即 r − 2 k + 1 r-2^k+1 r2k+1

第二段区间就是 [ r − 2 k + 1 , k ] [r-2^k+1,k] [r2k+1,k]

求解 RMQ 代码如下:

int k=log2(r-l+1);
writeln(max(st[l][k],st[r-(1<<k)+1][k]));

总结

ST 表运用的倍增思想,可以说跟分治有异曲同工之妙(虽然完全就不同)。

前者不断地将指数增加,求解 2 0 2^0 20 2 1 2^1 21 2 2 2^2 22 等子问题,再推出大问题。

后者呢是一个大问题,不断地二分下去(或者划成更多子问题)。

ST 表在 LCA 问题中也有广泛的运用。

Part2. 树状数组

最基本的树状数组可以维护单点修改、区间查询的问题,时间复杂度在 O ( log ⁡ n ) \mathcal{O}(\log n) O(logn),且常数较小。

树状数组运用了差分思想。

树状数组非常的优美,不像隔壁线段树,我 *@%~?*#…,码量和常数都很大(相对树状数组)。

树状数组核心就 3 个函数,不过呢需要理解一个非常重要的点:lowbit。

简单来说,一个数的 lowbit 就是这个数在二进制下最低位的 1 1 1 所对于的值,例如 ( 6 ) 10 = ( 110 ) 2 (6)_{10}=(110)_2 (6)10=(110)2,那么它的 lowbit 就是 2 2 2

lowbit 函数可以用如下代码实现:

inline int lowbit(int i){return i&-i;}

非常简洁。

树状数组直接用一个 c c c 表示即可,简单到我都不用专门建一个小节出来。

I. 单点修改

代码如下:

void add(int i,int k){for(;i<=n;i+=lowbit(i))c[i]+=k;}

II. 区间查询

树状数组可以简单维护前缀和。

代码如下:

int sum(int i){
    int s=0;
    for(;i;i-=lowbit(i))s+=c[i];
    return s;
}

至于区间查询,结合前缀和的思想即可写出来了。

拓展

树状数组有个很奇妙的用途就是求逆序对。

将树状数组当成一个加强的桶,每插入一个数 a i a_i ai,先给 c a i ← c a i + 1 c_{a_i}\leftarrow c_{a_i}+1 caicai+1,答案就加上 i − ∑ j = 1 a i a j i-\sum\limits_{j=1}^{a_i}a_j ij=1aiaj 即可。

那么,为什么?

c a i ← c a i + 1 c_{a_i}\leftarrow c_{a_i}+1 caicai+1:就是 a i a_i ai 的出现次数增加了 1 1 1,不要把这里的 c c c 当树状数组,当成一个普通的桶即可。

i − ∑ j = 1 a i a j i-\sum\limits_{j=1}^{a_i}a_j ij=1aiaj:到目前出现了 i i i 个数,减去小于它的数的出现次数。

如果是 P1908 的话还需要离散化处理,这里不细说了。

代码实现:

int ans=0;
for(int i=1;i<=n;i++){
    a[i]=read();
    add(a[i],1);
    ans+=i-sum(a[i]);
}

总结

之后的树套树,若是树状数组的话,处理起来会比线段树、平衡树简单很多。

但是呢它简便,维护的东西自然少,树状数组能维护的东西,线段树都能维护

至于求逆序对,理解还是有点困难的(至少对于我),理解之后会发现非常的巧妙。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值