树状数组学习笔记
树状数组作为一个常数小且好写的数据结构,虽然功能没有线段树那么齐全,但是其中的扩展内容还是很多的。
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;
}

浙公网安备 33010602011771号