[信息学][C++][数据结构]浅谈树状数组

引入

什么是树状数组?它能做什么?
树状数组是一种支持单点修改区间求和的数据结构
当然,这并不代表它的作用仅限于此,它同样可以实现区间修改

基础树状数组

基础树状数组能够用 O ( n log ⁡ ( n ) ) O(n\log(n)) O(nlog(n))的时间实现单点修改区间求和

树状数组的存储

当我们想要求一个数列的和时,我们就直接累加(毕竟暴力出奇迹 ),但这样是不是慢了一点……
事实上,树状数组就可以解决这个问题。它就有一点倍增的思想在内了。
首先,我们先来了解一下 l o w b i t lowbit lowbit 函数,树状数组就与它有关。它是指一个数转化为二进制后最小位的“1”所代表的值
举个栗子, l o w b i t ( 1 ) = 1 lowbit(1)=1 lowbit(1)=1 l o w b i t ( 6 ) = 2 lowbit(6)=2 lowbit(6)=2
理解了?
要不我们现在开始求 l o w b i t ( i ) lowbit(i) lowbit(i)吧,把 i i i化为二进制固然可以,但代码太长了(是的没错,四五行确实太长了
其实通过补码我们就能求出 l o w b i t ( i ) lowbit(i) lowbit(i)
奉上代码:

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

好,我们继续来看,如果有了 l o w b i t lowbit lowbit ,数字可以怎么拆分?
嗯,要不先从“6”开始吧。“6”按照二进制可拆分为“110”,没错吧。
我们按照从低位到高位来拆分,那么6可以先拆出一个2,即 l o w b i t ( 6 ) lowbit(6) lowbit(6) 。剩下4,而 l o w b i t ( 4 ) lowbit(4) lowbit(4) 正好等于4,拆完了。
其实树状数组的存储和使用我们到这里都讲完了,如果你注意思考的话。
我们可以让 s [ i ] s[i] s[i] 存储 i i i 为尾连续 l o w b i t ( i ) lowbit(i) lowbit(i) 个数的和。这样存储的部分就结束了。树状数组的存储时间复杂度分析 对于每个 s [ i ] s[i] s[i],都会加 x ≤ log ⁡ ( i ) x \le \log(i) xlog(i) 次,所以时间复杂度为 n log ⁡ ( n ) n\log(n) nlog(n)

树状数组的修改

修改就按照上面的树就行啦
比如说,如果 a [ 5 ] a[5] a[5] 改变了,那么 s [ 5 ] , s [ 6 ] , s [ 8 ] s[5],s[6],s[8] s[5],s[6],s[8]就要改变,我们发现它实际上可以稍微移动,就是完全二叉树。每次修改都是 l o g ( n ) log(n) log(n)的啦。

void add(int x,int v){
	while(x<=n){
		c[x]+=v;
		x+=lowbit(x);
	}
}

树状数组的求和

在上面的树中,我们就能发现,
a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] + a [ 5 ] + a [ 6 ] + a [ 7 ] a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7] a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]可以拆分为:
a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] a[1]+a[2]+a[3]+a[4] a[1]+a[2]+a[3]+a[4], a [ 5 ] + a [ 6 ] a[5]+a[6] a[5]+a[6], a [ 7 ] a[7] a[7]
这就和前面的拆分方法有关了
代码如下:

int sum(int x){
	int ret=0;
	while(x>0){
		ret+=c[x];
		x-=lowbit(x);
	}
	return ret;
}

所以 a [ l ] + a [ l + 1 ] + . . . . . . + a [ r ] a[l]+a[l+1]+......+a[r] a[l]+a[l+1]+......+a[r]怎么求呢,当然是 s u m ( r ) − s u m ( l − 1 ) sum(r)-sum(l-1) sum(r)sum(l1)
到这里,基础的树状数组部分正式结束,要不看几道题吧

题目

洛谷P3374【模版】树状数组1

P3374题目通道
这真的是模板题了,没什么好说的,把前面的总结一下就能做出来了
AC代码:

#include<iostream>
using namespace std;
int n,m,c[1000005];
int lowbit(int x){
	return x&-x;
}
void add(int x,int v){
	while(x<=n){
		c[x]+=v;
		x+=lowbit(x);
	}
}
int sum(int x){
	int ret=0;
	while(x>0){
		ret+=c[x];
		x-=lowbit(x);
	}
	return ret;
}
int main(){
	cin>>n>>m;
	for(int i=1,x;i<=n;i++){
		cin>>x;
		add(i,x);
	}
	for(int i=1,k,l,r;i<=m;i++){
		cin>>k>>l>>r;
		if(k==2){
			cout<<sum(r)-sum(l-1)<<endl;
		}else{
			add(l,r);
		}
	}
	return 0;
}

先把模板给出来,题目的话后面我再补充

树状数组+差分

光是树状数组只能实现单点修改,但是如果你会前缀和差分的话,就可以实现区间修改
本着循序渐进的原则,我们先来讨论较简单的区间修改单点查询

单点查询

我们可以按照差分的方式来存储原数列,这个确实挺简单,如果你会差分(不会就任由天命吧
假设我们用差分存储的数列叫 b [ i ] b[i] b[i],那么按照差分的性质, b [ i ] = a [ i ] − a [ i − 1 ] b[i]=a[i]-a[i-1] b[i]=a[i]a[i1]。注意:我们默认 a [ 0 ] = 0 a[0]=0 a[0]=0,尽管并没有输入。
那么如果要对 [ l , r ] [l,r] [l,r]内的 a [ i ] a[i] a[i]进行修改(比如说加 x x x),怎么办
差分的思想告诉我们,可以让 b [ l ] b[l] b[l] x x x b [ r + 1 ] b[r+1] b[r+1] x x x,查询 a [ i ] a[i] a[i]的时候我们就理解为求 b [ i ] b[i] b[i]的前缀和。
那现在我们要能实现单点修改区间查询,怎么实现?
树状数组!对吧,问题就解决了。
这里同样有一道模版题

题目

洛谷P3368【模版】树状数组2

P3368题目通道
思路前面都讲过了,这里直接上代码吧

#include<iostream>
using namespace std;
int n,m,c[1000005],y;
int lowbit(int x){
	return x&-x;
}
void add(int x,int v){
	while(x<=n){
		c[x]+=v;
		x+=lowbit(x);
	}
}
int sum(int x){
	int ret=0;
	while(x>0){
		ret+=c[x];
		x-=lowbit(x);
	}
	return ret;
}
int main(){
	cin>>n>>m;
	for(int i=1,x;i<=n;i++){
		cin>>x;
		add(i,x-y);
		y=x;
	}
	for(int i=1,k,l,r,a;i<=m;i++){
		cin>>k;
		if(k==2){
			cin>>a;
			cout<<sum(a)<<endl;
		}else{
			cin>>l>>r>>a;
			add(l,a);
			add(r+1,-a);
		}
	}
	return 0;
}

那现在我们只差 区间查询+一些题目了,对吧

区间查询

我们还是来看看我们要求的到底是什么吧,假如说 a [ i ] a[i] a[i]的前缀和数组是 q [ i ] q[i] q[i]
但注意,我们维护的并不是 a [ i ] a[i] a[i],而是 b [ i ] b[i] b[i]
q [ x ] = ∑ i = 1 x a [ i ] q[x]=\sum_{i=1}^x a[i] q[x]=i=1xa[i]
q [ x ] = ∑ i = 1 x ∑ j = 1 i b [ j ] q[x]=\sum_{i=1}^x \sum_{j=1}^i b[j] q[x]=i=1xj=1ib[j]
∴ q [ x ] = ∑ i = 1 x ( x − i + 1 ) b [ i ] ∴q[x]=\sum_{i=1}^x (x-i+1)b[i] q[x]=i=1x(xi+1)b[i]
可是我们在查询前是不知道 x x x是多少的,那我们就没法直接用树状数组维护。
但我们发现稍微变一下就简单多了
∵ q [ x ] = ∑ i = 1 x ( x − i + 1 ) b [ i ] ∵q[x]=\sum_{i=1}^x (x-i+1)b[i] q[x]=i=1x(xi+1)b[i]
∴ q [ x ] = ( x + 1 ) ∑ i = 1 x b [ i ] − ∑ i = 1 x i b [ i ] ∴q[x]=(x+1)\sum_{i=1}^x b[i]-\sum_{i=1}^x ib[i] q[x]=(x+1)i=1xb[i]i=1xib[i]
发现了吗?现在我们要维护的有两个 ∑ i = 1 x b [ i ] \sum_{i=1}^x b[i] i=1xb[i] ∑ i = 1 x i b [ i ] \sum_{i=1}^x ib[i] i=1xib[i]
这个知识点的部分我还没找到有关题目,就先不放代码了。如果有找到的,可以放在评论区。然后前面的部分如果有比较好的题目的话,也可以放在评论区。

写在最后

感谢各位花了这么长的时间来看一个初二牲蒟蒻的博客。
我的博客

  • 14
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值