简单树状数组

树状数组

概述

树状数组是一种基于倍增和二进制划分思想,用于维护简单区间操作的数据结构,短小精悍

我们知道,每一个数都可以使用二进制表示为a0a1a2a3ak(i[0,k],ai=0/1)(由低位到高位共k+1位)的形式,其中第i位所表示的二进制的值为2i×ai,根据二进制的性质,第i位若为1,那么比其余的低位的权值全部加起来都至少大1,这样的话我们可以使用二进制的形式来表示区间

我们假设以这个数n二进制每一个为1的位都表示区间,设这个数二进制下为1的位每一位为bi,i[1,m],那么我们可以将一个长度为n的区间表示为:
[1,2b1]
[2b1+1,2b2]
[2b1+2b2+1,2b3]
……
[1+q=1i12bq,2i]

这样可以不重不漏的把整个长度为n的区间分为m个长度分别为2bi的小区间,树状数组就是基于这样的区间划分

具体的,区间[1,x]的最后一个区间即为[xlowbit(x)+1,x],我们使用这样就可以倒序遍历出[1,x]分成的区间

#define lowbit(x) (x&-x)
while(x){  
    printf("[%d,%d]\n",x-lowbit(x)+1,x);
    x-=lowbit(x);
}

而这样的子区间我们就视为树状数组里x的子节点,下面附上一个长度为16的树状数组的样子(c即为树状数组)

该结构满足的性质有:

  1. 树的深度为O(logn)
  2. 每个节点x的子节点个数为log2(lowbit(x))+1
  3. 除根节点外每个节点x的父亲为x+lowbit(x)

基本操作

树状数组满足两个基本操作,分别是前缀和的查询和单点修改,此时每个c[x]=i=xlowbit(x)+1xa[i],a[i]
前缀和查询:
前缀和查询其实很简单,假设我们需要查询[1,x]的和,只需要将[1,x]分成的若干个小区间的和加上去就完事了,肥肠简单

int ask(int x){//1~x
    int ans=0;
    while(x){  
        ans+=c[x];
        x-=lowbit(x);
    }
    return ans;
}

单点修改呢,其实也是比较简单的,因为若我们要将a[x]+d,我们只需要把x在树状数组上所有包含它的c全部加上d即可,至于这个的办法,我们知道,第一个包含a[x]的区间(最小的)就是c[x],所以我们只需要把c[x]及其所有的祖先节点全部加上d即可

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);}

扩展应用

区间修改+单点查询

记得怎么把区间修改变成单点修改吗,差分撒
所以说我们的树状数组就变成了原序列a的一个差分数组,特别的,令c[1]=a[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));
        }
    }
}

区间修改+区间查询

区间修改还要区间查询比较复杂,在区间修改+单点查询的时候我们维护了差分数组,那么我们这里考虑也从上题开始扩展
若我们设d数组表示每一次区间修改的差分操作,最初全是0,那么对于a[x],它经过历史修改后就应该是a[x]+i=1xd[x],那么对于区间[1,x],整体增加的值为:

k=1xi=1kd[i]

变式得:

i=1x(xi+1)·d[i]=(x+1)i=1xd[i]i=1xi·d[i]

所以等同于我们要维护两边的式子,看样子都是一个差分序列或者变式,那么就等同于需要两个树状数组进行维护,前一个式子就使用树状数组c1像单点查询那样进行维护,后一个式子比较麻烦,我们需要维护一个当前位置×当前权值的一个数组,采用树状数组c2,对于一次差分操作,执行add2(l,l* d),add2(r+1,-r*d-d),所以说总的来说,修改操作是这样的:设对区间[l,r]+d

add1(l,d);
add1(r+1,-d);
add2(l,l*d);
add2(r+1,-(r+1)*d);

查询:
我们设原序列的前缀和数组为s,若查询区间和[l,r]:

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;

在具体实现中,为了代码方便,我们直接使用一个二维数组c[N][2]表示两个树状数组

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);
		}
	}
}

对于第二个树状数组正确性的证明,估计很多人都看不明白,(鄙人也是随便证证,错误见谅)
其实大家都走进一个误区,那就是i=1xi·d[i]它其实上不要忘记了d是一个差分数组,我们只是需要将新的维护这个式子的差分数组c2的每一次修改都×上节点位置即可,注意是d[i]而不是d啊大哥大姐们,我们又不需要它相互抵消之类的,从本质上来讲c2并不是一个差分数组,它只是差分数组的一个变形,即在原差分数组上每一个数都乘上它所在的位置即可

权值树状数组

顾名思义,就是建立一个值域上的树状数组,对于这种事情一般会离散化再做,即我们的权值树状数组从根本来说支持的操作就是
1.查询小于x的数的个数
2.插入m个值为x的数
原理是在普通树状数组上进行变形,即普通树状数组维护序列,权值树状数组维护的是值域,其本质也可以看作序列,只是序列的大小与值域相同
对于操作1,即查询前缀和[1,x]
对于操作2,即add(x,m);

逆序对求法

回想起逆序对的定义,若i<ja[i]>a[j]i,j构成逆序对,那么其实对于这个,归并排序比较复杂,这时候权值树状数组是一个不错的选择(常数还要比归并小)

具体的,根据逆序对的定义,我们只需要从左往右扫描a数组,然后i[1,n],统计当前树状数组中大于a[i]的数的个数(用i1ask(a[i])),累加上答案,然后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);

肥肠之简单

树状数组与倍增

我们知道,在树状数组划分区间本质上是基于二进制划分和倍增思想,导致树状数组的结构本身很适合倍增,即我们倍增2p几乎相当于直接加上了一个区间,因为每个区间的大小都是2的整次幂
例题:你需要维护一个01序列,每一次需要查找第k个1的位置,并且需要支持修改

修改就不说了,板子问题

做法1:树状数组+二分
我们可以二分前缀和查找第k个1的位置,复杂度是O(nlog2n)

做法2:树状数组+倍增

用树状数组c维01序列a的前缀和,在每次查询时:

  1. 初始化两个变量ans=sum=0;
  2. logn到0倒序考虑每一个整数p
  3. 对于每一个p,若ans+2pnsum+c[ans+2p]<k,则sum=sum+c[ans+2p],ans=ans+2p
  4. 最后ans+1即为所求

2的整次幂数为步长,能累加则累加,树状数组恰好已经存有关于2的幂的一些信息,直接结合树状数组使用,即可快速求解,得到一个复杂度为O(nlogn)的优秀算法

posted @   spdarkle  阅读(20)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示