树状数组(图解+代码详解)

树状数组的用途:

例:给定一个大小为n的数组,要求在数组中对数进行操作。

操作规则:

1.将数组中某个数进行修改(加减运算)

2.将数组从下标a到下标b这个区间的数进行修改(都同时进行加或减运算)

3.查询修改后的某个数或者某个区间

其实,我想读者也应该猜到了一种做法:前缀和

当然,这类问题可以用前缀和来做,但是前缀和做法最坏的情况下的复杂度的会达到O(n^2),当然,这对于大数据而言,是肯定不能接受的,那么我们就需要用“树状数组”的知识来解决这类问题。

接下来我们来逐步引入树状数组的概念!

前置知识点:1.前缀和      2.差分数组       3.lowbit()函数  

1:前缀和

引入一个问题:给定一个数组 a[6]={0,1,2,3,4,5},求区间[1,4]所有元素的和(区间和)?

常规做法:从数组下标1枚举到下标4,依次进行加法,求得最终值10(区间和)

前缀和做法:sum[6]={0,1,3,6,10,15}  最终值为:sum[4]-sum[0]=10(区间和)

不了解前缀和的巨巨们,可能会感觉很神奇,sum数组是如何得到的?为什么是sum[4]-sum[0]的值就是区间和?

(会前缀和的巨巨们可忽略本段)

{

sum数组求法:

void SUM(int a[],int n)
{
	sum[0]=0;
	for(int i=1;i<n;i++)
	sum[i]=sum[i-1]+a[i];//简单的递推式,不会的话,可以自己手动模拟一下过程哦!
}

就拿以上的例子来算区间[2,4]的区间和

我们可以知道,常规做法是从下标1枚举到下标4,再依次加法:2+3+4=9;

由图中可以知道,sum[1]是区间[0,1]区间和,sum[n]是区间[0,n]区间和,那么我们要算区间[2,4]的区间和,就需要知道区间[0,4]和区间[0,1]的区间和。那么肯定有读者会问,为什么不是区间[0,2]的区间和?由以下图解可知!

由此我们得到一个算前缀和区间的公式:求区间[L,R]的区间和:ans=sum[R]-sum[L-1]

代码实现:

int qujianhe(int L,int R)
{
   return sum[R]-sum[L-1];
}

好耶!然后我们就可以用前缀和求任意区间和了

例题:给定一个数组:a[5]={1,3,5,7,9},求区间[2,4]的区间和

代码实现:

#include<stdio.h>
const int N=10001;
int a[N],sum[N];
int n;
int main()
{
	scanf("%d",&n);
	for(int i=0;i<n;i++)
	scanf("%d",&a[i]);
	
	sum[0]=1;//注意这里的sum[0],应该是等于数组中的第一位数
	for(int i=1;i<n;i++)
	sum[i]=sum[i-1]+a[i];
	
	printf("%d",sum[4]-sum[1]);
}

以上就是前缀和的概念,很简单吧!(我相信巨巨们是不在话下的....0.0)

}

2:差分数组

咱们同样引入一个问题:给定一个数组,对数组的某个区间同时加一个数或者减一个数,求某个数修改后的值(所属修改区间内)。

常规做法:枚举区间,然后同时进行加减操作,最后输出想要查询某个位置修改后的值

差分数组:求下标为5的位置修改后的值:sum[5](注意,这里是差分数组的前缀和)

(会差分数组的巨巨们可忽略本段)

{

差分数组的实现:

int Chafen(int a[],int n)
{
	d[0]=0;
	for(int i=1;i<n;i++)
	d[i]=a[i]-a[i-1];
}

然后问题来了,给定差分数组有什么用呢?

问的好!(自问自答-0.0-) 

如果我们想改变一个区间的值(同时变化相等的量),那么它的差分数组是不会发生改变的,但是它的前缀和就会发生改变,一个数发生改变,前缀和就会改变一次,一个区间里面有n个数发生改变,前缀和就会对应每个数改变n次,从而得到新的前缀和,这样是非常麻烦的,那么差分数组所解决的问题就是查询某个变化的值,它同时也运用了前缀和的思想。

仔细看上图:会发现一个神奇的现象

我们会发现:原数组每个位置上的值,就是差分数组里,每个位置的前缀和

举个例子:原数组 a[3]=4   等于  差分数组:sum[3]=0+1+2+1=4

至于解释,跟上面前缀和的解释方法是一样的,这里,就请读者自行证明吧,因为本节的重点是lowbit()函数的用法

对于对区间的修改,其差分数组在修改的区间的差值是不会变的,但是问题来了,对区间进行修改,修改区间的差分是不会变的,但是会影响区间结束后 后面区间的差分,那么如何来进行阻止这一变化呢?

问得好!(再次自问自答,嘻嘻嘻...)

区间修改的代码实现:

void qujianxiugai(int L,int R,int k)//分别代表,左区间,右区间,修改的值
{
	d[L]+=k;
	d[R+1]-=k;
}

                                                                  very good!

例题:给定一个数组a[6]={0,1,3,4,7,11},对区间[1,3]进行加2,修改后的区间,下标为2的值

常规做法:略...

差分数组+前缀和:代码实现

#include<stdio.h>
const int N=10001;
int a[N],sum[N],d[N];
int n;

void qujianxiugai(int L,int R,int k)
{
	d[L]+=k;
	d[R+1]-=k;
}

void Chafen(int a[],int n)
{
	d[0]=0;
	for(int i=1;i<n;i++)
	d[i]=a[i]-a[i-1];
}

int presum(int x)
{
	sum[0]=0;
	for(int i=1;i<n;i++)
	sum[i]=sum[i-1]+d[i];
	return sum[x];
}

int main()
{
	printf("输入数组a[6]\n");
	printf("数组大小为:");
	scanf("%d",&n);
	printf("a数组:");
	for(int i=0;i<n;i++)
	scanf("%d",&a[i]);
	int L,R,k,x;
	printf("输入要修改的区间:");
	scanf("%d%d",&L,&R);
	printf("输入区间变化的值:");
	scanf("%d",&k);
	printf("输入需要查询变化后数组下标:");
	scanf("%d",&x);
	Chafen(a,n);
	qujianxiugai(L,R,k);
	printf("所查询的值为:");
	printf("%d",presum(x));
}

以上就是差分数组的概念,很简单吧!(我相信巨巨们是不在话下的....0.0)

}

3.lowbit()函数

先看lowbit函数的代码:

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

lowbit函数的含义即为:x(二进制)最低位1后面的0组成的数

例如:

5(二进制:101)最低位1后面0的个数为0,则lowbit(5)=2^0=1(十进制)

12(二进制:1100) 最低位1后面0的个数为2,则lowbit(12)=2^2=4(十进制)

如果x为负数怎么办?其实lowbit(-x)=lowbit(x)

好的,咱们学完了前置知识,现在来具体介绍树状数组,经过前缀知识点的学习,大家也许发现了,树状数组其实是前缀和思想的一种优化。

树状数组的概念:

树状数组就是基于一种二进制思想的数据结构,基本用途是维护序列前缀和。对于给定的序列a,设数组数组为c,则c[x]保存在序列a的区间[x-lowbit(x)-1,x]中所有数的和。

主要有以下两个基本操作:

1.单点修改,修改序列a中的某个元素

2.区间查询,查询序列a中区间[1,x]的所有数的和

树状数组图解:

 

 

 

 以上树状数组的截图选自树状数组

总结:其实树状数组的操作,无非就是+-lowbit(x),如果读者还是不懂的话,可以点击以下链接去看更为具体的树状数组讲解。

视频链接:https://www.bilibili.com/video/BV1xq4y1i7et?spm_id_from=333.337.search-card.all.click&vd_source=8080cf7f15002bb90feb6d114558209a

另外,我再给予读者两道树状数组的模板题(附题解)

例题1:https://www.luogu.com.cn/problem/P3374

题解代码:

#include<bits/stdc++.h>
using namespace std;
const int N=500001;
int c[N],n,m;
long long ans;
int lowbit(int x)
{
	return x&(-x);
}

void add(int x,int k)
{
	
	for(int i=x;i<=n;i+=lowbit(i))
	c[i]+=k;
}

void query(int l,int r)
{
	for(int i=r;i;i-=lowbit(i))
	ans+=c[i];
	for(int i=l-1;i;i-=lowbit(i))
	ans-=c[i];
	cout<<ans<<endl;
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int number;
		cin>>number;
		add(i,number);
	}
	
	while(m--)
	{
		int a,x,y;
		cin>>a>>x>>y;
		if(a==1) add(x,y);
		else 
		{
			ans=0;
			query(x,y);
		}
	}
}

例题2:https://www.luogu.com.cn/problem/P3368

题解代码:

#include<bits/stdc++.h>
using namespace std;
const int N=500005;
int a[N],c[N],d[N];
int n,m;

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

void add(int pos,int k)
{
	for(int i=pos;i<=n;i+=lowbit(i))
	c[i]+=k;
}

void query(int pos)
{
	long long ans=0;
	for(int i=pos;i;i-=lowbit(i))
	ans+=c[i];
	cout<<ans<<endl;
}

int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		d[i]=a[i]-a[i-1];
		add(i,d[i]);
	}
	
	while(m--)
	{
		int a,x,y,k;
		cin>>a;
		if(a==1)
		{
			cin>>x>>y>>k;
			add(x,k);
			add(y+1,-k);
		}
		else
		{
			cin>>x;
			query(x);
		}
	}
}

欢迎各位巨巨评论区留言哦!

创作不易,各位读者大大点个赞再走吧(点个关注就更好了q.q..)

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值