【DS】浅谈树状数组倍增
无意中看到的一个小 trick,便记录下来。
引入
给您一个数组,您需要实现以下操作和询问:
插入一个数字 。
查询排名为 的数 。
显然我们有权值线段树或者平衡树的做法。
但是我偏不(傲娇),我们来考虑树状数组怎么做。
树状数组倍增
定义:
:数组大小
:离散化后第 个数出现的次数
:树状数组
思路
插入一个数字就是单点修改啦,考虑询问。
写过平衡树模板的我们肯定知道,一个数 (离散化后)的排名就是比 小的数的个数 ,即 。那么反过来,要查询排名为 的数 , 它的位置就是 ,我们把它展开来看,即
左半部分可以直接树状数组求,我们枚举一个 ,那么我们枚举到的符合条件的最大的 就是 ,答案即为 。
为什么不能直接 ?因为有的数可能不存在,例如 ,我们查询排名为 的数,按正确的方法答案就是 ,如果直接 则输出为 。不过有的时候我们会需要这样做,在后文会讲到。
如何求 ?显然不能 ,那有没有什么方法能降低枚举复杂度至 呢?
废话,看标题不就知道了
具体实现
引理
由树状数组的定义知显然。
倍增
我们能不能通过枚举跳 的距离去找呢?
代码如下:
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;
}
这里每次 是什么意思呢?根据上面得引理可知,加上 相当于加上 ,即不断地向后拓展,到最后 的值即为 。
为什么这样就能满足 最大?因为我们是从大到小枚举的。
优点 & 缺点
优点:常数小,码量小,容易记。
缺点:需要离线。
用途
一般用于优化部分需要求 小的操作,不作为主要算法。
例题
题意:将 个数划分成 段使得每中不同数字的个数 ,对于每个 满足 ,求出最小的 。
考虑对于一个 怎么求。我们贪心地尽可能分最大的段,使得段内不同数字个数不大于 ,询问区间不同数字的个数,就是数颜色嘛,我们想到 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);
}
为什么 要加一而 不用加一?因为我们计算数字个数的时候,中间可能有一串 ,例如:,。如果是之前的代码的话,返回的答案是 ,而实际上能扩展到的最远的右端点是 。我们把 加一并且 不加一就可避免此问题。
对于 到 的所有 怎么求?我们可以一起求啊!毕竟每个数的贡献仅在数本身,我们对于所有询问的左端点维护个优先队列,然后按照上面的方法计算答案即可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探