(笔记)树状数组 BIT 树状数组上二分

树状数组(Binary Index Tree)

\(\texttt{Tips}\)\(\text{lowbit}(x)\) 表示 \(x\) 二进制位下最小的满足 \(k\in \N,v=2^k,x\cap v=v\)\(v\),其中 \(\cap\) 为按位与,文中所有“前面的位”表示更高位,“后面的位”表示更低位。

  • 树状数组是一个 \(O(\log n)\) 修改,\(O(\log n)\) 查询的小常数数据结构,操作次数依赖于位置 \(p\) 二进制中 \(0/1\) 的个数。其原理主要是 \(sum_i\) 储存的信息是 \((i-\text{lowbit}(i),i]\) 的信息(可以是 \(\min,\max,\sum\) 等)。

需要证明:节点 \(i\) 维护的是 \([i-\text{lowbit}(i)+1,i]\) 的连续一段区间的信息,即 \([i-\text{lowbit}(i)+1,i]\)(写成整数域就是 \((i-\text{lowbit}(i),i]\))所有点都可以通过不断自增 \(\text{lowbit}\)(这里的 \(\text{lowbit}\) 是实时更新的)到达 \(i\)

之前我脑子抽了写了个超级构式的“严谨证明”,现在看来简直是和溢位。实际上考虑这个问题会发现,每次加 \(\text{lowbit}\) 相当于把末位一段连续 \(1\) 消掉然后进位到连续段邻接的第一个 \(0\) 上,这样接下去一个 \(0\) 至多变成一次 \(1\) 然后又变回 \(0\)

那么显然是从低位往高位影响,可达性显然,因为除 \(\text{lowbit}\) 的更高位都是相同的,可以合并所有低位存在的 \(1\) 达到低位只有 \(\text{lowbit}\)\(1\) 的效果。会不会统计非法信息?如果更高位不同则永远无法到达 \(i\),因为合并到 \(\text{lowbit}\) 相同位以后,由于更高位不同仍需继续自增,而这会导致 \(\text{lowbit}\) 相同位的 \(1\) 被消成 \(0\) 且永远无法恢复,所以两者永远不可能相等。

留档先前讲解

Lemma1:显然 \(\text{lowbit}\) 在自增过程中具有单调递增性

很诡异发现很久以前学了但是对其原理并不是很理解。对于节点 \(i\) 维护的是 \([i-\text{lowbit}(i)+1,i]\) 的连续一段区间的信息,维护方式见下。要理解这种方法为什么可以这么维护,我们需要证明儿子到祖先节点的可达性(祖先节点不需要到达儿子)。

我们先要搞清楚 \(+,-\) 一个 \(\text{lowbit}(i)\) 在干什么。显然 \(-\text{lowbit}(i)\) 就是从低到高消去二进制数 \(1\) 的位置,而 \(+\text{lowbit}(i)\) 则需要不同的分析。我们考虑一个数的 \(\text{lowbit}\) 的定义,显然比它低的位置此时都是 \(0\),那么加上后如果前一位是 \(0\),那么就进位 \(1\) 到前一位,否则找到以它为结尾的极长 \(1\) 的连续段,然后把它全部清除为 \(0\),并进位 \(1\) 到左边第一个为 \(0\) 的位置,这时候新进位的位置的后面所有位置构成一个全 \(0\) 段。

再考虑如何通过 \(+\text{lowbit}(i)\) 得到一个二进制数,显然在自增的过程中任意一个为 \(0\) 的位变成 \(1\) 的唯一情况当且仅当该位前面的数位形态保持不变,然后后面全都是 \(0\),且该位为 \(1\)。并且,对于自增过程中的任意一个数 \(x\) 也唯一对应着一个原来是 \(0\) 的位变成 \(1\) 的情况(类似一个双射关系)。这个东西很好理解,严谨证明应该也不难。

也就是一个二进制数 \(y\) 可以被 \(x\) 自增得到的充要条件是:

  1. \(y\) \(\text{lowbit}(y)\) 前面的数位形态和 \(x\) 相同

  2. \(y\)\(\text{lowbit}(y)\) 后面全都是 \(0\) \(x\) 后面必然有至少一位 \(1\)

  3. \(y\)\(\text{lowbit}(y)\) 位为 \(1\),且 \(x\) 该位上为 \(0\)

Proof

该证明实际上是Lemma1的复杂版本。

这个证明是构造性的。上述条件的满足确保了 \(y>x\),因为在所有高位相同的情况下 \(y\) 的低位全都是 \(0\),而 \(x\) 可能有数,但由第三点中间位(即 \(\text{lowbit}(y)\))一定是 \(y\)\(1\)\(x\)\(0\) 的。

证明:首先后者容易理解,因为每次 \(+\text{lowbit}(i)\) 都对应着一次必然的进位,而该次进位必然落在之前为 \(0\) 的某个位置上,且不会重复。除了开始比 \(\text{lowbit}(i)\) 后的所在 \(0\) 位以外,其他 \(0\) 都会被覆盖,因为它们的存在“截断”了连续 \(1\) 段的合并。由于进位最多会使一个位从 \(0\) 变成 \(1\)(然后会使很多个 \(1\) 变成 \(0\)),所以每个数对应的位置有唯一性,即每个自增都可推出满足上述条件的一个二进制数 \(y\),且该位一定是之前没有被覆盖过的,因为把 \(1\) 变为 \(0\) 的操作是不可逆的,一旦一个位加入了后面的极长连续 \(0\) ,那么根据 \(\text{lowbit}\) 定义无法再加一个 \(\le\) 它的数,从而该位会在之后永远都是 \(0\)

再考虑前者,考虑其唯一性,我们发现一个比 \(\text{lowbit}(i)\) 前的 \(0\) 被覆盖为 \(1\) 的时刻(由上该时刻必定存在),下一次自增就会使其进位并重新清空该位为 \(0\),所以每位 \(0\) 也唯一对应一个数

综合上面可以得到每个满足上述条件的一个二进制数都可以由一个自增推出

举例说明

举例说明:我们拿二进制数 \(1100100\) 举例,并观察树状数组插入的方式,发现其是每次加上 \(\text{lowbit}(i)\),加的位置一定会构成连续一段的 \(1\) 的末尾,然后进位到左端第一个为 \(0\) 的地方,并把这一段连续的 \(1\) 都变成 \(0\),此时发现原来连续一段 \(1\) 的位置到末尾就是一个全 \(0\) 段(由 \(\text{lowbit}\) 定义可得。考虑到 \(1100100\) 的最低位为 \(2^2\),那么如果 \(i\in[1100000+1,1100100]\),不妨忽略 \(2^2\) 以上的其他位,发现一定存在一个时刻使得加了若干次 \(\text{lowbit}\) 后进位到 \(2^2\),且此时后面的位都是 \(0\),这时就一定可以贡献到该数上。

然后证明不在区间内数的不可达性,对于 \(>1100100\) 的数,显然每次都在加,不可能贡献到它上面。对于 \(<1100000\) 的数,比 \(\text{lowbit}((1100100)_2)\) 高的位置显然形态会改变,不符合我们一位 \(0\) 要被覆盖为 \(1\) 需要满足的条件(即更高位形态相同)。对于 \(1100000\),我们发现它的 \(\text{lowbit}\) 已经改变且比原来要高位,那么自增就永远无法覆盖到之前为 \(1\) 的那位(\(2^2\))了。

总结:根据上面的条件,考虑对于任意二进制数 \(x\),其 \(<x-\text{lowbit}(x)\) 的位置一定会使得比 \(\text{lowbit}\) 高的位数位形态不同\(>x\) 显然不能自增到 \(x\)\(x-\text{lowbit}(x)\)\(\text{lowbit}\)\(\text{lowbit}(x)\) 大,所以不能自增得到。

而根据上面三条条件,显然 \([x-\text{lowbit}(x)+1,x)\) 的所有数都符合条件,然后 \(x\) 本身就不需要自增,因此我们证明了儿子到父亲的可达性

时间分析

每次加 \(\text{lowbit}\) 相当于把末位一段连续 \(1\) 消掉然后进位,这样的操作最劣情况下是从 \(1\) 加到 \(2^{\lfloor\log n\rfloor}\),时间复杂度 \(O(\log n)\),且绝大多数情况跑不满。

每次减 \(\text{lowbit}\) 简单理解为消去一位,最劣情况是全 \(1\),时间复杂度也是 \(O(\log n)\)

模板

一维树状数组单点修改和区间查询的差分性质,使其能够在 \(O(\log{n})\) 的时间内求出:

\(\sum_{i=1}^{n}A[i]\)

还可以做到 \(O(\log n)\) 的动态修改。

这是一维:

struct Tre{
	int av[N];
	int lowbit(int x){return x&-x;}
	void ins(int p,int x){
		p++;
		for(int i=p;i<=n+1;i+=lowbit(i))
			av[i]+=x;
	}
	int que(int p){
		p++;
		int res=0;
		for(int i=p;i;i-=lowbit(i))
			res+=av[i];
		return res;
	}
}T;

树状数组区间加区间求和

有点逆天一玩意,基本思路是树状数组可以进行区间加和单点查询。

具体地,用树状数组维护一个关于原序列 \(\{a_i\}\) 的差分数组 \(\{c_i\}\) 使得 \(c_n=a_n-a_{n-1}\) ,则有 \(a_n=\sum_{i=1}^n c_i\)

考虑区间查询 \([1,p]\),则结果为

\[\begin{aligned} ans&=\sum_{i=1}^p a_i \\&=\sum_{i=1}^p \sum_{j=1}^i c_j \\&=\sum_{j=1}^p (p-j+1)c_j \\&=(p+1)\sum_{j=1}^p c_j-\sum_{j=1}^p jc_j \end{aligned} \]

考虑到查询 \(p\)\(c_j\) 没有必然联系,则维护 \(\sum c_i\)\(\sum ic_i\) 即可。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e6+5;
const LL MOD=1e9+7;
int n,m,a[N];
struct Tre{
	LL av[N],bv[N];//av即ci,bv即ici
	int lowbit(int x){
		return x&-x;
	}
	void ins(int p,LL x){
		for(int i=p;i<=n;i+=lowbit(i))
			av[i]+=x,bv[i]+=x*p;
	}
	LL que(int p){
		LL res=0;
		for(int i=p;i;i-=lowbit(i))
			res+=av[i]*(p+1)-bv[i];
		return res;
	}
}T;
void update(int L,int R,LL val){
	T.ins(L,val);
	T.ins(R+1,-val);
}
LL query(int L,int R){
	return T.que(R)-T.que(L-1);
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		update(i,i,a[i]);
	}
	for(int i=1;i<=m;i++){
		int opt,x,y;
		cin>>opt>>x>>y;
		if(opt==1){
			LL k;
			cin>>k;
			update(x,y,k);
		}
		else {
			cout<<query(x,y)<<'\n';
		}
	}
	return 0;
}

树状数组 + DFS 序实现树上链加与单点查

只需要先记录 DFS 序(序列长度为 \(2n\),在进入 DFS 时记录一次,离开 DFS 时记录一次)中节点 \(u\),第一次出现 \(L_u\),第二次出现 \(R_u\)。差分思想实现修改。每次修改只能从根节点(钦定为 \(1\))到 \(u\) 的一条路径,其修改过程就是在序列上对 \([1,L_u]\) 进行区间加,那么所有在链上的节点 \(v\) 只有 \(L_v\) 会被覆盖,因此点权值就是 \(query(L_v)-query(R_v)\)。对于 \(s\to t\) 的链修改 \(+val\),就是在 \(1\to s,1\to t\)\(+val\),在 \(1\to LCA(s,t),1\to fa[LCA(s,t)]\)\(-val\)

这种东西比较方便的应用场景应该是要修改的链是一条直的即一个端点是另一个端点的祖先,这时候就不需要考虑这么多 LCA 的问题。

代码懒得写,还是写树剖罢(?

树状数组上二分

比线段树上二分难写(确),但是常数小。

实际上可以把所有区间二分的问题转化为二分一个前缀和另一个东西做差分(当然需要具备可差分性,所以树状数组上二分具有局限性),然后二分这个前缀长度 \(len\) 的话考虑从高到低位考虑如果有这一位是否能满足条件,然后不断逼近边界,有点像倍增的思想。由于前缀信息是单调的,所以一定可以找到这个边界。

P6859 蝴蝶与花

喵喵题。利用双指针拓展的做法,分讨一下然后优化在树状数组上二分找到第一个满足位置的点就找到了答案,具体细节看出题人题解?/fn。

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=2e6+5;
int n,m,a[N],ub;
struct Tre{
	int av[N];
	int lowbit(int x){return x&-x;}
	void ins(int p,int x){for(int i=p;i<=n;i+=lowbit(i))av[i]+=x;}
	int find(int x){
		int res=0,id=0;
		for(int i=ub;i>=0;i--)
			if(((1<<i)|id)<=n&&res+av[(1<<i)|id]<=x)
				id|=(1<<i),res+=av[id];
		return id;
	}
	int findpro(int len){
		int res=0,id=0,sum=que(len);
		for(int i=ub;i>=0;i--){
			if(((1<<i)|id)<=len)
				id|=(1<<i),res+=av[id];
			else if(((1<<i)|id)<=n&&res+av[(1<<i)|id]-sum==2*(((1<<i)|id)-len))
				id|=(1<<i),res+=av[id];
		}
		return id;
	}
	int que(int p){int res=0;for(int i=p;i;i-=lowbit(i)){res+=av[i];}return res;}
}T;
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>n>>m;ub=log2(n);
	for(int i=1;i<=n;i++)
		cin>>a[i],T.ins(i,a[i]);
	for(int i=1;i<=m;i++){
		char c;cin>>c;
		if(c=='A'){
			int s;cin>>s;
			if(!s){cout<<"none\n";continue;}
			int p=T.find(s);
			if(T.que(p)==s){cout<<'1'<<' '<<p<<'\n';continue;}
			else {
				p++;
				if(p>n){cout<<"none\n";continue;}
			}
			int cnt1=T.findpro(0);
			int cnt2=T.findpro(p-1);
			if(cnt1==n||cnt2<p-1){cout<<"none\n";continue;}
			cnt2=cnt2-p+1;
			if(cnt1<cnt2)cout<<2+cnt1<<' '<<p+cnt1<<'\n';
			else {
				if(p+cnt2-1==n){cout<<"none\n";continue;}
				cout<<1+cnt2<<' '<<p+cnt2<<'\n';
			}
		}
		else {
			int pos,v;
			cin>>pos>>v;
			T.ins(pos,v-a[pos]);
			a[pos]=v;
		}
	}
	return 0;
}

二维树状数组

和一维树状数组极其类似的数据结构。

二维树状数组在 \(O(\log{n}\log{m})\) 的时间内求出:

\(\sum_{i=1}^{n} \sum_{j=1}^{m}A[i][j]\)

这是二维:

struct Tre{
	int av[N][N];
	int lowbit(int x){return x&-x;}
	void ins(int x,int y,int val){
		for(int i=x;i<=N;i+=lowbit(i))
			for(int j=y;j<=N;j+=lowbit(j))
				av[i][j]+=val;
	}
	int que(int x,int y){
		int res=0;
		for(int i=x;i;i-=lowbit(i))
			for(int j=y;j;j-=lowbit(j))
				res+=av[i][j];
		return res;
	}
}T;
posted @ 2025-04-24 14:46  TBSF_0207  阅读(24)  评论(0)    收藏  举报