树状数组入门+进阶

一、简介

1 定义

树状数组是一种用于维护序列前缀和的数据结构,它支持单点修改和区间查询,时间复杂度均为 O(logn)。树状数组的核心思想是利用二进制的思想将序列分成若干个区间,从而实现快速查询和修改。

2 优点

假设我们有一个序列 a a a,我们需要支持两种操作:

1.单点修改:将 a i ​ a_i​ ai的值加上 x x x
2.区间查询:查询 a 1 a_1 a1 a i ​ a_i​ ai的和。

如果我们使用普通数组保存序列并实现上述两种操作,则时间复杂度分别为 O ( n ) O(n) O(n) O ( 1 ) O(1) O(1)如果使用前缀和,则时间复杂度分别为 O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)。假设我们有 n n n个操作,则最坏情况下的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
那么,有没有一种可能使得时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),也就是两个操作的时间复杂度都是 O ( l o g n ) O(logn) O(logn)呢?树状数组便是支持上述两种操作,且时间复杂度为 O ( l o g n ) O(logn) O(logn)的一种数据结构。

二、实现—单点修改+区间查询

假设输入时原数组为 a a a,树状数组为 c c c

0 预备知识

在位运算中,有一种操作,可以只保留 i i i的二进制表示中最右边的 “ 1 ” “1” “1”: l o w b i t ( x ) = x lowbit(x)=x lowbit(x)=x& ( − x ) (-x) (x)

int lowbit(int x){
	return x&(-x);
}

1 结构

请添加图片描述
如图,我们可以发现:
c [ 1 ] = a [ 1 ] c[1]=a[1] c[1]=a[1]
c [ 2 ] = a [ 1 ] + a [ 2 ] c[2]=a[1]+a[2] c[2]=a[1]+a[2]
c [ 3 ] = a [ 3 ] c[3]=a[3] c[3]=a[3]
c [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] c[4]=a[1]+a[2]+a[3]+a[4] c[4]=a[1]+a[2]+a[3]+a[4]
c [ 5 ] = a [ 5 ] c[5]=a[5] c[5]=a[5]
c [ 6 ] = a [ 5 ] + a [ 6 ] c[6]=a[5]+a[6] c[6]=a[5]+a[6]
c [ 7 ] = a [ 7 ] c[7]=a[7] c[7]=a[7]
c [ 8 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] + a [ 8 ] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8] c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]
如果将它们转变成二进制,可以发现,每一层的 l o w b i t lowbit lowbit都是相同的。
因此,节点 x x x的父节点为 x + l o w b i t ( x ) x+lowbit(x) x+lowbit(x)

2 操作

2.2 单点更新

更新操作是将输入数组中的一个元素增加一个值。在树状数组中,这需要更新多个元素。具体来说,对于输入数组中位置 i i i的更新,我们需要更新所有包含区间i的树状数组元素。这些元素可以通过在 i i i上加 l o w b i t ( i ) lowbit(i) lowbit(i)得到:
c [ i ] = a [ i − 2 k + 1 ] + a [ i − 2 k + 2 ] + … … a [ i ] c[i]=a[i-2^k+1]+a[i-2^k+2]+……a[i] c[i]=a[i2k+1]+a[i2k+2]+……a[i]

void updata(int x,int y){
	//x为更新位置,y为变化量 
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=y;
	}
}

2.3 区间查询

查询操作是计算输入数组中前 i i i个元素的和。在树状数组中,这需要累加多个元素。具体来说,我们从位置 i i i开始,每次去掉 l o w b i t ( i ) lowbit(i) lowbit(i),直到 i i i变为 0 0 0

int find_sum(ll x){
	int ans=0;
	for(int i=x;i;i-=lowbit(i)){
		ans+=c[i];
	}
	return ans;
}

注意,这个代码只能求区间 [ 1 , x ] [1,x] [1,x]的区间和,如果想求 [ l , r ] [l,r] [l,r]的区间和,利用前缀和相减就可以了—— [ l , r ] = [ 1 , r ] − [ 1 , l − 1 ] [l,r]=[1,r]-[1,l-1] [l,r]=[1,r][1,l1]

3 实践

1 BIT-1

题面

给定数组 1 , 2... a 1 , a 2 . . . a n 1,2...a_1,a_2...a_n 1,2...a1,a2...an,进行 q q q次操作,操作有两种:
1 i i i x x x:将 a i a_i ai加上 x x x
2 l l l r r r:求 a l + . . . + a r a_l +...+a_r al+...+ar

输入描述

第一行:输入两个数 n n n q q q,表示给定数组的长度和操作数
第二行:输入 n n n个数,表示长度为 n n n的给定数组
接下来 q q q行:每行输入3个数字,表示如题的某一种操作

输出描述

对于每个2操作,输出对应结果

思路

板子题,直接写

AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
ll n,q,c[1000002];
ll lowbit(ll x){
	return x&(-x);
}
void updata(ll x,ll y){
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=y;
	}
}
ll find_sum(ll x){
	ll ans=0;
	for(ll i=x;i;i-=lowbit(i)){
		ans+=c[i];
	}
	return ans;
}
int main(){
	scanf("%lld%lld",&n,&q);
	for(int i=1;i<=n;i++){
		ll x;
		scanf("%lld",&x);
		updata(i,x);
	}
	while(q--){
		ll k;
		scanf("%lld",&k);
		if(k==1){
			ll i,x;
			scanf("%lld%lld",&i,&x);
			updata(i,x);
		}
		else{
			ll l,r;
			scanf("%lld%lld",&l,&r);
			printf("%lld",find_sum(r)-find_sum(l-1));pr;
		}
	}
	return 0;
}

2 逆序对

题面

对于给定的一段正整数序列,逆序对就是序列中 i < j i<j i<j a i > a j a_i>a_j ai>aj的有序对,求一序列中有多少个逆序对。
第一行:输入一个数 n n n,表示序列有多少个数字
第二行:输入 n n n个数字 a i a_i ai
输出序列中逆序对的数目

思路

从后向前遍历
PS:数据太大要开离散化

AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
const ll N=1e6+5;
ll n,cnt[N],a[N],b[N];
ll sum;
map<ll,ll> mp;
ll lowbit(ll x){return x&(-x);}
void update(ll x,ll y){for(int i=x;i<=n;i+=lowbit(i)){cnt[i]+=y;}}
ll find(ll x){ll ans=0;for(int i=x;i;i-=lowbit(i)){ans+=cnt[i];}return ans;}
int main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		b[i]=a[i];
	}
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++){
		mp[a[i]]=i;
	}
	for(int i=1;i<=n;i++){
		ll x=mp[b[i]];
		update(x,1);
		sum+=i-find(x);
	}
	printf("%lld",sum);
	return 0;
}

三、实现—区间修改+单点查询

1 结构

树状数组虽然可以在 O ( l o g n ) O(logn) O(logn)的时间复杂度内完成单点更新或查询前缀和的操作。但是,如果我们想要进行区间修改和单点查询,我们需要使用到树状数组的一个变种——差分树状数组。

2 操作

差分树状数组的主要思想是将原序列转化为差分序列,然后在差分序列上建立树状数组。差分序列的第 i i i个元素等于原序列的第 i i i个元素和第 i − 1 i-1 i1个元素的差。

2.0 前置—差分中的区间修改

当修改 a l a_l al~ a r a_r ar时(如 + k +k +k),需要将差分数组中第 l l l个位置 + k +k +k,同时将第 r + 1 r+1 r+1个位置 − k -k k

2.1 建立/区间修改

在输入时,不能以原数组进行建树,而是使用其差分数组。具体见下:

long long x,last=0;
for(int i=1;i<=n;i++){
	scanf("%lld",&x);
	update(i,x-last);
	last=x;
}

当进行区间修改时,利用差分数组的特性即可。

long long l,r,k;
scanf("%lld%lld%lld",&l,&r,&k);
update(l,k);
update(r+1,-k);

3 实践

BIT-2

题面

给定数组 a 1 , a 2 . . . a n a_1 ,a_2 ...a_n a1,a2...an ,进行q次操作,操作有两种:
1 l r k:将 a i a_i ai~ a r a_r ar 每个数都加上 k;
2 k:输出 a k a_k ak

思路

和上面一样

AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
ll n,q,c[1000002];
ll lowbit(ll x){
	return x&(-x);
}
void update(ll x,ll y){//单点修 
	//x为更新位置,y为变化量 
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=y;
	}
}
ll query(ll x){//区间查询 
	ll ans=0;
	for(ll i=x;i;i-=lowbit(i)){
		ans+=c[i];
	}
	return ans;
}
int main(){
	scanf("%lld%lld",&n,&q);
	ll x,last=0;
	for(int i=1;i<=n;i++){
		scanf("%lld",&x);
		update(i,x-last);
		last=x;
	}
	while(q--){
		ll k;
		scanf("%lld",&k);
		if(k==1){
			ll l,r,k;
			scanf("%lld%lld%lld",&l,&r,&k);
			update(l,k);
			update(r+1,-k);
		}
		else{
			ll x;
			scanf("%lld",&x);
			printf("%lld",query(x));pr;
		}
	}
	return 0;
}

三、实现—区间修改+区间查询

1 操作

还是差分,我们只需要两个数组,分别存储运输组和差分数组。

2 实践

2.1 BIT-3

给定数组 a 1 , a 2 . . . a n a_1,a_2...a_n a1,a2...an ,进行q次操作,操作有两种:
1 r k:将 a i   a r a_i ~ a_r ai ar 每个数都加上k;
2 1 r:求 a 1 + . . . + a r a_1 + ... + a_r a1+...+ar

AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
ll read(){ll x=0,t=1;char ch;ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-'){t=-1;}ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*t;}
ll n,q;
ll a,b,v;
ll k,now,last;
ll sum1,sum2;
ll c1[1000005];
ll c2[1000005];
ll lowbit(ll x){return x&(-x);}
void update(ll *t,ll x,ll w){
	while(x<=n){
		t[x]+=w;
			x+=lowbit(x);
	}
	return ;
} 
ll query(ll *t,ll x){
	ll s=0;
	while(x>0){
		s+=t[x];
		x-=lowbit(x);
	}
	return s;
}
int main(){
	n=read();q=read();
	for(ll i=1;i<=n;i++){
		now = read();
		update(c1,i,now-last);
		update(c2,i,(i-1)*(now-last));
		last=now;
	}
	while(q--){
		k =read();
		if(k==1){
			a=read(),b=read(),v=read();	
			update(c1,a,v);update(c1,b+1,-v);
			update(c2,a,v*(a-1));
			update(c2,b+1,-v*b);
		}
		if(k==2){
			a = read(),b = read();
			sum1=(a-1)*query(c1,a-1)-query(c2,a-1);
			sum2=b*query(c1,b)-query(c2,b);
			printf("%lld\n",sum2-sum1);	
		}
	}
	return 0;	
}

2.2 前缀和的前缀和

前缀和(prefix sum) S i = ∑ k = 1 i a k S_i=\sum_{k=1}^i a_k Si=k=1iak
前前缀和(preprefix sum) 则把 S i S_i Si作为原序列再进行前缀和。记再次求得前缀和第i个是 S S i SS_i SSi
给一个长度n的序列 a 1 , a 2 , ⋯   , a n a_1, a_2, \cdots, a_n a1,a2,,an,有两种操作:

  1. Modify i x:把 a i a_i ai改成 x x x
  2. Query i:查询 S S i SS_i SSi
思路

将差分数组改为前缀和数组即可。

AC代码
#include<bits/stdc++.h>
#define ll long long
#define bug printf("---OK---")
#define pa printf("A: ")
#define pr printf("\n")
#define pi acos(-1.0)
using namespace std;
ll read(){ll x=0,t=1;char ch;ch=getchar();while(ch<'0'||ch>'9'){if(ch=='-'){t=-1;}ch=getchar();}while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return x*t;}
ll n,m,last,t1[1000008],t2[1000008],a[1000008];
ll lowbit(ll x){return x&(-x);}
void update(ll pos,ll x){
    for(ll i=pos;i<=n;i+=lowbit(i)){
    	t1[i]+=x,t2[i]+=pos*x;
	}
}
ll query(ll pos){
    ll res=0;
    for(ll i=pos;i;i-=lowbit(i)){
    	res+=t1[i]*(pos+1)-t2[i];
	}
    return res;
}
char opt[8];
int main(){
    n=read(),m=read();
    for(ll i=1,x;i<=n;i++){
        a[i]=read();
        update(i,a[i]);
    }
    for(ll i=1,x,y;i<=m;i++){
        scanf("%s",opt+1);
        if(opt[1]=='Q'){
            x=read();
            printf("%lld",query(x));pr;
        }
        else{
            x=read(),y=read();
            update(x,y-a[x]);
            a[x]=y;
        }
    }
}

四、吐槽
树状数组扩展性太小了,我爱线段树
~~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值