ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

【DS】浅谈树状数组倍增

2022-08-18 20:30:36  阅读:179  来源: 互联网

标签:浅谈 树状 int sum tot 我们 枚举 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+2i}_{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 卷] 冰火战士

标签:浅谈,树状,int,sum,tot,我们,枚举,DS
来源: https://www.cnblogs.com/RuntimeErr/p/16599951.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有