树状数组模板

首先对于追求编码简单快捷的同学来说,比如ACM。树状数组可以实现的线段树都能实现。

树状数组是动态维护前缀和的工具

树状数组:
1.单点修改,单点查询
2.区间修改,单点查询
3.单点修改,区间查询
这三种我们使用树状数组比线段树敲起来简单,似乎还更快

至于其他,就敲线段树吧

区间修改,单点查询用数组数组也可以的后面有补充

那么修改指什么呢?比如给某个数加上一个数,或者给某个区间(要结合差分数组)加上一个数

查询又可以是什么呢?比如查询某个点的前缀和,进而到区间求和。

至于时间复杂度你都来百度模板了,那应该懂时间节省在哪了 其实省时你会发现就省在区间操作,使O(n)降至O(log n)

1.单点修改,区间查询

例题:P3374 【模板】树状数组 1
如题,已知一个数列,你需要进行下面两种操作:

将某一个数加上 xx

求出某区间每一个数的和

输入格式

第一行包含两个正整数 n,mn,m,分别表示该数列数字的个数和操作的总个数。

第二行包含 nn 个用空格分隔的整数,其中第 ii 个数字表示数列第 ii 项的初始值。

接下来 mm 行每行包含 33 个整数,表示一个操作,具体如下:

1 x k 含义:将第 x 个数加上 k

2 x y 含义:输出区间 [x,y][x,y] 内每个数的和

输出格式

输出包含若干行整数,即为所有操作 22 的结果。

输入输出样例

5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4

输出 #1

14
16

AC模板:

#include<bits/stdc++.h>
using namespace std;
#define ll long long int
int n;//长度 
ll a[1000005];//原输入数组 
ll tree[1000005];//树状数组空间复杂度O(n) 
int lowbit(int x)//x二进制数的末尾最后一个1和其后的所有0构成的二进制数 
{
	return x&(-x); //取反+1就等于 -x 
}//返回的值就是其覆盖的长度 
void add(int x,ll k)//单点修改 O(log n)
{
	for(;x<=n;x+=lowbit(x))
	tree[x]+=k;
	return; 
}//层层往父亲节点修改 
ll ask(int x)//查询x的前缀和,向左上查找 O(log n) 
{
	ll ans=0ll;
	for(;x;x-=lowbit(x))
	ans+=tree[x];
	return ans;
}
//区间<->单点 
int main(void)
{
	int q,k;
	ll x;
	scanf("%d %d",&n,&q);
	for(int i=1;i<=n;i++){
		scanf("%lld",&x);
		add(i,x);
	}
	while(q--)
	{
		int key;
		ll r,l;
		scanf("%d %lld %lld",&key,&l,&r);
		if(key==1) add(l,r);//单点修改 给定 l, r,将 a[l] 加上 r;
		else//求区间【l,r】的和  两个前缀和相减 
		printf("%lld\n",ask(r)-ask(l-1));
	}
	return 0;
}

由于tree数组初始化为0,add()既可以进行“建树”,又可以进行单点修改

2.区间修改,单点查询

区间修改:如果将x到y区间加上一个k,那就是引入差分数组修改x和y+1两点的差分值即可。

题目描述

如题,已知一个数列,你需要进行下面两种操作:

1.将某区间每一个数数加上x

2.求出某一个数的值

输入格式

第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。

第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。

接下来M行每行包含2或4个整数,表示一个操作,具体如下:

操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k

操作2: 格式:2 x 含义:输出第x个数的值

输出格式

输出包含若干行整数,即为所有操作2的结果。

输入输出样例

输入 #1

5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4

输出 #1

6
10

引入差分数组求区间修改问题的
何为差分数组:用洛谷P3368一楼大佬的题解:{

介绍一下差分

设数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}

也就是说b[i]=a[i]-a[i-1];(a[0]=0;),那么a[i]=b[1]+…+b[i];(这个很好证的)。

假如区间[2,4]都加上2的话

a数组变为a[]={1,8,10,7,10},b数组变为b={1,7,2,-3,3};

发现了没有,b数组只有b[2]和b[5]变了,因为区间[2,4]是同时加上2的,所以在区间内b[i]-b[i-1]是不变的.

所以对区间[x,y]进行修改,只用修改b[x]与b[y+1]:

b[x]=b[x]+k;b[y+1]=b[y+1]-k;
}

这里再多讲一点:树状数组是维护前缀和的工具
差分数组优点在于区间修改,求某个数修改后的值需求前缀和,用差分数组时一般在输出时要遍历多个连续的数据
前者适用在线修改查询,后者适用于离线查询
送一道差分数组的题https://blog.csdn.net/qq_43791377/article/details/103394793

所以我们求区间修改问题就变为:我们用树状数组维护差分数组
那么当对 x ~ y 的区间进行修改的时候需要在树状数组中的第 x 个位置 + k, 第 y + 1 个位置 -k
那么代码只需小小的修改即可:

AC代码:

#include<bits/stdc++.h>
using namespace std;
#define ll long long int
int n;//长度  
int a[1000005];//原输入数组 
ll tree[1000005];//树状数组空间复杂度O(n) 
int lowbit(int x)//x二进制数的末尾最后一个1和其后的所有0构成的二进制数 
{
	return x&(-x); //取反+1就等于 -x 
}//返回的值就是其覆盖的长度 
void add(int x,ll k)//单点修改 O(log n) 
{
	for(;x<=n;x+=lowbit(x))
	tree[x]+=k;
	return; 
}//层层往父亲节点修改 
ll ask(int x)//查询x的前缀和,向左上查找 O(log n) 
{
	ll ans=0ll;
	for(;x;x-=lowbit(x))
	ans+=tree[x];
	return ans;
}
//区间<->单点 
int main(void)
{
	int q,k;
	ll x;
	scanf("%d %d",&n,&q);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		add(i,a[i]-a[i-1]);//维护差分数组 
	}
	while(q--)
	{
		int key;
		ll r,l;
		scanf("%d",&key);
		if(key==1)//区间【l,r】都加 x
		{
			scanf("%d %d %lld",&l,&r,&x);
			//由于树状数组维护的是差分数组: 
			add(l,x);
			add(r+1,-x);
		}
		else
		{
			scanf("%d",&l);
			printf("%lld\n",ask(l));//为什么是前缀和呢?注意这是差分数组 
			//故ask(l)就是a[l]的值 
		}
	}
	return 0;
}

为社么要用树状数组维护差分数组而不是原数组??

引入差分数组就不用修改n-x次add()了【O(nlogn)+查询O(2log n)】,直接修改两个点【[O(2)+查询O(logn)]的值就行了!!!这样看快了好多有木有

补充:

前面我说过 “区间修改,单点查询”不建议用数组数组,但是我发现引入差分数组后,“区间修改,单点查询”也不会说用树状数组很难打。

区间修改,单点查询
需要题目的请点击:洛谷P3372
直接用B站某位大佬的视频截图吧:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
总结:其实就是用两个树状数组维护差分数组 b[i] 和 i*b[i],还有计算前缀和(O(logn))

参考代码:(也不长,不难理解)

#include<bits/stdc++.h>
using namespace std;
#define ll long long int
int n;//长度  
int a[1000005];//原输入数组 
ll tree1[1000005];//维护差分数组b 
ll tree2[1000005];//维护i*b[i]  为什么要维护这两个,因为后面区间查询时要求这两个的前缀和 
int lowbit(int x)
{
	return x&(-x); //取反+1就等于 -x 
}//返回的值就是其覆盖的长度 
void add(int x,ll k)//由于维护的是差分数组故相当于区间修改 
{
	for(int i=x;i<=n;i+=lowbit(i))
	tree1[i]+=k,tree2[i]+=x*k;
	return; 
}//层层往父亲节点修改(修改父亲节点的值就是某一段的前缀和) ,所以为i 
ll ask(int x)//查询x的前缀和,并计算 a[x]的 前缀和 
{
	ll ans=0ll;
	for(int i=x;i;i-=lowbit(i))
	ans+=(x+1)*tree1[i]-tree2[i];
	return ans;
}
//a[x]的前缀和=∑∑d[j]=∑d[i](x-i+1)=(x+1)*(∑d[i])-(∑d[i]*i)     注‘∑’表示1~x求和 
ll rang_ask(int l,int r)//区间查询 
{
	return ask(r)-ask(l-1); 
}
int main(void)
{
	int q,k;
	ll x;
	scanf("%d %d",&n,&q);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		add(i,a[i]-a[i-1]);//维护差分数组 
	}
	while(q--)
	{
		int key;
		ll r,l;
		scanf("%d",&key);
		if(key==1)//区间【l,r】都加 x
		{
			scanf("%d %d %lld",&l,&r,&x);
			//由于树状数组维护的是差分数组: 
			add(l,x);
			add(r+1,-x);
		}
		else//求区间和,就是求 两个前缀和的差 ! 
		{
			scanf("%d %d",&l,&r);
			printf("%lld\n",rang_ask(l,r));
		}
	}
	return 0;
}

区间查询也可以写成这样:

ll ask(ll *z,int x)//单点查询前缀和 
{
	ll ans=0ll;
	for(int i=x;i;i-=lowbit(i))
	ans+=z[i]; 
	return ans;
}
ll rang_ask(int l,int r)//区间查询 
{
	return ((r+1)*ask(tree1,r)-ask(tree2,r)) - ( (l-1 +1)*ask(tree1,l-1)-ask(tree2,l-1) ) ;
}

需要看线段树代码的请点击

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值