树状数组学习笔记

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

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;
}
posted @ 2023-08-08 17:55  fze  阅读(52)  评论(0)    收藏  举报