简单树状数组
树状数组
概述
树状数组是一种基于倍增和二进制划分思想,用于维护简单区间操作的数据结构,短小精悍
我们知道,每一个数都可以使用二进制表示为
我们假设以这个数
……
这样可以不重不漏的把整个长度为
具体的,区间
#define lowbit(x) (x&-x)
while(x){
printf("[%d,%d]\n",x-lowbit(x)+1,x);
x-=lowbit(x);
}
而这样的子区间我们就视为树状数组里
该结构满足的性质有:
- 树的深度为
- 每个节点
的子节点个数为 - 除根节点外每个节点
的父亲为
基本操作
树状数组满足两个基本操作,分别是前缀和的查询和单点修改,此时每个
前缀和查询:
前缀和查询其实很简单,假设我们需要查询
int ask(int x){//1~x
int ans=0;
while(x){
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
单点修改呢,其实也是比较简单的,因为若我们要将
void add(int x,int d){
for(int i=x;i<=n;i+=lowbit(i))c[i]+=d;
}
至于区间和的查询void sum(int l,int r){return ask(r)-ask(l-1);}
扩展应用
区间修改+单点查询
记得怎么把区间修改变成单点修改吗,差分撒
所以说我们的树状数组就变成了原序列
那么对于区间修改就差分正常操作了,对于单点查询的话就是查前缀和了
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]),a[i]=a[i]-a[i-1];
c[1]=a[1];
while(m--){
int opt,l,r,d;
scanf("%d%d",&opt,&l);
if(opt==1){//区间修改
scanf("%d%d",&r,&d);
add(l,d);
add(r+1,-d);
}
else {
printf("%d\n",ask(l));
}
}
}
区间修改+区间查询
区间修改还要区间查询比较复杂,在区间修改+单点查询的时候我们维护了差分数组,那么我们这里考虑也从上题开始扩展
若我们设
变式得:
所以等同于我们要维护两边的式子,看样子都是一个差分序列或者变式,那么就等同于需要两个树状数组进行维护,前一个式子就使用树状数组add2(l,l* d),add2(r+1,-r*d-d)
,所以说总的来说,修改操作是这样的:设对区间
add1(l,d);
add1(r+1,-d);
add2(l,l*d);
add2(r+1,-(r+1)*d);
查询:
我们设原序列的前缀和数组为
l--;
int ansl=s[l-1]+l*ask1(l-1)+ask2(l-1);
int ansr=s[r]+(r+1)*ask1(r)+ask2(r);
ans=ansr-ansl;
在具体实现中,为了代码方便,我们直接使用一个二维数组
void add(int k,int x,int a){
while(x<=n){
c[x][k]+=a;
x+=lowbit(x);
}
}
int ask(int k,int x){
int ans=0;
while(x){
ans+=c[x][k];
x-=lowbit(x);
}
return ans;
}
signed main(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=n;i++){
scanf("%lld",&a[i]);
sum[i]=sum[i-1]+a[i];
}
char q;
int l,r,d;
while(m--){
cin>>q;
if(q=='C'){
scanf("%lld%lld%lld",&l,&r,&d);
add(0,l,d);
add(0,r+1,-d);
add(1,l,d*l);
add(1,r+1,-d*(r+1));
}
else {
scanf("%lld%lld",&l,&r);
long long ans=sum[r]+(r+1)*ask(0,r)-ask(1,r);
ans-=sum[l-1]+l*ask(0,l-1)-ask(1,l-1);
printf("%lld\n",ans);
}
}
}
对于第二个树状数组正确性的证明,估计很多人都看不明白,(鄙人也是随便证证,错误见谅)
其实大家都走进一个误区,那就是
权值树状数组
顾名思义,就是建立一个值域上的树状数组,对于这种事情一般会离散化再做,即我们的权值树状数组从根本来说支持的操作就是
1.查询小于
2.插入
原理是在普通树状数组上进行变形,即普通树状数组维护序列,权值树状数组维护的是值域,其本质也可以看作序列,只是序列的大小与值域相同
对于操作1,即查询前缀和
对于操作2,即add(x,m);
逆序对求法
回想起逆序对的定义,若
具体的,根据逆序对的定义,我们只需要从左往右扫描add(a[i],1);
int ans=0;
for(int i=1;i<=n;i++)ans+=(i-1-ask(a[i])),add(a[i],1);
printf("%d",ans);
肥肠之简单
树状数组与倍增
我们知道,在树状数组划分区间本质上是基于二进制划分和倍增思想,导致树状数组的结构本身很适合倍增,即我们倍增
例题:你需要维护一个01序列,每一次需要查找第
修改就不说了,板子问题
做法1:树状数组+二分
我们可以二分前缀和查找第
做法2:树状数组+倍增
用树状数组
- 初始化两个变量
; - 从
到0倒序考虑每一个整数 - 对于每一个
,若 ,则 - 最后
即为所求
以
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!