简单の暑假总结——树状数组

2.1 树状数组

树状数组,顾名思义,长得像树的数组,用于处理一些单点修改以及区间查询的问题。其时间复杂度为 O ( log ⁡ 2 n ) O(\log _2n) O(log2n) 。如过我们使用一些一般的数据结构,那么一定会 TLE。这时,树状数组就可以帮助我们解决这些问题。

Tips:

树状数组和线段树具有相似的功能,但他俩毕竟还有一些区别:树状数组能有的操作,线段树一定有;线段树有的操作,树状数组不一定有。但是树状数组的代码要比线段树短,思维更清晰,速度也更快,在解决一些单点修改的问题时,树状数组是不二之选。——OI WiKi

在我正式介绍树状数组的三大基本操作之前,我们要先了解树状数组的内部结构及基本函数:

在这里插入图片描述

对于一个数组 A [   6   ] = { 1 , 2 , 5 , 8 , 10 , 20 } A[\ 6\ ]=\left\{1,2,5,8,10,20\right\} A[ 6 ]={1,2,5,8,10,20} ,它所生成的树状数组为 B i t [   6   ] = { 1 , 3 , 5 , 16 , 10 , 30 } Bit[\ 6\ ]=\left\{1,3,5,16,10,30\right\} Bit[ 6 ]={1,3,5,16,10,30} ,如上图所示。

那么,我们可以明显发现,对于 B i t [   i   ] Bit[\ i\ ] Bit[ i ] 而言,它应该等于 a [   i   ] + B i t [   k   ] ( 1 ⩽ k < i ) a[\ i\ ]+Bit[\ k\ ](1 \leqslant k <i ) a[ i ]+Bit[ k ](1k<i)

那么,如何求得 i i i 呢?

这牵扯到树状数组的一个关键函数: L o w B i t LowBit LowBit 函数(烦诶,还能不能进正题!

2.1.1 LowBit 函数

理解 LowBit 的含义:

L o w B i t [   i   ] LowBit[\ i\ ] LowBit[ i ] 存储着 i i i 转化成二进制数之后,只保留最低位的 1 1 1 及其后面的 0 0 0,然后再转成十进制数后的书。(也就是树状数组中第 i i i 号位的子叶个数)(当然,在实际操作中,LowBit 往往以函数的形式出现)

那么,我们一般有两种方法求 LowBit:

int LowBit_1(int num){
	return num&-num;
}
int LowBit_2(int num){
	return num-(num&(num–1));
}
//我们在实际操作中往往使用第1种

下面,我来简单证明一下为什么第 1 1 1 种方法是可行的:

假设有一整数 N N N ,若我们直接将 N N N 按位取反得到 M M M 后,会有 N & M = 0 N \& M=0 N&M=0

但是,在二进制中, N N N 的相反数应该是 M + 1 M+1 M+1 那么答案就有所不同。

在假设 N N N 的最后一位 1 1 1 出现在第 p p p 位,一共有 q q q 位,那么 N N N 的第 p + 1 p+1 p+1 ,第 p + 2 p+2 p+2 位一直到第 q q q 位都应该是 0 0 0

易得: M M M 的第 p + 1 p+1 p+1 ,第 p + 2 p+2 p+2 位一直到第 q q q 位都应该是 1 1 1 ,而第 p p p 位是 0 0 0

因为有加 1 1 1 操作,所以,第 q q q 位就变为 2 2 2 ,向前进 1 1 1 ,第 q − 1 q-1 q1 位就变为 2 2 2 ,向前进 1 1 1

如此相加,直到第 p p p 位变为 1 1 1 为止。

那么此时, M M M 的第 p + 1 p+1 p+1 ,第 p + 2 p+2 p+2 位一直到第 q q q 位都应该是 0 0 0 ,而第 p p p 位是 1 1 1

由于 N , M N,M N,M 的第 1 1 1 位到第 p − 1 p-1 p1 位都未改变,所以只考虑第 p p p 位到第 q q q 位。

N N N 的第 p p p 位是 1 1 1 ,而第 p + 1 p+1 p+1 ,第 p + 2 p+2 p+2 位一直到第 q q q 位都是 0 0 0

M M M 的第 p p p 位是 1 1 1 ,而第 p + 1 p+1 p+1 ,第 p + 2 p+2 p+2 位一直到第 q q q 位都是 0 0 0

很明显,最终结果只有第 p p p 位是 1 1 1 ,而其余全部是 0 0 0 .

最终结果与假设相符,命题得证

为什么我看着像伪证?

有了 LowBit ,我们就可以进行其它操作了。

2.1.2 UpDate 函数

UpDate 就是将一个新元素 N N N 塞进 B i t Bit Bit 数组里的一个操作。

一般来讲,我们会在输入 A A A 数组时,通过 UpDate 初始化 B i t Bit Bit 数组,也会在修改 A A A 数组里面的值时,用 UpDate 来更新 B i t Bit Bit 数组

但是,由于树状数组的结构特殊,所以在处理时不能只修改 B i t [   i   ] Bit[\ i\ ] Bit[ i ] 的值:

在这里插入图片描述

还是这个例子:假设我们要改变 A [   1   ] A[\ 1\ ] A[ 1 ] 里的值,我们实际上要修改 B i t [   1   ] , B i t [   2   ] , B i t [   4   ] Bit[\ 1\ ],Bit[\ 2\ ],Bit[\ 4\ ] Bit[ 1 ],Bit[ 2 ],Bit[ 4 ] 的值。

显然,对于 B i t [   i   ] Bit[\ i\ ] Bit[ i ] ,它与 B i t [   i + L o w B i t (   i   )   ] Bit[\ i+LowBit(\ i\ )\ ] Bit[ i+LowBit( i ) ] 之间有 1 1 1 条连线,自然,处理 B i t [   i   ] Bit[\ i\ ] Bit[ i ] 时,我们也需要将 B i t [   i + L o w B i t (   i   )   ] Bit[\ i+LowBit(\ i\ )\ ] Bit[ i+LowBit( i ) ] 处理了。

void UpDate(int num,int sum){
	for(int i=num;i<=n;i+=LowBit(i)){
		Bit[i]+=sum;			//上文所述,与 Bit[i] 有关系的全部都要处理
	}
}

2.1.3 Sum 函数

在实际操作中,往往需要求得区间和或某一点的值,一般情况下,我们会使用前缀和。但是,由于我们对 A A A 数组进行了修改,使用前缀和的时间复杂度会有所提升,这时,我们可以使用 Sum 函数来解决这一问题。

S u m (   i   ) Sum(\ i\ ) Sum( i ) 用于求解 A [   1   ] + A [   2   ] + ⋯ + A [   i   ] A[\ 1\ ]+A[\ 2\ ]+\cdots+A[\ i\ ] A[ 1 ]+A[ 2 ]++A[ i ] 的值。但是,我们可以观察下图,很明显, A [   1   ] + A [   2   ] + A [   3   ] + A [   4   ] = B i t [   4   ] A[\ 1\ ]+A[\ 2\ ]+A[\ 3\ ]+A[\ 4\ ]=Bit[\ 4\ ] A[ 1 ]+A[ 2 ]+A[ 3 ]+A[ 4 ]=Bit[ 4 ] A [   5   ] + A [   6   ] = B i t [   6   ] A[\ 5\ ]+A[\ 6\ ]=Bit[\ 6\ ] A[ 5 ]+A[ 6 ]=Bit[ 6 ] 。而 L o w B i t (   4   ) = 4 , L o w B i t (   6   ) = 2 LowBit(\ 4\ )=4,LowBit(\ 6\ )=2 LowBit( 4 )=4,LowBit( 6 )=2,自然我们可以通过 LowBit 以及 B i t Bit Bit 数组来达到缩减时间复杂度的效果。

long long int Sum(int num){			//一般情况下,Sum 函数需要开 long long
	long long int ans=0;
	for(int i=num;i>=1;i-=LowBit(i)){
		ans+=Bit[i];			//上文已提,可优化时间复杂度
	}
	return ans;
}

好的,三个基本函数已经具备了,我们可以欣赏三个基本操作了。

2.2 一维树状数组基本操作

接下来,我们将依次介绍单点修改,区间查询区间查询,单点修改区间修改,区间查询三个基本操作

2.2.1 单点修改,区间查询

单点修改,区间查询应该是最简单的一个操作,直接套用三个函数即可,只是在进行区间查询时需要注意采用前缀和的思想,即 S u m ( r i g h t ) − S u m ( l e f t − 1 ) Sum(right)-Sum(left-1) Sum(right)Sum(left1)

Eg_1 【模板】树状数组 1

这道题是就是一道裸题(英语老师:说了多少次了不能裸奔!

#include<cstdio>
long long int a[500005],Bit[500005];
int n,m,x,y,z;
int Low_Bit(int num){
	return num&-num;
}
void Up_Date(int num,int sum){
	for(int i=num;i<=n;i+=Low_Bit(i)){
		Bit[i]+=sum;
	}
}
long long int Sum(int num){
	long long int ans=0;
	for(int i=num;i>=1;i-=Low_Bit(i)){
		ans+=Bit[i];
	}
	return ans;
}			//基本操作。注意 long long 
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		Up_Date(i,a[i]);			//对 Bit 进行初始化
	}
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&x,&y,&z);
		if(x==1){			//修改操作
			Up_Date(y,z);
		}else{			//查询操作
			printf("%lld\n",Sum(z)-Sum(y-1));			//前缀和求解
		}
	}
	return 0;
}

2.2.2 区间修改,单点查询

如果抛开树状数组不看,我们一般用什么进行区间修改,单点查询呢?

是的,差分数组!

那么,我们需要简单回顾一下如何用差分数组进行区间修改,单点查询。

现有一数组 A [   5   ] = { 1 , 2 , 3 , 4 , 5 } A[\ 5\ ]=\{1,2,3,4,5\} A[ 5 ]={1,2,3,4,5} ,其对应的差分数组为 P [   5   ] = { 1 , 1 , 1 , 1 , 1 } P[\ 5\ ]=\{1,1,1,1,1\} P[ 5 ]={1,1,1,1,1}

现在,我们将区间 [   2 , 4   ] [\ 2,4\ ] [ 2,4 ] 加上 1 1 1 那么我们在差分数组中,只需要 P [   2   ] + = 1 , P [   5   ] − = 1 P[\ 2\ ]+=1,P[\ 5\ ]-=1 P[ 2 ]+=1,P[ 5 ]=1 即可。

我们都知道,差分数组的前缀和数组是原数组,当我们要将区间 [   l , r   ] [\ l,r\ ] [ l,r ] 加上 x x x 时,我们需要将 P [   l   ] P[\ l\ ] P[ l ] 加上 x x x ,这样,我们的数组从第 l l l 号元素开始都会增加 x x x ,同样,当我们将 P [   r   ] P[\ r\ ] P[ r ] 减去 x x x 后,数组从第 r r r 号元素开始都会减去 x x x ,和前面的增加 x x x 刚好抵消,我们也i就达到了 P [   l   ] P[\ l\ ] P[ l ] 加上 x x x 的效果。

那么,我们只需要将差分数组的操作塞进树状数组里就可以了。

Eg_2 【模板】树状数组 2

裸题梅开二度

#include<cstdio>
long long int a[1000005],Bit[1000005],p[1000005];
long long int n,m,x,y,z;
long long int lowbit(long long int num){
	return num&-num;
}
void update(long long int num,long long int sum){
	for(int i=num;i<=n;i+=lowbit(i)){
		Bit[i]+=sum;
	}
}
long long int Sum(long long int num){
	long long int ans=0;
	for(int i=num;i>=1;i-=lowbit(i)){
		ans+=Bit[i];
	}
	return ans;
}			//基本操作
int main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		p[i]=a[i]-a[i-1];			//求得初始差分数组
		update(i,p[i]);			//塞入树状数组
	}
	for(int i=1;i<=m;i++){
		scanf("%lld",&x);
		if(x==1){			//更改操作
			scanf("%lld%lld%lld",&x,&y,&z);
			update(x,z);			//修改 Bit[x]
			update(y+1,-z);				//修改 Bit[y+1]
		}else{			//查询操作
			scanf("%lld",&y);
			printf("%lld\n",Sum(y));			//求差分数组的前缀和即可
		}
	}
	return 0;
}

2.2.3 区间修改,区间查询

那么,对于区间修改这一操作,我们依旧可以使用差分数组来完成,那么,我们如何进行区间查询操作呢?

设原数组为 A A A ,差分数组为 P P P ,那么,求区间 [   1 , i   ] [\ 1,i\ ] [ 1,i ] 的结果为:

S u m (   1 , i   ) = ∑ x = 1 i a [   x   ] = ∑ x = 1 i ∑ y = 1 x P [   y   ] = ∑ x = 1 i x × P [   i − x + 1   ] = i × ∑ x = 1 i P [   i   ] − ∑ x = 1 i ( x − 1 ) × P [   x   ] \begin{aligned}Sum(\ 1,i\ ) &=\sum_{x=1}^ia[\ x\ ]\\& = \sum_{x=1}^i\sum_{y=1}^xP[\ y\ ]\\ & =\sum_{x=1}^ix\times P[\ i-x+1\ ]\\&=i\times \sum_{x=1}^iP[\ i\ ]-\sum_{x=1}^i(x-1)\times P[\ x\ ]\end{aligned} Sum( 1,i )=x=1ia[ x ]=x=1iy=1xP[ y ]=x=1ix×P[ ix+1 ]=i×x=1iP[ i ]x=1i(x1)×P[ x ]

观察减式两边,我们发现:左边式子的基本结构为 P [   i   ] P[\ i\ ] P[ i ] ,而右边式子的基本结构为 i × P [   i + 1   ] i\times P[\ i+1\ ] i×P[ i+1 ] 。那么,我们就可以分别用两个树状数组进行维护。

在修改时,分别对两个树状数组进行更改,查询时,通过前缀和,套用上面的公式即可。

Eg_3 区间修改,区间查询

又双叒叕是道裸题

#include<cstdio>
long long int a[1000005],Bit[1000005],Bit_1[1000005],p[1000005];
long long int n,m,x,y,z;
long long int Low_Bit(long long int num){
	return num&-num;
}
void Up_Date(long long int num,long long int sum){
	for(int i=num;i<=n;i+=Low_Bit(i)){
		Bit[i]+=sum;
		Bit_1[i]+=sum*(num-1);			//上文已提,分别维护 P[i] 和 P[i]*(i-1) 的值
	}
}
long long int Sum(long long int num){
	long long int ans=0;
	for(int i=num;i>=1;i-=Low_Bit(i)){
		ans+=Bit[i]*num-Bit_1[i];			//上文已提,套用公式即可
	}
	return ans;
}
int main(){
	scanf("%lld%lld",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		p[i]=p[i-1]+a[i];
		Up_Date(i,a[i]-a[i-1]);			//塞入差分数组
	}
	for(int i=1;i<=m;i++){
		scanf("%lld",&x);
		if(x==1){			//修改
			scanf("%lld%lld%lld",&x,&y,&z);
			Up_Date(x,z);
			Up_Date(y+1,-z);			//按照差分思想进行修改
		}else{			//查询
			scanf("%lld%lld",&y,&z);
			printf("%lld\n",Sum(z)-Sum(y-1));			//按照前缀和思想进行求解
		}
	}
	return 0;
}

2.3 离散化

考虑到在用树状数组时,往往需要使用离散化,在这里简单介绍一下

离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。(其实就是哈希的一种)

有些数据本身很大自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对位置有关,而与具体是多少无关时,可以进行离散化。比如当数据个数很小,数据范围却很大时(超过1e9)就考虑离散化成更小的值,能够实现更多的算法。——PPT

举个例子:我们有数组 A [   5   ] = { 114514 , 996 , 857 , 442759 , 2147483647 } A[\ 5\ ]=\{114514,996,857,442759,2147483647\} A[ 5 ]={114514,996,857,442759,2147483647} 。显然,如果要将 A [   i   ] A[\ i\ ] A[ i ] 作为数组下标,显然是不行的。采用离散化,我们就可以将 A A A 数组改为 A [   5   ] = { 3 , 2 , 1 , 4 , 5 } A[\ 5\ ]=\{3,2,1,4,5\} A[ 5 ]={3,2,1,4,5}

废话那么多,所以如何离散化怎么写呢?

2.3.1 结构体离散化

我们在输入数据时,可以通过结构体得到对应的下标。我们通过对数据进行从小到大的排序,会得到一个相关的下表的顺序,越靠前的下标所对应的数值越小,所以,在遍历时,我们也要将一个小一点的值给它。

#include<cstdio>
#include<algorithm>
using namespace std;
struct node{
	int num,sum;			//sum 存数值,num 存下标
}a[1005];
bool cmp(node a,node b){
	return a.sum<b.sum;			//按 sum 从小到大排序
}
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i].sum);
		a[i].num=i;
	}
	sort(a+1,a+1+n,cmp);
	for(int i=1;i<=n;i++){
		a[a[i].num].sum=i;			//对对应的下标所对应的数值进行更改
	}
	for(int i=1;i<=n;i++){
		printf("%d ",a[i].sum);
	}
	return 0;
}

运行结果如下:
在这里插入图片描述

2.3.2 STL+二分离散化(这个常用)

对于上面的离散化方法,有一点美中不足的地方:

在这里插入图片描述

如上图所示,有时候,我们希望它最后得到的结果是 1 , 1 , 2 , 3 , 3 1,1,2,3,3 1,1,2,3,3 ,怎么办呢?

那么,我们就需要两个函数: sort 和 lower_bound 函数

sort 函数大家都很熟悉,lower_bound 函数则用于查找有序序列中第一个大于或等于所要查找元素的位置

那么,我们就可以得到 STL 离散化的 0.1 版本

#include<cstdio>
#include<algorithm>
using namespace std;
int a[1005],b[1005];
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		b[i]=a[i];			//复制一份原数组
	}
	sort(b+1,b+1+n);			//排序,因为 lower_bound 必须在有序数列下进行
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(b+1,b+1+n,a[i])-b;			//查找该元素第一次出现的下标,赋值给原数组
	}
	for(int i=1;i<=n;i++){
		printf("%d ",a[i]);
	}
	return 0;
}

程序结果:

在这里插入图片描述

很好,我们离想要的结果又近了一步。

观察样例,我们发现:对于数组中重复的元素,我们依旧给了它一个坑位占,所以接下来我们需要去重

#include<cstdio>
#include<algorithm>
using namespace std;
int a[1005],b[1005];
int main(){
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+1+n);
	int len=unique(b+1,b+1+n)-b-1;			//unique 是一个去重函数,同样要在排序之后才能使用
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(b+1,b+1+len,a[i])-b;			//由于去重后,数组大小减小了,需要注意
	}
	for(int i=1;i<=n;i++){
		printf("%d ",a[i]);
	}
	return 0;
}

程序结果:
在这里插入图片描述

那么,我们就完成了离散化

2.4 一维树状数组运用

Eg_4 逆序对

序列中每个数字不超过 1 0 9 10^9 109

自然,我们需要一个离散化

我们会得到一个离散化后的数组 B B B ,对于一个 B [   i   ] B[\ i\ ] B[ i ] 我们可以将 B i t [   B [   1   ]   ] Bit[\ B[\ 1\ ]\ ] Bit[ B[ 1 ] ] 及其之后有关的元素全部加一,这时,因为下标比 i i i 小的数全部塞进了树状数组了,所以, S u m (   B [   i   ]   ) Sum(\ B[\ i\ ] \ ) Sum( B[ i ] ) 表示第一号元素到第 i i i 号与第 i i i 号元素所组成的数对中非逆序对的数量

显然,因为凡是 B [   i   ] B[\ i\ ] B[ i ] 小的下标又比 i i i 小的都已经塞入了树状数组中,只需用 Sum 函数过一遍即可。

显然,有 i i i 表示第一号元素到第 i i i 号与第 i i i 号元素所组成的数对的数量,用总数对数量减去非逆序对对数的数量就有了逆序对的数量

#include<cstdio>
#include<algorithm>
using namespace std;
long long int a[500005],b[500005];
long long int Bit[500005];
long long int n,ans;
long long int Low_Bit(long long int num){
	return num&-num;
}
void Up_Date(long long int num,long long int sum){
	for(int i=num;i<=n;i+=Low_Bit(i)){
		Bit[i]+=sum;
	}
}
long long int Sum(long long int num){
	long long int ans=0;
	for(int i=num;i>=1;i-=Low_Bit(i)){
		ans+=Bit[i];
	}
	return ans;
}			//基本操作
int main(){
	scanf("%lld",&n);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
		b[i]=a[i];
	}
	sort(b+1,b+1+n);
	for(int i=1;i<=n;i++){
		a[i]=lower_bound(b+1,b+1+n,a[i])-b;
	}			//离散化,这里去重可有可无
	for(int i=1;i<=n;i++){
		Up_Date(a[i],1);
		ans+=i-Sum(a[i]);			//上文已提具体含义
	}
	printf("%lld",ans);			//As we all know:不开 long long 见祖宗
	return 0;
}

2.5 二维树状数组

二维树状数组和一维树状数组一样,同样有单点修改,区间查询区间查询,单点修改区间修改,区间查询三个基本操作

2.5.1 单点修改,区间查询

二维树状数组的单点修改,区间查询和一维树状数组的单点修改,区间查询思想一致。

如果你忘了二维数组的前缀和…

Practice [HNOI2003]激光炸弹

总而言之,我们可以简单的概括一下:

P P P 数组代表二维数组 A A A 的前缀和,则有 P [   i   ] [   j   ] = P [   i − 1   ] [   j   ] + P [   i   ] [   j − 1   ] − P [   i − 1   ] [   j − 1   ] + A [   i   ] [   j   ] P[\ i\ ][\ j\ ]=P[\ i-1\ ][\ j\ ]+P[\ i\ ][\ j-1\ ]-P[\ i-1\ ][\ j-1\ ]+A[\ i\ ][\ j\ ] P[ i ][ j ]=P[ i1 ][ j ]+P[ i ][ j1 ]P[ i1 ][ j1 ]+A[ i ][ j ]

如果要求左上角为 ( x 1 , y 1 ) (x_1,y_1) (x1,y1) ,右下角为 ( x 2 , y 2 ) (x_2,y_2) (x2,y2) 的矩阵的元素和,我们计算 P [   x 2   ] [   y 2   ] + P [   x 1 − 1   ] [   y 1 − 1   ] − P [   x 1 − 1   ] [   y 2   ] − P [   x 2   ] [   y 1 − 1   ] P[\ x_2\ ][\ y_2\ ]+P[\ x_1-1\ ][\ y_1-1\ ]-P[\ x_1-1\ ][\ y_2\ ]-P[\ x_2\ ][\ y_1-1\ ] P[ x2 ][ y2 ]+P[ x11 ][ y11 ]P[ x11 ][ y2 ]P[ x2 ][ y11 ] 即可。

那么,我们只需要将前缀和塞入树状数组里即可

Eg_4 二维树状数组 1:单点修改,区间查询

裸题一个,代码奉上:

#include<cstdio>
int Bit[5005][5005];
int n,m,x,y,z,x1,y1,x2,y2;
int Low_Bit(int num){
	return num&-num;
}
void Up_Date(int x,int y,int sum){
	for(int i=x;i<=n;i+=Low_Bit(i)){
		for(int j=y;j<=m;j+=Low_Bit(j)){
			Bit[i][j]+=sum;
		}
	}
}
int Sum(int x,int y){
	int ans=0;
	for(int i=x;i>=1;i-=Low_Bit(i)){
		for(int j=y;j>=1;j-=Low_Bit(j)){
			ans+=Bit[i][j];
		}
	}
	return ans;
}			//因为是二维树状数组,所以 Up_Date 函数和 Sum 函数要改为二重循环
int main(){
	scanf("%d%d",&n,&m);
	while(scanf("%d",&x)!=EOF){
		if(x==1){			//修改操作
			scanf("%d%d%d",&x,&y,&z);
			Up_Date(x,y,z);	
		}else{
			scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
			printf("%d\n",Sum(x2,y2)+Sum(x1-1,y1-1)-Sum(x2,y1-1)-Sum(x1-1,y2));			//上文已提,前缀和思想
		}
	}
	return 0;
}

2.5.2 区间修改,单点查询

跟一维树状数组一样,我们依旧要采用差分思想。

请看下面神奇的图片

在这里插入图片描述

设左上角的下标为 [   x 1 , y 1   ] [\ x_1,y_1\ ] [ x1,y1 ] ,右下角的下标为 [   x 2 , y 2   ] [\ x_2,y_2\ ] [ x2,y2 ]

我们要使红色区域的元素都加上 a a a ,那么,我们可以依葫芦画瓢的将红色区域的左上角 A [   x 1   ] [   y 1   ] A[\ x_1\ ][\ y_1\ ] A[ x1 ][ y1 ] 增加 a a a ,同样,为了可以抵消左上角所增加的 a a a ,我们需要在 A [   x 1   ] [   y 2 + 1   ] A[\ x_1\ ][\ y_2+1\ ] A[ x1 ][ y2+1 ] 以及 A [   x 2 + 1   ] [   y 1   ] A[\ x_2+1\ ][\ y_1\ ] A[ x2+1 ][ y1 ] 减去一个 a a a

考虑蓝色矩阵,我们多抵消了一个 a a a ,所以,我们在 A [   x 2 + 1   ] [   y 2 + 1   ] A[\ x_2+1\ ][\ y_2+1\ ] A[ x2+1 ][ y2+1 ] 再加上 a a a ,就完成了对二维数组的差分。

老觉得怪怪的,又说不出来……

那么,我们只需要往树状数组里塞入差分操作即可。

Eg_5 二维树状数组 2:区间修改,单点查询

今天的裸题真多啊

#include<cstdio>
long long int a[10005],Bit[10005][10005];
long long int Low_Bit(long long int num){
	return num&-num;
}
long long int n,m;
void Up_Date(long long int x,long long int y,long long int sum){
	for(int i=x;i<=n;i+=Low_Bit(i)){
		for(int j=y;j<=m;j+=Low_Bit(j)){
			Bit[i][j]+=sum;
		}
	}
}
long long int Sum(long long int x,long long int y){
	long long int ans=0;
	for(int i=x;i>=1;i-=Low_Bit(i)){
		for(int j=y;j>=1;j-=Low_Bit(j)){
			ans+=Bit[i][j];
		}
	}
	return ans;
}
long long int x,y,z,x1,y1,x2,y2,sum;
int main(){
	scanf("%lld%lld",&n,&m);
	while(scanf("%lld",&x)!=EOF){
		if(x==1){
			scanf("%lld%lld%lld%lld%lld",&x1,&y1,&x2,&y2,&sum);
			Up_Date(x1,y1,sum);
			Up_Date(x2+1,y2+1,sum);
			Up_Date(x1,y2+1,-sum);
			Up_Date(x2+1,y1,-sum);
		}else{
			scanf("%lld%lld",&x2,&y2);
			printf("%lld\n",Sum(x2,y2)+Sum(0,0)-Sum(x2,0)-Sum(0,y2));			//前缀和思想
		}
	}
	return 0;
}

2.5.3 区间修改,区间查询

与一维相类似,主要还是推公式

我们假设 a a a 数组为原数组, p p p 数组为差分数组

S u m (   x , y   ) = ∑ i = 1 x ∑ j = 1 y a [   i   ] [   j   ] = ∑ i = 1 x ∑ j = 1 y ∑ k = 1 i ∑ h = 1 j p [   k   ] [   h   ] = ∑ i = 1 x ∑ j = 1 y p [   i   ] [   j   ] × ( x + 1 − i ) × ( y + 1 − j ) = ( x + 1 ) × ( y + 1 ) × ∑ i = 1 x ∑ j = 1 y p [   i   ] [   j   ] + ∑ i = 1 x ∑ j = 1 y p [   i   ] [   j   ] × i × j − ( x + 1 ) × ∑ i = 1 x ∑ j = 1 y p [   i   ] [   j   ] × j − ( y + 1 ) × ∑ i = 1 x ∑ j = 1 y p [   i   ] [   j   ] × i \begin{aligned}Sum(\ x,y\ )&=\sum_{i=1}^x\sum_{j=1}^ya[\ i\ ][\ j\ ]\\&=\sum_{i=1}^x\sum_{j=1}^y\sum_{k=1}^i\sum_{h=1}^jp[\ k\ ][\ h\ ]\\&=\sum_{i=1}^x\sum_{j=1}^yp[\ i\ ][\ j\ ]\times(x+1-i)\times(y+1-j)\\&=(x+1)\times(y+1)\times\sum_{i=1}^x\sum_{j=1}^yp[\ i\ ][\ j\ ]+\sum_{i=1}^x\sum_{j=1}^yp[\ i\ ][\ j\ ]\times i\times j-(x+1)\times\sum_{i=1}^x\sum_{j=1}^yp[\ i\ ][\ j\ ]\times j-(y+1)\times\sum_{i=1}^x\sum_{j=1}^yp[\ i\ ][\ j\ ]\times i\end{aligned} Sum( x,y )=i=1xj=1ya[ i ][ j ]=i=1xj=1yk=1ih=1jp[ k ][ h ]=i=1xj=1yp[ i ][ j ]×(x+1i)×(y+1j)=(x+1)×(y+1)×i=1xj=1yp[ i ][ j ]+i=1xj=1yp[ i ][ j ]×i×j(x+1)×i=1xj=1yp[ i ][ j ]×j(y+1)×i=1xj=1yp[ i ][ j ]×i

LaTeX \LaTeX LATEX 打得我心累

那么,我们就需要 4 4 4 个树状数组,分别维护 p [   i   ] [   j   ] , p [   i   ] [   j   ] × x , p [   i   ] [   j   ] × y , p [   i   ] [   j   ] × x × y p[\ i\ ][\ j\ ],p[\ i\ ][\ j\ ]\times x,p[\ i\ ][\ j\ ]\times y,p[\ i\ ][\ j\ ]\times x\times y p[ i ][ j ],p[ i ][ j ]×x,p[ i ][ j ]×y,p[ i ][ j ]×x×y

Eg_6 二维树状数组 3:区间修改,区间查询

这应该是最后一道裸题了

#include<cstdio>
int read(){
	int a=0,b=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-'){
			b=-1;
		}
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		a=a*10+ch-'0';
		ch=getchar();
	}
	return a*b;
}			//快读
int x1,y1,x2,y2,sum;
int n,m;
long long int Bit[2100][2100],Bit1[2100][2100],Bit2[2100][2100],Bit3[2100][2100];
int Low_Bit(int num){
	return num&-num;
}
void Up_Date(int x,int y,int sum){
	for(int i=x;i<=n;i+=Low_Bit(i)){
		for(int j=y;j<=m;j+=Low_Bit(j)){
			Bit[i][j]+=sum;
			Bit1[i][j]+=sum*x;
			Bit2[i][j]+=sum*y;
			Bit3[i][j]+=sum*x*y;
		}			//上文已提,分别对4个值进行维护
	}
}
long long int Sum(int x,int y){
	long long int ans=0;
	for(int i=x;i>=1;i-=Low_Bit(i)){
		for(int j=y;j>=1;j-=Low_Bit(j)){
			ans+=Bit[i][j]*(x+1)*(y+1)+Bit3[i][j]-Bit1[i][j]*(y+1)-Bit2[i][j]*(x+1);
		}			//上文已提,套公式
	}
	return ans;
}
int main(){
	n=read(),m=read();
	while(scanf("%d",&sum)!=EOF){
		if(sum==1){
			x1=read(),y1=read(),x2=read(),y2=read(),sum=read();
			Up_Date(x1,y1,sum);
			Up_Date(x2+1,y2+1,sum);
			Up_Date(x1,y2+1,-sum);
			Up_Date(x2+1,y1,-sum);			//差分思想进行维护
		}else{
			x1=read(),y1=read(),x2=read(),y2=read();
			printf("%lld\n",Sum(x2,y2)+Sum(x1-1,y1-1)-Sum(x2,y1-1)-Sum(x1-1,y2));
			//上文已提,前缀和思想
		}
	}
	return 0;
}
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值