数据结构(3) 树状数组
引入
对树状数组有基本了解的可以跳到下个环节。特别的,由于树状数组维护一些不可差分信息时间复杂度远劣于线段树,故本文不做描述。
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\)。 
树状数组可解决的问题:
树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。
通常来说,我们会用树状数组维护一下几种操作:
- 
单点修改,区间查询 
- 
区间修改,单点查询 
- 
区间修改,区间查询 
树状数组相比线段树的优势:代码短,常数小,空间小(线段树要开四倍空间而树状数组不用)
树状数组的性质:
- 
对于 \(x \le y\),要么有 \(c_x\) 和 \(c_y\) 不交,要么有 \(c_x\) 包含于 \(c_y\)。 
- 
\(c_x\) 真包含于 \(c[x + \operatorname{lowbit}(x)]\)。 
- 
对于任意 \(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)\)。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号