树状数组学习笔记

树状数组作为一个常数小且好写的数据结构,虽然功能没有线段树那么齐全,但是其中的扩展内容还是很多的。

1.维护区间和

树状数组可以做到单次 logn 求前缀和,单次 logn 修改信息维护一个前缀和。

1.1 区间修改 单点查询

考虑维护差分数组 \(c[i]=a[i]-a[i-1]\)

在查询的部分,一个点的值 \(a[i]\) 将等于 \(\sum\limits_{j=1}^i{c[j]}\) 的值,也就是求前缀和。

修改的话,由于我们要维护的是前缀和,对于一个差分数组来说,显然等价于给 \(l\) 上的点加上 \(v\) , 给 \(r+1\) 上的点加上 \(-v\)

1.2 区间修改 区间查询

这个部分需要使用两个 BIT 去维护了,设 \(r\) 表示右端点。

首先,由于加法满足结合率,所以可以将区间转化为两端点前缀和之差。

单点的值仍然满足 \(a[i]=\sum\limits_{j=1}^i{c[j]}\) ,因为要求 \(\sum{a[i]}\) ,所以原式子可以写为 \(\sum\limits_{i=1}^r\sum\limits_{j=1}^i{c[j]}\)

观察这个式子,不难发现每个 \(c[j]\) 出现了 \(r-j+1\) 次,则原式子又可以写成 \(\sum\limits_{i=1}^rc[i]\times(r+1)-\sum\limits_{i=1}^rc[i]\times i\)

因此我们需要开两个 BIT 去单独维护 \(c[i]\)\(c[i]\times i\)

第一个好维护,第二个就将 \(l\) 上的点加上 \(v\times l\) , 给 \(r+1\) 上的点加上 \(-v\times(r+1)\)

【例题1】 线段树1【模板】 :

点击查看代码
#include<bits/stdc++.h> #define int long long #define lowbit(i) i&-i using namespace std; const int N=1e5+5; inline int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9') f=(ch!='-'),ch=getchar(); while(ch<='9'&&ch>='0') x=(x*10)+(ch^48),ch=getchar(); return f?x:-x; } int t1[N],t2[N]; int n,m,op,l,r,k,a[N]; void add(int p,int v) { for(int i=p;i<=n;i+=lowbit(i)) t1[i]+=v,t2[i]+=v*p; } int ask(int p) { int ans=0; for(int i=p;i;i-=lowbit(i)) ans+=t1[i]*(p+1)-t2[i]; return ans; } signed main() { n=read(),m=read(); for(int i=1;i<=n;++i) a[i]=read(),add(i,a[i]-a[i-1]); while(m--) { op=read(),l=read(),r=read(); if(op==1) { k=read(); add(l,k),add(r+1,-k); } if(op==2) printf("%lld\n",ask(r)-ask(l-1)); } return 0; }

1.3 二维树状数组

既然是二维,那么就有一点类似于树套树,但是常数非常小,空间为 \(n^2\)

对于最简单的单点修改,矩阵查询,我们只需要无脑套一层 y 轴方向的循环就好了。

【例题2】 上帝造题的七分钟:

点击查看代码
#include<bits/stdc++.h> #define lowbit(i) i&-i #define rint register int using namespace std; const int N=2e3+55; int tr1[N][N],tr2[N][N],tr3[N][N],tr4[N][N]; inline int read() { rint x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9') f=(ch!='-'),ch=getchar(); while(ch>='0'&&ch<='9') x=(x*10)+(ch^48),ch=getchar(); return f?x:-x; } char ch[3]; int n,m,a,b,c,d,k; inline void add(int x,int y,int o) { for(rint i=x;i<=n;i+=lowbit(i)) for(rint j=y;j<=m;j+=lowbit(j)) tr1[i][j]+=o,tr2[i][j]+=x*o, tr3[i][j]+=y*o,tr4[i][j]+=x*y*o; } inline int sum(int x,int y) { rint ans=0; for(rint i=x;i;i-=lowbit(i)) for(rint j=y;j;j-=lowbit(j)) ans+=(x+1)*(y+1)*tr1[i][j]-(y+1)*tr2[i][j]-(x+1)*tr3[i][j]+tr4[i][j]; return ans; } int main() { n=read(),m=read(); while(~scanf("%s",ch)) { a=read(),b=read(), c=read(),d=read(); if(!(ch[0]^76)) { k=read(); add(a,b,k); add(c+1,d+1,k); add(a,d+1,-k); add(c+1,b,-k); } else printf("%d\n",sum(c,d)+sum(a-1,b-1)-sum(a-1,d)-sum(c,b-1)); } return 0; }

2. 权值树状数组

所谓权值数组,就是一个桶,\(tr[i]\) 表示的是权值为 \(i\) 的数的个数。

2.1 静态逆序对

静态逆序对问题,暴力做法是对于每一个 \(a_i\) ,找到 \(a_j>a_i\)\(j<i\) ,设这个值为 \(w_i\)

但是显然这样的复杂度是 \(O(n^2)\) 的,考虑优化。

考虑加速求 \(w_i\) 的过程。由于是顺着遍历的,所以每求出一个 \(w_i\) ,便可以将这个数丢入桶中,然后再快速地判断原有桶中有多少个数大于 \(a_i\) ,而这个过程,树状数组就可以帮助求和。

如果数字的值非常大或者是小数、负数的话,我们需要离散化处理(或者使用动态开点线段树,总的时间复杂度仍为 \(O(n\log n)\)

【例题3】 逆序对

点击查看代码
#include<bits/stdc++.h> #define int long long #define lowbit(i) i&-i #define h(x) lower_bound(b+1,b+m+1,x)-b using namespace std; const int N=5e5+5; inline int read() { int x=0;char ch=getchar(); while(ch<'0'||ch>'9')ch=getchar(); while(ch<='9'&&ch>='0') x=(x*10)+(ch^48),ch=getchar(); return x; } int n,m,ans,a[N],b[N],tr[N]; inline void add(int x) { for(int i=x;i<=m;i+=lowbit(i)) ++tr[i]; return; } inline int ask(int x) { int ans=0; for(int i=x;i;i-=lowbit(i)) ans+=tr[i]; return ans; } signed main() { n=read(); for(int i=1;i<=n;++i) a[i]=b[i]=read(); sort(b+1,b+n+1); m=unique(b+1,b+n+1)-b-1; for(int i=1;i<=n;++i) { int x=h(a[i]); add(x);ans+=i-ask(x); } printf("%lld",ans); return 0; }

2.2 二维数点

二维数点,顾名思义,给出一个平面内的点集,每次询问一个矩阵内点的数量。

首先,我们先使用容斥,将 \((x1,y1,x2,y2)\) 的询问拆成 \((0,0,x2,y2)+(0,0,x1,y1)-(0,0,x1,y2)-(0,0,x2,y1)\) ,这样就可以把问题转换成点与坐标轴围成区域中所包含的点数量。

考虑对于新询问点的横坐标从小到大排序(点集中的点一样),每次只要是点集中的点横坐标小于询问的横坐标,我们便将这个点的纵坐标加入桶中。处理询问时,我们直接找出桶中小于询问点纵坐标的点的数量就好了,这个过程使用树状数组解决。

二维数点还有很多拓展,当题目要求关键字别分满足某些条件的二元组时,都可以转化为二维数点问题去解决。

【例题4】 老C的任务

点击查看代码
#include<bits/stdc++.h> #define int long long #define rint register int using namespace std; const int N=1e5+5; int ans[N],Y[N]; int n,m,a,b,c,d,cnt,k; struct Q{ int x; int y; int id; int op; }q[N<<2]; inline bool cmp1(Q a,Q b){return a.x<b.x;} struct D{ int x,y,p; }L[N]; inline bool cmp2(D a,D b){return a.x<b.x;} struct Tree{ int t[N]; #define lowbit(i) i&-i inline void add(int pos,int j) { for(rint i=pos;i<N;i+=lowbit(i)) t[i]+=j; return; } inline int ask(int pos) { rint res=0; for(rint i=pos;i;i-=lowbit(i)) res+=t[i]; return res; } }T; inline int read() { rint x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9') f=(ch!='-'),ch=getchar(); while(ch<='9'&&ch>='0') x=(x*10)+(ch^48),ch=getchar(); return f?x:-x; } inline int get(int y) { rint g=lower_bound(Y+1,Y+cnt+1,y)-Y; return (Y[g]==y)?g:g-1; } signed main() { n=read(),m=read(); for(rint i(1);i<=n;++i) L[i].x=read(),Y[i]=L[i].y=read(),L[i].p=read(); sort(L+1,L+n+1,cmp2); sort(Y+1,Y+n+1),cnt=unique(Y+1,Y+n+1)-Y-1; for(rint i(1);i<=m;++i) { a=read(),b=read(),c=read(),d=read(); q[i*4-3]=(Q){c,d,i,1}; q[i*4-2]=(Q){c,b-1,i,-1}; q[i*4-1]=(Q){a-1,d,i,-1}; q[i*4]=(Q){a-1,b-1,i,1}; } m<<=2,k=1; sort(q+1,q+m+1,cmp1); for(rint i(1);i<=m;++i) { while((L[k].x<=q[i].x)&&(k<=n)) T.add(get(L[k].y),L[k].p),++k; ans[q[i].id]+=q[i].op*T.ask(get(q[i].y)); } m>>=2; for(rint i(1);i<=m;++i) printf("%lld\n",ans[i]); return 0; }

2.3 求解全局第 k 小

由于树状数组特殊的性质,\(tr_i\) 管辖的范围刚好是 \((i-lowbit(i),i]\) 恰好可以减少倍增时候的多余计算。

那么,如果我们要找到一个集合中的第 k 小的数字 \(x\) ,转换为树状数组的语言便是 \(\sum\limits_{i=1}^xtr_i=k\) 。如果每次暴力二分,再用 logn 的复杂度求和,那么时间复杂度将会是 \(O( \log^2 n)\) ,但是如果结合倍增思想,我们就可以省去一些不必要的计算,从而达到 \(O( \log n)\) 的优秀复杂度。

求解 kth 问题的模板
int kth(int k) { int sum=0,x=0; for(int i=log2(值域);~i;--i) { x+=1<<i; if(x>=m||sum+tr[x]>=k) x-=1<<i; else sum+=tr[x]; } return x+1; }

__EOF__

本文作者fzeのblog
本文链接https://www.cnblogs.com/fzefze/p/17615051.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   fze  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示