数据结构(3) 树状数组

引入

对树状数组有基本了解的可以跳到下个环节。特别的,由于树状数组维护一些不可差分信息时间复杂度远劣于线段树,故本文不做描述。

image

lowbit

lowbit 指的是某数 \(x\) 在二进制下最低位的 \(1\) 与后面所有的零共同组成的 \(2^k\) 的数,在实际 OI 中的运用比较少,较多就是用在树状数组。

lowbit 的求法:lowbit(x) = x & -x

树状数组可维护的信息

普通树状数组维护的信息及运算要满足 结合律可差分,如加法(和)、乘法(积)、异或等。

  • 结合律:\((x \circ y) \circ z = x \circ (y \circ z)\),其中 \(\circ\) 是一个二元运算符。

  • 可差分:具有逆运算的运算,即已知 \(x \circ y\)\(x\) 可以求出 \(y\)

树状数组可解决的问题

树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。

通常来说,我们会用树状数组维护一下几种操作:

  1. 单点修改,区间查询

  2. 区间修改,单点查询

  3. 区间修改,区间查询

树状数组相比线段树的优势:代码短,常数小,空间小(线段树要开四倍空间而树状数组不用)

树状数组的性质

  1. 对于 \(x \le y\),要么有 \(c_x\)\(c_y\) 不交,要么有 \(c_x\) 包含于 \(c_y\)

  2. \(c_x\) 真包含于 \(c[x + \operatorname{lowbit}(x)]\)

  3. 对于任意 \(x < y < x + \operatorname{lowbit}(x)\),有 \(c_x\)\(c_y\) 不交。

单点修改,区间查询

前面胡扯了半天,到这里是正式应用。

树状数组中 \(c_i\) 维护的区间为 \([i - \operatorname{lowbit}(i) + 1 , i]\),所以我们可以根据这一特点进行树状数组的修改和查询。

修改:如果我们要修改 \(a_i\),我们必然要修改所有维护区间包括 \(i\) 的点。即修改完 \(c_i\) 后循环修改 \(c_{i + \operatorname{lowbit}(i)}\)

void update(int x,int y){
	for(int i=x;i<=n;i+=lowbit(i)){
		t[i]+=y;
	}
}

让我们思考这样的一个修改和查询为什么是对的。

管辖 \(a_x\)\(c_y\) 一定包含 \(c_x\),所以 \(y\) 在树状数组树形态上是 \(x\) 的祖先。因此我们从 \(x\) 开始不断跳父亲,直到跳得超过了原数组长度为止。

查询:我们可以通过树状数组很容易的查询 \([1 , i]\) 的和,如果我们要查询 \([l , r]\) 的和,我们可以用 \(sum_r - sum_{l-1}\)

int find(int x){
	int sum=0;
	for(int i=x;i>0;i-=lowbit(i)){
		sum+=t[i];
	}
	return sum;
}

这下我们就可以完成P3374 【模板】树状数组 1了。

Code.
#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
typedef long long ll;

const int MAXN=100000+5;
int n,m;
int a[MAXN],t[MAXN];

int lowbit(int x){
	return x&(-x);
} 
void update(int x,int y){
	for(int i=x;i<=n;i+=lowbit(i)){
		t[i]+=y;
	}
	return;
}
int query(int x){
	int sum=0;
	for(int i=x;i>0;i-=lowbit(i)){
		sum+=t[i];
	}
	return sum;
}
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,a[i]);
	}
	for(int i=0;i<m;i++){
		int tmp,x,y;
		cin>>tmp>>x>>y;
		if(tmp==1)update(x,y);
		if(tmp==2)cout<<query(y)-query(x-1)<<endl;
	}
	
	return 0;
}

区间修改,单点查询

注意到我们可以将树状数组当成差分数组来用。

我们如果想对 \([l,r]\) 进行区间修改的话,我们可以选择 \(update(l,x)\)\(update(r+1,-x)\),这样我们就成功完成了区间修改。

让我们思考一下如何进行单点查询。由于我们这次在树状数组中维护的是 \(a_i\) 的修改值,故实际答案为 \(a_i + query(i)\)

这下我们就可以完成P3368 【模板】树状数组 2了。

Code.
#include<bits/stdc++.h>
#define endl "\n"
using namespace std;
typedef long long ll;

const ll MAXN=500000+5;
ll n,m;
ll a[MAXN],t[MAXN];

ll lowbit(ll x){
	return x&(-x);
} 
void update(ll x,ll y){
	for(ll i=x;i<=n;i+=lowbit(i)){
		t[i]+=y;
	}
	return;
}
ll find(ll x){
	ll sum=0;
	for(ll i=x;i!=0;i-=lowbit(i)){
		sum+=t[i];
	}
	return sum;
}
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
	cin>>n>>m;
	for(ll i=1;i<=n;i++){
		cin>>a[i];
	}
	for(ll i=0;i<m;i++){
		ll tmp;
		cin>>tmp;
		if(tmp==1){
			ll l,r,k;
			cin>>l>>r>>k;
			update(l,k);
			update(r+1,-k);
		}
		if(tmp==2){
			ll x;
			cin>>x;
			cout<<a[x]+find(x)<<endl;
		}
	}
	
	return 0;
}


区间修改,区间查询

可以想到这个东西其实是区间修改单点查询的增强版。

由于是区间修改区间查询,我们考虑用树状数组维护差分数组。由于差分数组的定义是 \(d_i = a_i - a_{i-1}\),其中 \(d_i\) 表示的是差分数组,\(a_i\)表示原数组,故对差分数组求前缀和后可以得到原数组。

让我们考虑区间查询:我们依考虑查询区间 \([1,r]\),即:

\(\begin{aligned} \sum_{i=1}^{r} a_i = \sum_{i=1}^r\sum_{j=1}^i d_j \ = \sum_{i=1}^r\sum_{j=1}^i d_j = \sum_{i=1}^r d_i\times(r-i+1)\ = (r+1)\sum_{i=1}^r d_i\ - \sum_{i=1}^r d_i\times i \end{aligned}\)

推完这一串式子我们就可以明确我们要维护的是什么,我们构建两个树状数组,一个维护 \(d_i\),一个维护 \(d_i * i\)

查询考虑完了继续考虑区间修改,对于差分数组,我们可以只改变 \(l\)\(r+1\),对于维护 \(d_i\) 的树状数组,对 \(l\) 单点加 \(v\)\(r + 1\) 单点加 \(-v\);对于维护 \(d_i \times i\) 的树状数组,对 \(l\) 单点加 \(v \times l\)\(r + 1\) 单点加 \(-v \times (r + 1)\)

这个样子我们就可以完成P3372 【模板】线段树 1了。

Code.
#include<bits/stdc++.h>
#define endl "\n"
#define pb push_back
#define mkp make_pair
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
//value 
const int inf=2147483647;
const int mod=1e9+7;
ll a[1000005],c1[1000005],c2[1000005];


//function 
void solve(){
	
	
	
	return;
}
ll lowbit(ll x){
	return x & (-x);
}
void add(ll x,ll k){
	for(int i=1;i<=1e5+5;i+=lowbit(i))c1[i]+=k;
	for(int i=x;i<=1e5+5;i+=lowbit(i)){
		c1[i]-=k;
		c2[i]+=x*k;					
	}
}
ll query(ll x){
	ll res=a[x];
	for(int i=x;i>0;i-=lowbit(i))res+=c1[i]*x+c2[i];
	return res; 
}


 
int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0); 
	
//	freopen(".in","r",stdin);
//	freopen(".out","w",stdout);
	
	ll n,q;
	cin>>n>>q;
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1;i<=n;i++)a[i]+=a[i-1];
	
	while(q--){
		ll opt,x,y;
		cin>>opt>>x>>y;
		if(opt==1){
			ll k;
			cin>>k;
			add(y,k);
			if(x>1)add(x-1,-k);
		}
		else{
			cout<<query(y)-query(x-1)<<endl;
		}
	}
	
	
	
	
	
	return 0;
}

权值树状数组

本质和普通树状数组没有区别,可以算是树状数组的简单应用。

首先解释一下这个“权值”代表的是什么,“权值”指的是权值数组,一个序列 \(a\) 的权值数组 \(b\),满足 \(b_x\) 的值为 \(x\)\(a\) 中的出现次数。权值树状数组就是就是在权值数组上建了一颗线段树。

而这个权值数组我们可以发现数组大小和值域是相关的,所以我们需要考虑将原数组离散化。

单点修改,查询全局第 \(k\):

我们注意到在权值树状数组中,\(i\) 的前缀和表示的是在原数组中小于 \(i\) 的元素的数量,所以我们可以在树状数组上二分,时间复杂度约为 \(O(\log^2 n)\)

全局逆序对(全局二维偏序)

由上一种权值树状数组应用我们可以得到启发,我们将原数组离散化后,从头到尾依次插入权值树状数组。设我们插入的元素是 \(x\),则树状数组中到 \(x\) 位的前缀和,就是排在插入元素在原数组的该元素前且数字比 \(x\) 小的数。则我们可以发现,每次插入时,本数对全局逆序对的贡献就是 \(i - get(a_i)\),其中 \(get()\) 是树状数组中的求和。时间复杂度是 \(O(n \log n)\)

posted @ 2025-07-22 20:58  -Delete  阅读(4)  评论(0)    收藏  举报