详解树状数组(各种花活)

之前一直以为树状数组十分局限,最多就是“单点查询+区间修改”或者是“单点加+区间查询”,没想到可以“区间加+区间修改”,快记一下awa。

树状数组

树状数组的应用

  1. 树状数组模板(单点+区间)
  2. 树状数组 pro(区间+区间)
  3. 树状数组 pro max(单点+高维区间)
  4. 树状数组 pro max plus(高维区间+高维区间)
  5. 树状数组上二分

树状数组模板(单点+区间)

原理:

树状数组最基础的应用,网上有很多讲解的。大致原理就是对于树状数组上的某一节点 $ i $ ,它记录的信息就是 $ [ i - 2^{lowbit(i)} + 1 \text{,}i ] $ 的和。

比如说 $ 6 $ ,它的二进制就是 $ 110 $,就意味着 $ 6 $ 号节点的信息就是$ [ 6 - 2^{lowbit(6)} + 1 \text{,}6 ] = [ 6 - 2 + 1 \text{,}6 ] = [ 5 \text{,} 6 ] $ 的和。

\(lowbit(x)\) 是求 \(x\) 的最低的,二进制位为1的数,原理就不细讲了,你只要知道由于计算机存储负数的底层原理, \(lowbit(x)=(x \& (-x))\)

放个图以供理解:

那么我们就可以通过以下(见Code)的方式来做到 \(O(\log n)\) 的修改和查询。

然后现在我们可以做到单点修改,区间查询(查询 \([1,k]\) 的区间和)

同时对于区间修改,单点查询的话可以利用差分,转化成单点修改,区间查询。

注意,我们只能实现查询 \([1,k]\) 的区间和,所以在查询 \([l,r]\) 的时候可以 \(sum(r)-sum(l-1)\)

Code:

#define lowbit(x) (x&(-x))

int t[1000005];
inline void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
inline int sum(int x)
{
	int rt=0;
	while(x)
	{
		rt+=t[x];
		x-=lowbit(x);
	}
	return rt;
}

相应题目:

【模板】树状数组 1

【模板】树状数组 2


树状数组 pro(区间+区间)

好了,开始玩花活

我们已经知道树状数组可以“单点+区间”,那么树状数组怎么“区间+区间”呢?

原理:

首先,对于区间修改,我们可以套路化的将其用差分转化为单点修改。

即:\([l,r]\) 区间加 \(x\),就可以转换成在差分数组(设为 \(t\) )上,\(t_l+=x\)\(t_{r+1}-=x\)

这样,对于数组 \(i\) 这个位置上的数字的真实值就应该是 \(a_i=\sum_{j=1}^i t_j\)

其次,对于区间求和,即对于区间\([l,r]\) 的和,我们可以转化成 \(sum(r)-sum(l-1)\),这样我们就将问题转化成了求前缀和 \(ans=\sum_{i=1}^k a_j\) 即可。

因为 \(ans=\sum_{i=1}^k a_j\),又 \(a_i=\sum_{j=1}^i t_j\)

所以:

\[ans=\sum_{i=1}^k \sum_{j=1}^i t_j \]

可以发现(实在不会拆就用手模几组小的):

\[ans=k\times t_1 +(k-1)\times a_2 + \dots +1\times t_k \]

但是上面这个和 \(k\) 的关联性很强,无法快速的处理,那我们再转化:

\[ans=k\times \sum_{i=1}^k t_i - \sum_{i=1}^k (t_i\times(i-1)) \]

转化成这样,我们就可以求解了。

我们建两棵树状数组,支持单点加,区间求和。

一棵维护 \(\sum_{i=1}^k t_i\),另一棵维护 \(\sum_{i=1}^k (t_i\times(i-1))\)

Code:

struct node{
	ll t[100005];
	void jia(int x,ll k){while(x<=n)t[x]+=k,x+=lowbit(x);}
	ll SUM(int x){ll rt=0;while(x)rt+=t[x],x^=lowbit(x);return rt;}
}t1,t2;	//t1维护区间和	t2维护 *(i-1) 之后的区间和
inline void add(int l,int r,ll k)
{
	t1.jia(l,k);	t1.jia(r+1,-k);
	t2.jia(l,k*(l-1));	t2.jia(r+1,-k*r);
}
inline ll asksum(int k){return t1.SUM(k)*k-t2.SUM(k);}//求[1,k]的区间和

相关题目:

(虽然说是叫线段树,但是可以用升级版的树状数组解)

P3372 【模板】线段树 1


树状数组 pro max(单点+高维区间)

普通树状数组的拓展

原理:

和普通的树状数组一样,就是多了几维,直接看代码就好了。

同样可以通过多维前缀和将“单点修改+高维区间和”和“单点查询+高维区间修改”相互转化。

我这里用二维来举例子。

Code:

inline void add(int x,int y,int k)
{
	for(int i=x;i<=n;i+=lowbit(i))
		for(int j=y;j<=m;j+=lowbit(j))
			t[i][j]+=k;
}
inline int asksum(int x,int y)
{
	int rt=0;
	for(int i=x;i;i-=lowbit(i))
		for(int j=y;j;j-=lowbit(j))
			rt+=t[i][j];
	return rt;
}

相关题目:

暂时没有,咕咕咕


树状数组 pro max plus(高维区间+高维区间)

(花活的极致升华(bushi)

这就已经是通法了,其实就是前几种花活综合起来的灵活运用。

我这里还是用二维来举例子。

原理:

首先,还是老套路,老老实实区间加 \(k\) 显然不行,要通过二位前缀和来转化。\((x_1,y_1,x_2,y_2)\) 区间加 \(k\) 就转化成 \(t_{x_1,y_1}+=k\ \ ,\ \ t_{x_1,y_2+1}-=k\ \ ,\ \ t_{x_2+1,y_1}-=k\ \ ,\ \ t_{x_2+1,y_2+1}+=k\)

则,对于某个点,它真实的值就是 \(a_{x,y}=\sum_{i=1}^{x}\sum_{j=1}^{y}t_{i,j}\)

同时对于区间求和,我们也可以套路的转化成 \(ans=sum_{x_2,y_2}-sum_{x_2,y_1-1}-sum_{x_1-1,y_2}+sum_{x_1-1,y_1-1}\)\(sum_{i,j}\) 表示 \((i,j)\) 的二位前缀和)。

所以我们只需要求 \((x,y)\) 的二位前缀和 \(sum_{x,y}\) 就行了。

并且综上:

\[sum_{x,y}=\sum_{i=1}^{x}\sum_{j=1}^{y}\sum_{h=1}^{i}\sum_{k=1}^{j}t_{h,k} \]

而拆开后就是:

\[sum_{x,y}=\sum_{i=1}^{x}\sum_{j=1}^{y}t_{i,j}\times (x-i+1)\times (y-j+1) \]

再展开就变成:

\[sum_{x,y}=\sum_{i=1}^{x}\sum_{j=1}^{y}t_{i,j}\times (xy-xj-yj+x+y+ij-i-j+1) \]

然后,我们设:

\[G=\sum_{i=1}^{x}\sum_{j=1}^{y}t_{i,j}\\ G_i=\sum_{i=1}^{x}\sum_{j=1}^{y}t_{i,j}\times i\\ G_j=\sum_{i=1}^{x}\sum_{j=1}^{y}t_{i,j}\times j\\ G_{ij}=\sum_{i=1}^{x}\sum_{j=1}^{y}t_{i,j}\times i\times j\\ \]

那么:

\[sum_{x,y}=(xy+x+y+1)\times G-(x+1)\times G_j-(y+1)\times G_i+G_{ij} \]

所以我们要维护4棵树状数组,分别维护 \(G,G_i,G_j,G_{ij}\) 就可以求解了。

Code:

int n,m;
struct node{
	int t[2100][2100];
	void add(int x,int y,int k)
	{
		for(int i=x;i<=n;i+=lowbit(i))
			for(int j=y;j<=m;j+=lowbit(j))
				t[i][j]+=k;
	}
	int asksum(int x,int y)
	{
		int rt=0;
		for(int i=x;i;i-=lowbit(i))
			for(int j=y;j;j-=lowbit(j))
				rt+=t[i][j];
		return rt;
	}
}t,ti,tj,tij;
inline void jia(int x,int y,int k){
	t.add(x,y,k);		ti.add(x,y,k*x);
	tj.add(x,y,k*y);	tij.add(x,y,k*x*y);
}
inline int qwq(int x,int y)
{
	return (t.asksum(x,y)*(x*y+x+y+1)
			-tj.asksum(x,y)*(x+1)
			-ti.asksum(x,y)*(y+1)
			+tij.asksum(x,y));
}
inline void ADD(int x,int y,int xx,int yy,int k){
	jia(x,y,k);			jia(x,yy+1,-k);
	jia(xx+1,y,-k);		jia(xx+1,yy+1,k);
}
inline int SUM(int x,int y,int xx,int yy){
	return qwq(xx,yy)-qwq(xx,y-1)-qwq(x-1,yy)+qwq(x-1,y-1);
}
…………
………………
main()
{
	…………………
	ADD(x,y,xx,yy,k);
	SUM(x,y,xx,yy);
}

相关题目:

这道题目你要是非要用别的方法也不是不彳亍,就是麻烦的要死。

P4514 上帝造题的七分钟


树状数组上二分

首先,先明确它可以解决什么问题:

$ n $ 个数,要求支持:

  1. 单点修改。
  2. 给定 $ t $ ,求一个最大的下标 $ m $ ,使得 $ 1-m $ 的前缀和 $ <= t$。

或者:

  1. 单点修改或插入新的数。
  2. 全局第k大(小)

其实就是可以快速的解决动态区间的可以用普通二分解决的问题。

它的原理你可以类比着倍增来理解,就是对于树状数组上的某一节点 $ i $ ,它记录的信息就是 $ [ i - 2^{lowbit(i)} + 1 \text{,}i ] $ 的和,有点类似于倍增,所以对于每一位来决定是否为 \(1\)\(0\)

比如说 $ 6 $ ,它的二进制就是 \(110\),就意味着 $ 6 $ 号节点的信息就是$ [ 6 - 2^{lowbit(6)} + 1 \text{,}6 ] = [ 6 - 2 + 1 \text{,}6 ] = [ 5 \text{,} 6 ] $ 的和。

可以参考下图:

那么就可以从高到低,决定每一个二进制位,然后就好了。

或者可以换一个说法:

就是对于一个二进制数,比如说01101100,在我们决定某第 \(i\) 个二进制位的时候,可以想象成是是否要选取已经选到的位置 \(now\)\(now+2^{i}\) 这一段数。

那么,我们在枚举时,先是第 \(7\) 位,我们选定为 \(1\),就意味着我们向后跳了 $ 2^{7} $ 步,然后又选了第 $ 5 $ 位,就相当于我们又向后跳了 \(2^{5}\) 步,就是相当于又选了 \(2^{7}+1\)\(2^{7}+2^{5}\) 之间的数……然后以此类推……(很像倍增,对吧awa)

(可以自己对着图意会一下awa)

不懂得可以理解一下代码(两种写法):

Code:

int n,_log;
//全局第k小 
//_log=log2(n);
int erfen1(int k)//正着,有点类似于倍增,更好理解; 
{
	int ans=0; 
	for(int i=_log;i>=0;i--)
		if(t[ans+(1<<i)]<=k)
			ans+=(1<<i),k-=t[ans+(1<<i)];
	return ans;
}
//_log=ceil(log2(n));
int erfen2(int k)//倒着,从最高位决定是0还是1 
{
	int ans=1<<_log;vc
	for(int i=_log-1;i>=0;i--)
		if(t[ans-(1<<i)]>=k)	ans-=(1<<i);
		else k-=t[ans-(1<<i)];
	return ans;
}

完整测试版:(树状数组记得是某一个数出现了几次)

#include<bits/stdc++.h>
#define lowbit(x) (x&(-x))
using namespace std;
int n,_log;
int t[100005];
void add(int x,int k)
{
	while(x<=n)
	{
		t[x]+=k;
		x+=lowbit(x);
	}
}
//全局第k小 
//_log=log2(n);
int erfen1(int k)//正着,有点类似于倍增,更好理解; 
{
	int ans=0; 
	for(int i=_log;i>=0;i--)
		if(t[ans+(1<<i)]<=k)
			ans+=(1<<i),k-=t[ans+(1<<i)];
	return ans;
}
//_log=ceil(log2(n));
int erfen2(int k)//倒着,从最高位决定是0还是1 
{
	int ans=1<<_log;
	for(int i=_log-1;i>=0;i--)
		if(t[ans-(1<<i)]>=k)	ans-=(1<<i);
		else k-=t[ans-(1<<i)];
	return ans;
}
int main()
{
	cin>>n;
	_log=ceil(log2(n));
	while(1)
	{
		int a,b;
		cin>>a>>b;
		if(b)	add(a,b);
		else cout<<erfen2(a)/*erfen1(a)*/<<'\n';
	}
	return 0;
}

相应题目:

一道有一点高级的“模板”题:

P6619 [省选联考 2020 A/B 卷] 冰火战士

posted @ 2024-07-11 15:05  YT0104  阅读(2)  评论(0编辑  收藏  举报