【DS】浅谈树状数组倍增

无意中看到的一个小 trick,便记录下来。

引入

给您一个数组,您需要实现以下操作和询问:

\(\bullet\) 插入一个数字 \(x\)

\(\bullet\) 查询排名为 \(k\) 的数 \(x\)

显然我们有权值线段树或者平衡树的做法。
但是我偏不(傲娇),我们来考虑树状数组怎么做。

树状数组倍增

定义:

\(n\):数组大小

\(a_i\):离散化后第 \(i\) 个数出现的次数

\(c_i\):树状数组

思路

插入一个数字就是单点修改啦,考虑询问。

写过平衡树模板的我们肯定知道,一个数 \(x\) (离散化后)的排名就是比 \(x\) 小的数的个数 \(+1\),即 \(\operatorname{rank}(x)=(\sum\limits^{x-1}_{i=1}a_i)+1\) 。那么反过来,要查询排名为 \(k\) 的数 \(x\), 它的位置就是 \(\max\limits_{\operatorname{rank}(x)\leq k}x\),我们把它展开来看,即

\[(\sum^{x-1}_{i=1}a_i)+1\leq k \]

\[(\sum^{x-1}_{i=1}a_i)\leq k-1 \]

左半部分可以直接树状数组求,我们枚举一个 \(r\),那么我们枚举到的符合条件的最大的 \(r\) 就是 \((x-1)\),答案即为 \(r+1\)

为什么不能直接 \(\leq k\)?因为有的数可能不存在,例如 \(a=\{1,0, 1, 0, 0\}\) ,我们查询排名为 \(2\) 的数,按正确的方法答案就是 \(3\),如果直接 \(\leq k\) 则输出为 \(5\)。不过有的时候我们会需要这样做,在后文会讲到。

如何求 \(r\)?显然不能 \(O(n\log{n})\),那有没有什么方法能降低枚举复杂度至 \(O(\log{n})\) 呢?

废话,看标题不就知道了

具体实现

引理

\[c_i=\sum\limits^{i}_{j=i-lowbit(i)+1}a_i \]

由树状数组的定义知显然。

倍增

我们能不能通过枚举跳 \(2^{\log n}, 2^{\log n-1}, ... , 2^1, 2^0\) 的距离去找呢?

代码如下:

int kth(int k){
    int r=0,tot=0,x,y;
    for(int i=log(值域);~i;--i){
        x=r+(1<<i);if(x>值域)continue;
        y=tot+c[x];
        if(y<k)r=x,tot=y;
    }
    return r+1;
}

这里每次 \(tot\leftarrow tot+c_{r+2^i}\) 是什么意思呢?根据上面得引理可知,加上 \(c_{r+2^i}\) 相当于加上 $ \sum\limits^{r+2^i}_{j=r}a_i $,即不断地向后拓展,到最后 \(tot\) 的值即为 \(\sum\limits^{r}_{i=1}a_i\)

为什么这样就能满足 \(r\) 最大?因为我们是从大到小枚举的。

优点 & 缺点

\(\bullet\) 优点:常数小,码量小,容易记。

\(\bullet\) 缺点:需要离线。

用途

一般用于优化部分需要求 \(k\) 小的操作,不作为主要算法。

例题

\(\circ\) CF786C Till I Collapse

题意:将 \(n\) 个数划分成 \(m\) 段使得每中不同数字的个数 \(\le k\),对于每个 \(k\) 满足 \(1\le k \le n\) ,求出最小的 \(m\)

考虑对于一个 \(k\) 怎么求。我们贪心地尽可能分最大的段,使得段内不同数字个数不大于 \(k\),询问区间不同数字的个数,就是数颜色嘛,我们想到 HH 的项链 的做法,但是有点不同:对于 HH 的项链里,我们知道右端点的位置,于是对于每一个数字的贡献都放到尽可能右边;但是这道题里面我们并不知道右端点(而是要求它),所以对于每一个枚举到的左端点的数字,(如果它对答案有贡献)我们计算贡献之后将它的贡献放到下一个出现的位置上就行了。每一次分段询问最远能到达的右端点即可,我们有这样的代码:

void add(int x,int v){while(x<=n)c[x]+=v,x+=lb;}
int kth(int k){
    int r=0,tot=0,x,y;
    ++k;//注意这里 k 要加一
    for(int i=18;~i;--i){
        x=r+(1<<i);if(x>n)continue;
        y=tot+c[x];
        if(y<k)r=x,tot=y;
    }
    return r;//注意这里 r 不用加一
}

for(int l=1,r=0;l<=n;++l){
    if(l==r+1)++ans,r=kth(l);
    add(l,-1),add(nxt[l],1);
}

为什么 \(k\) 要加一而 \(r\) 不用加一?因为我们计算数字个数的时候,中间可能有一串 \(0\),例如:\(a=\{1,1, 0, 1, 0, 0, 1, 0\}\)\(k=3\)。如果是之前的代码的话,返回的答案是 \(4\),而实际上能扩展到的最远的右端点是 \(6\)。我们把 \(k\) 加一并且 \(r\) 不加一就可避免此问题。

对于 \(1\)\(n\) 的所有 \(k\) 怎么求?我们可以一起求啊!毕竟每个数的贡献仅在数本身,我们对于所有询问的左端点维护个优先队列,然后按照上面的方法计算答案即可。

提交记录

其他练习:

\(\circ\) CF1181D Irrigation

\(\circ\) Luogu P6619 [省选联考 2020 A/B 卷] 冰火战士

posted @ 2022-08-18 20:24  RuntimeErr  阅读(618)  评论(1编辑  收藏  举报