树状数组详解
树状数组
以下有错误的话欢迎指正
由于篇幅问题每道题目的代码在每一板块最后折叠给出
其实线段树能维护的东西比树状数组能维护的东西多得多,但是树状数组代码好写啊!
一维树状数组#
最为常用的树状数组,我们一般都是用这个来解决问题,二维的后面会讲。
引入#
我们在进行数列操作的时候,经常会遇到带有区间修改的一类问题,比如给定一个数列,支持区间加单点查询,或者支持区间查询单点修改之类的。
给定一个序列,需要支持单点修改和区间加,设元素个数为
介绍#
首先要学会树状数组,我们就要先理解一个函数:lowbit
函数。
lowbit
函数的返回值就是由当前数转为二进制后最低位的 lowbit
值就是
我们如何能求出这个 lowbit
函数的值呢?
-
暴力,每次模
,但显然复杂度为 ,由于有更快速便捷的方法,我们不能接受多一只 的复杂度。 -
我们考虑到是在二进制下,那我们就可以用快速的位运算来解决这个问题,我们先把原数如
进行取反,得到: ,这个时候,我们再给取反后的数字加上 ,这个时候,你会发现,得到的数为 ,这个数值和一开始的数的关系,就是除了最后一位 和后面的 ,其他位都是不同的!所以我们将这两个一&
就得到了我们的lowbit
,也就是 。 -
我们想到计算机中对于负数的存储用的是补码,也就是如果一个数是正数,他的补码就是他本身,如果是负数,那么他的补码就是他的反码然后加一。根据这个特性,我们知道
的反码为 那么补码就是 ,这样再和&
一下不就也得到了lowbit
吗,也就是 。
于是我们转化成代码就是:
inline int lowbit(int x){return x&-x;}
这个有什么用,我们后面讲到操作就知道了。
树状数组是长这个样子的:
感谢树状数组详解 - Xenny - 博客园的图片。
其中黑色的块是原来的数组下面会用
树状数组的每一个块,存放的是子节点的和。
用十进制可以不是很好结合前面的 lowbit
,所以下面的下标我用的是二进制。
我们可以形象的看作每个节点
单点修改区间查询#
也就是这道题目:【模板】树状数组 1 - 洛谷
我们现在来考虑如何单点修改,假设我们要修改里面的
我们发现,由于有一些节点是包含
不知道你发现了什么没有。
可以看出,下一个修改的节点的编号,就是当前的数 lowbit
值的和!
所以我们对于一次修改操作,可以写出以下的代码:
inline void add(int x,int k){while(x<=n)t[x]+=k,x+=lowbit(x);}
好神奇!
然后我们再来看区间求和的操作。
我们的树状数组维护的就是前缀和,所以我们在查询的时候肯定不能一个一个遍历,举个例子,假设我们需要查询前
我们发现我们实际上需要查找的有
我们再转成二进制看看:
这次的很明显了吧!
我们发现,下一个需要修改的节点,就是当前
inline int ask(int x){int o=0;while(x>=1)o+=t[x],x-=lb(x);return o;}
对于区间查询,我们利用前缀和的思想,设需要查询的区间为
至此我们就完成了支持区间查询和单点修改的操作。
区间查询和单点修改的复杂度最坏均为
完整代码:
树状数组单点修改区间查询
#include<bits/stdc++.h>
#define int long long
#define N 1001000
using namespace std;
int n,m,t[N];
inline int lb(int x){return x&-x;}
inline void add(int x,int k){while(x<=n)t[x]+=k,x+=lb(x);}
inline int sum(int x){int ans=0;while(x>=1)ans+=t[x],x-=lb(x);return ans;}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++){int a;cin>>a;add(i,a);}
for(int i=1;i<=m;i++)
{
int op,x,y;
cin>>op>>x>>y;
if(op==1)add(x,y);
if(op==2)cout<<(sum(y)-sum(x-1))<<endl;
}
return 0;
}
区间修改单点查询#
我们想一想,什么东西做区间修改最快?
那当然是差分!直接
那我们用树状数组维护这个差分数组不就好了?
我们单点查询变成了之前的查询前
完整 code:
树状数组区间修改单点查询
#include<bits/stdc++.h>
#define int long long
#define N 1000100
using namespace std;
int n,m,a[N],t[N];
inline int lb(int x){return x&-x;}
inline void add(int x,int k){while(x<=n)t[x]+=k,x+=lb(x);}
inline int ask(int x){int ans=0;while(x>=1)ans+=t[x],x-=lb(x);return ans;}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=m;i++)
{
int op,x,y,k;
cin>>op;
if(op==1)
{
cin>>x>>y>>k;
add(x,k);
add(y+1,-k);
}
if(op==2)
{
cin>>x;
int ans=ask(x)+a[x];
cout<<ans<<endl;
}
}
return 0;
}
区间查询区间修改#
线段树的模板的操作就是区间查询和区间修改,但线段树码量比较大,所以我们还可以用树状数组来解决。
我们如果想要完成这个操作的话,还是需要用到上面的差分的思想。
首先我们和上面一样,用差分的话,我们就可以做到
我们假设要查询前
图片来源:# 完全理解并深入应用树状数组 | 支持多种动态维护区间操作
图中的蓝色阴影部分就是我们要求的值,用
然后在把
我们发现这个式子一点也不好算,所以我们转换一下思路:
图片来源:# 完全理解并深入应用树状数组 | 支持多种动态维护区间操作
补全之后发现,这个总的面积,就是
那我们看看黄色面积怎么求:
这个东西好像比上面的好算一点(?
用前缀和维护一下不就更好了!
我们记
我们最后用两个树状数组,来维护了两个东西达到了区间修改区间查询的目的。
对于我们的修改操作的代码,我们改成这样:
inline void add(int x,int k){int xx=x;while(x<=n)t1[x]+=k,t2[x]+=(k*xx),x+=lb(x);}
我们很容易理解,就是多了个 t2[x]+=(k*xx)
,因为我们说了是维护的前
再来看我们的查询操作:
inline int ask(int x){int ans=0,xx=x;while(x>=1)ans+=(xx+1)*t1[x]-t2[x],x-=lb(x);return ans;}
里面对于
值得注意的是,
完整代码:
树状数组区间修改区间求和
#include<bits/stdc++.h>
#define int long long
#define N 1000100
using namespace std;
int n,m,a[N],t1[N],t2[N];
inline int lb(int x){return x&-x;}
inline void add(int x,int k){int xx=x;while(x<=n)t1[x]+=k,t2[x]+=(k*xx),x+=lb(x);}
inline int ask(int x){int ans=0,xx=x;while(x>=1)ans+=(xx+1)*t1[x]-t2[x],x-=lb(x);return ans;}
signed main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)add(i,a[i]-a[i-1]);
for(int i=1;i<=m;i++)
{
int op,l,r,x;
cin>>op;
if(op==1)
{
cin>>l>>r>>x;
add(l,x);
add(r+1,-x);
}
if(op==2)
{
cin>>l>>r;
cout<<(ask(r)-ask(l-1))<<endl;
}
}
return 0;
}
二维树状数组#
树状数组比线段树更容易扩展到二维,所以我们就来研究如何把它扩到二维。
我们在之前就已经说过,一维的树状数组可以看作是一个点以自身为起点向左 lowbit
个单位的总和,那我们的二维的树状数组的一点
答案是 true。
单点修改区间查询#
我们对于这个操作,需要掌握的前置知识就是二维的前缀和。
我们可以看到利用二维前缀和的思想,假设我们要求横坐标为
因为我们维护的相当于二维前缀和,那么我们就可以根据这个同样去求解我们的二维树状数组的区间查询。
贴一下完整 code:
二维树状数组单点修改区间查询
#include<bits/stdc++.h>
#define int long long
#define N 4100
using namespace std;
int n,m,t[N][N];
inline int lb(int x){return x&-x;}
inline void add(int x,int y,int k)
{
while(x<=n)
{
int j=y;
while(j<=m)
{
t[x][j]+=k;
j+=lb(j);
}
x+=lb(x);
}
}
inline int ask(int x,int y)
{
int res=0;
while(x>=1)
{
int j=y;
while(j>=1)
{
res+=t[x][j];
j-=lb(j);
}
x-=lb(x);
}
return res;
}
signed main()
{
cin>>n>>m;
int op,a,b,c,d;
while(cin>>op)
{
if(op==1)
{
cin>>a>>b>>c;
add(a,b,c);
}
if(op==2)
{
int ans=0;
cin>>a>>b>>c>>d;
ans=ask(c,d)+ask(a-1,b-1)-ask(c,b-1)-ask(a-1,d);
cout<<ans<<endl;
}
}
return 0;
}
我们在修改的时候,就像之前的一样进行修改即可,只不过是多了一个
在区间求和的时候,我们就可以直接跟上面二维前缀和一样直接求出来了。
单次修改或查询近似为
单点查询区间修改#
我们上面用了一维树状数组的单点修改区间查询的前缀和思想,那么我们是不是也可以用一维树状数组的区间修改单点查询的差分思想来转到二维上来呢?
答案为 true。
我们还是看上面那张图:
我们可以推出差分数组中一个点的值为:
其实就是根据原数组是差分数组的前缀和数组来推的。
那我们就可以和之前的差分一样,对于单点查询我们就直接求前缀和,也就是这样写:
inline int ask(int x,int y)
{
int res=0;
while(x>=1)
{
int j=y;
while(j>=1)
{
res+=t[x][j];
j-=lb(j);
}
x-=lb(x);
}
return res;
}
对的就是和上面的一摸一样,调用的时候就直接 ask(x,y)
即可查询当前下标为
对于修改操作,我们从前面的前缀和公式也能得到一些灵感,我们这样写:
add(a,b,k);
add(a,d+1,-k);
add(c+1,b,-k);
add(c+1,d+1,k);
同理 add
函数的代码和上面的是一样的。
我们来看这四个操作,有没有很像是前缀和的四个矩形,只不过是换成了差分的
我们还是根据上面差分数组的公式来进行修改,我们就会发现实际上就是给
每一次操作的复杂度匀以下约为
完整代码:
二维树状数组区间修改单点查询
#include<bits/stdc++.h>
#define int long long
#define N 4100
using namespace std;
int n,m,t[N][N];
inline int lb(int x){return x&-x;}
inline void add(int x,int y,int k)
{
while(x<=n)
{
int j=y;
while(j<=m)
{
t[x][j]+=k;
j+=lb(j);
}
x+=lb(x);
}
}
inline int ask(int x,int y)
{
int res=0;
while(x>=1)
{
int j=y;
while(j>=1)
{
res+=t[x][j];
j-=lb(j);
}
x-=lb(x);
}
return res;
}
signed main()
{
cin>>n>>m;
int op,a,c,b,d,k;
while(cin>>op)
{
if(op==1)
{
cin>>a>>b>>c>>d>>k;
add(a,b,k);
add(a,d+1,-k);
add(c+1,b,-k);
add(c+1,d+1,k);
}
if(op==2)
{
cin>>a>>b;
cout<<ask(a,b)<<endl;
}
}
return 0;
}
区间查询区间修改#
我们根据上面的前缀和的定义,我们不难发现对于一个二维差分数组,一个点
只是看这四重循坏就知道暴力肯定是不行的,那我们怎么优化呢?
我们借鉴一下一维的区间查询区间修改的做法,对于上面的式子,我们把后两层枚举拆开看看。
我们可以发现
我们把后面的给展开一下:
最后我们稍微合并一下得到:
我们发现里面只需要维护四个东西就可以完成我们的查询操作了:
我们来看一下修改的代码:
inline void add(int x,int y,int k)
{
for(int i=x;i<=n;i+=lb(i))
for(int j=y;j<=m;j+=lb(j))
t[i][j]+=k,ti[i][j]+=k*x,tj[i][j]+=k*y,tij[i][j]+=k*x*y;
}
我们在进行修改的时候就要对应我们每一个数组维护的值来修改,修改的时候加上的值都要乘以对应的系数,而在主函数中,我们是跟差分的一样来写四次 add
操作。
而我们的求和操作需要这样写:
inline int ask(int x,int y)
{
int res=0;
for(int i=x;i>=1;i-=lb(i))
for(int j=y;j>=1;j-=lb(j))
res+=t[i][j]*(x*y+x+y+1)-ti[i][j]*(y+1)-tj[i][j]*(x+1)+tij[i][j];
return res;
}
这里乘起来的系数就是上面的公式推导的,对于我们已经维护好的我们就直接调用就好。
完整 code:
二维树状数组区间修改区间求和
#include<bits/stdc++.h>
//#define int long long
#define endl '\n'
#define N 2100
using namespace std;
int n,m,t[N][N],ti[N][N],tj[N][N],tij[N][N];
inline int lb(int x){return x&-x;}
inline int read(){int x=0,f=1;char ch=getchar();while(!isdigit(ch)){f=ch!='-';ch=getchar();}while(isdigit(ch)){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}return f?x:-x;}
inline void print(int x){if(x>=10)print(x/10);putchar(x%10+48);}
inline void add(int x,int y,int k)
{
for(int i=x;i<=n;i+=lb(i))
for(int j=y;j<=m;j+=lb(j))
t[i][j]+=k,ti[i][j]+=k*x,tj[i][j]+=k*y,tij[i][j]+=k*x*y;
}
inline int ask(int x,int y)
{
int res=0;
for(int i=x;i>=1;i-=lb(i))
for(int j=y;j>=1;j-=lb(j))
res+=t[i][j]*(x*y+x+y+1)-ti[i][j]*(y+1)-tj[i][j]*(x+1)+tij[i][j];
return res;
}
signed main()
{
char op;
int a,b,c,d,k;
cin>>op,n=read(),m=read();
while(cin>>op)
{
if(op=='L')
{
a=read(),b=read(),c=read(),d=read(),k=read();
add(a,b,k);
add(a,d+1,-k);
add(c+1,b,-k);
add(c+1,d+1,k);
}
if(op=='k')
{
a=read(),b=read(),c=read(),d=read();
int ans=ask(c,d)+ask(a-1,b-1);
ans-=ask(a-1,d)+ask(c,b-1);
cout<<ans<<endl;
}
}
return 0;
}
写在最后#
以前稀里糊涂的学了一遍树状数组,现在来算是重修了一下。
突然发现发明这个东西的人好厉害,尤其是对于 lowbit
的使用,很奇妙。
有没看明白的可以评论。
作者: 北烛青澜
出处:https://www.cnblogs.com/Multitree/p/17453019.html
本站使用「CC BY 4.0」创作共享协议,转载请在文章明显位置注明作者及出处。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!