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 ](1⩽k<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 q−1 位就变为 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 p−1 位都未改变,所以只考虑第 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(left−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 的效果。
那么,我们只需要将差分数组的操作塞进树状数组里就可以了。
裸题梅开二度
#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=1∑ia[ x ]=x=1∑iy=1∑xP[ y ]=x=1∑ix×P[ i−x+1 ]=i×x=1∑iP[ i ]−x=1∑i(x−1)×P[ x ]
观察减式两边,我们发现:左边式子的基本结构为 P [ i ] P[\ i\ ] P[ i ] ,而右边式子的基本结构为 i × P [ i + 1 ] i\times P[\ i+1\ ] i×P[ i+1 ] 。那么,我们就可以分别用两个树状数组进行维护。
在修改时,分别对两个树状数组进行更改,查询时,通过前缀和,套用上面的公式即可。
又双叒叕是道裸题
#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 一维树状数组运用
序列中每个数字不超过 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 单点修改,区间查询
二维树状数组的单点修改,区间查询和一维树状数组的单点修改,区间查询思想一致。
如果你忘了二维数组的前缀和…
总而言之,我们可以简单的概括一下:
若 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[ i−1 ][ j ]+P[ i ][ j−1 ]−P[ i−1 ][ j−1 ]+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[ x1−1 ][ y1−1 ]−P[ x1−1 ][ y2 ]−P[ x2 ][ y1−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 ,就完成了对二维数组的差分。
老觉得怪怪的,又说不出来……
那么,我们只需要往树状数组里塞入差分操作即可。
今天的裸题真多啊
#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=1∑xj=1∑ya[ i ][ j ]=i=1∑xj=1∑yk=1∑ih=1∑jp[ k ][ h ]=i=1∑xj=1∑yp[ i ][ j ]×(x+1−i)×(y+1−j)=(x+1)×(y+1)×i=1∑xj=1∑yp[ i ][ j ]+i=1∑xj=1∑yp[ i ][ j ]×i×j−(x+1)×i=1∑xj=1∑yp[ i ][ j ]×j−(y+1)×i=1∑xj=1∑yp[ 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
这应该是最后一道裸题了
#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;
}