树状数组详解(超详细)(完整代码在四 五最后)

本文介绍了树状数组的概念及其优点,作为前缀和和差分思想的优化,可以在O(n)预处理后实现O(1)的区间查询和修改。文章详细阐述了树状数组的位运算基础,查询区间和的步骤,以及单点更新的操作。虽然树状数组支持区间更新,但作者推荐使用线段树来处理此类问题,提供了完整的C++代码示例。
摘要由CSDN通过智能技术生成

一,树状数组的优点

前缀和的思想,可以通过O(n)的预处理,使得多次查询区间值都是o(1),但只能解决不修改,多次查询的问题。

差分思想,能通过差分数组,将区间修改变成O(1)的,最后通过一次O(n),可以恢复成原来的数组,但只能解决多次修改一次询问。

那有没有一种方法,一边修改一边询问,且时间复杂度是可以接受的呢?

当然有,那就是树状数组,代码量小,且可以实现上述操作

二,了解树状数组前置知识点

熟悉位运算,略微涉及补码和补码相关知识

1),补码是计算机中存储整数的方法

第一位是符号位,0表示正数,1表示负数

正数的原码反码补码,三码相同,下面讲的是负数的情况。

反码就是对原码进行取反操作(符号位不变,其他位取反)

补码就对原码先反码,然后再加一

例如:-8 原码 :10001000 反码 11110111 补码 11111000

那如何通过上述知识来获取一个数字最后一位1呢?

我们以40为例,还是假设八位

Lowbit(40)=40&-40=00101000 & 11011000=00001000=8

&表示按位与,即全是1才为1

三,树状数组如何查询区间和

给定一个原数组A,下标 1~n ,

新建一个数组c ,让其中每一个数字,掌管一个区间和。

注:以下内容只需先理解是怎么运行,不用明白原理,因为后文会解释

c[x] 就掌管长度为lowbit(x)的范围,即掌管范围是[x-lowbit(x)+1,x]

这样要询问1~x的前缀和,可以先用lowbit(x)得到前缀里面的一段子区间和,然后x-lowbit(x)

重复操作得到其他区间和,因为只用算log次lowbit,所以log次lowbit便可以得到结果,所以求[L,R],只要算两个前缀和,再相减即可。

下面为代码

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

int query(int x){ //求1~x的区间和 
	int ans=0;
	while(x)
	{
		ans+=d[x];
		x-=lowbit(x);
	}
	return ans;
}

那么为什么可以这样呢?

首先先看一个树状数组的图

 解释一下上图:A为原数组   C为树状数组(每一位存了他掌管的区间)即i-lowbit(i)+1到 i

C[1] lowbit(1)=1 范围 1-1

C[2] lowbit(2)=2 范围 1-2

C[3] lowbit(3)=1 范围 3-3

C[4] lowbit(4)=4 范围 1-4

C[5] lowbit(5)=1 范围 5-5

C[6] lowbit(6)=2 范围 5-6

C[7] lowbit(7)=1 范围 7-7

C[8] lowbit(8)=8 范围 1-8 

而且有个重要性质当前节点位置i ,加上lowbit (i),即为它的父亲节点(可带入上图验证

四,树状数组如何单点更新

若我们要让A i加上 4,那我们就让树状数组中全部包含A i的c i都加上 4

所以接下来就要求,找到包含Ai的所有区间 Ci

之前已经提过 lowbit 不光是掌控区间大小,还是当前点与父亲节点的距离

所以单点更新时,只要每次下标加lowbit ,且更新节点信息即可。

代码如下

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

完整代码如下

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int s[500050],d[500050];
int lowbit(int x){
	return x&(-x);
}

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

int query(int x){ //求1~x的区间和 
	int ans=0;
	while(x)
	{
		ans+=d[x];
		x-=lowbit(x);
	}
	return ans;
}
signed main()
{ 
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>s[i];
	}
	for(int i=1;i<=n;i++)
	{
		add(i,s[i]);
	}
	for(int i=1;i<=m;i++)
	{
		int op,x,k;
		cin>>op;
		if(op==1)
		{
			cin>>x>>k;
			add(x,k);
		}
		else{
			cin>>x>>k;
			int sum1=query(k);
			int sum2=query(x-1);
			cout<<sum1-sum2<<endl;
		}
	}
	return 0;
}

五,树状数组的区间更新(还是推荐用线段树来维护区间更新)

树状数组是可以支持区间更新的,但是线段树在思维上更有优势,并且可拓展性强

A 还是原数组,D 表示差分数组,D[i]=A[i]-A[i-1],且让D1=A1

这样我们用树状数组Ci来维护Di这个差分数组,而不是Ai原数组

求Ai变成了D1 到Di的前缀和,用树状数组实现

如过要让 L-R区间加上x ,就变成了单点更新,即让D[L] 加x D[R+1] 减去x

ok下面是区间更新,单点查询完整代码

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
int s[500050],d[500050];
int lowbit(int x){
	return x&(-x);
}

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

int query(int x){ //求1~x的区间和 
	int ans=0;
	while(x){
		ans+=d[x];
		x-=lowbit(x);
	}
	return ans;
}
signed main(){ 
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>s[i];
	}
	for(int i=1;i<=n;i++){
		add(i,s[i]-s[i-1]); 
	}
	for(int i=1;i<=m;i++){
		int op;
		cin>>op;
		if(op==1)
		{
			int x,y,k;
			cin>>x>>y>>k;
			add(x,k);
			add(y+1,-k);
		}
		else
		{
			int x;
			cin>>x;
			cout<<query(x)<<endl;
		}
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值