树状数组知识点总结和学习心得分享

先介绍一下树状数组的功能.
大概念上,树状数组其实只有一个功能,那就是储存数组的前缀和.
这次要说的几个功能(单点查询,单点修改,区间查询,区间修改.整体第k小(大))从简单到稍微复杂.留个二维树状数组的坑以后填.

1.树状数组的建立
先给张图(来源蓝书)
在这里插入图片描述

一个[1,x]的区间可以划分为log(n)个小区间.这个性质在倍增的时候已经用到过了.因为长度为x,二进制下表示为x = 2k1+2k2…2kn这些一个个2的次幂长度的小区间组合起来就是长度为x的区间.还有一个特点就是区间的个数=lowbit(x)的位数,比如lowbit(12) = 二进制下的10说明12直接管辖的区间有两个.另外一个有用的性质就是这些区间的长度就是lowbit( R ),R是区间右端点.而树状数组的c[i]的i表示的正是每个区间的右端点.所以c[i]表示的就是[i-lowbit(i)+1,i]这段范围上的和.每个c[i]的父节点就是c[i+lowbit(i)] (这个其实可以说不是一个性质,而是一种规定,这样子规定才能成为树状数组.因为不断的i+lowbit(i)可以消去末尾的1,不断往上面找.). 实在无法理解为什么(其实我也没完全理解)就只记下面说的ask和add方法就ok了.
建立的方式有两种:第一种是用单点修改的方式O(nlogn)建立,第二种是从小往大建立O(n)复杂度.一般来说第一种就够用了.

2.单点修改
树状数组的单点修改是一个不断往上的过程,可以按上图理解.修改位置x的值,需要不断修改x+lowbit(x)的值.很简单.就直接上代码了.

void add(int *xx,int x,int v){
	for(;x<=n;x+=x&-x) xx[x] += v;
}

3.单点查询
先引入一个ask操作.ask是求位置x的前缀和.那么单点查询自然就是ask®-ask(l-1)了.那ask具体怎么操作呢.其实也很简单.前面说了,我们是把[1,x]分成了log(n)个区间把这log(n)个区间加起来就ok了.加起来的方法就是不断的减掉lowbit(x),这样获得的就是x的前缀和了.
举个例子吧.比如说特殊一点的16,lowbit16 = 16.所以c[16]就是16的前缀和. 再比如 7 这个位置 二进制下是111 说明它由三个小区间 111 110 100组成,也就是c[7] (长度1)+c[6] (长度2)+c[1] (长度4).
代码

int ask(int *xx,int x){
	int res = 0;
	for(;x;x-=x&-x) res += xx[x];
	return res;
}

4.区间修改+单点查询
其实这个也不难,前面说了,树状数组其实维护的就是前缀和.我们现在树状数组维护的是原数组的差分数组的前缀和.
知识点:差分数组的前缀和就是原数组.
这里有两种写法:
第一种是直接对原数组建立差分数组然后修改.ask(x)就是答案.
第二种写法是维护一个增长差分数组.ask(x)+a[x]是答案.
为了和接下来的区间查询同步起来,我们采取第二种写法.
例题

代码

#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>

using namespace std;
const int N = 2e5+10;
const int mod = 9901;

LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}

LL a[N],c[N],d[N];
int n,k;
LL ask(int x){
	int res = 0;
	for(;x;x-=x&-x) res += c[x];
	return res;
}
void add(int x,int v){
	for(;x<=n;x+=x&-x) c[x] += 1LL*v;
}
int main(){
	cin >> n >> k;
	fir(i,1,n) cin >> a[i],d[i] = a[i]-a[i-1];
	fir(i,1,k){
		char op;
		int x,l,r;
		cin >> op;
		if(op == 'Q'){
			cin >> x;
			cout << ask(x)+a[x] << endl;
		}
		else{
			cin >> l >> r >> x;
			add(l,x);
			add(r+1,-x);
		}
	}
	
	return 0;
}	

5.区间修改+区间查询
这个不像前面几个那么简单.需要点数学知识.(真学不会就线段树大法吧.)
先明确一个知识,如果没有修改.我们用最简单的sum[r]-sum[l-1]的前缀和做法就可以得到答案.那么如果增加了修改呢?
考虑前一个题目的做法.我们是维护了差分数组d[i]的前缀和. i = 1 x d i {\displaystyle \sum _{i=1}^{x}d_{i}} = b[x].那[1,x]的区间增加了多少呢?
i = 1 x j = 1 i d j {\displaystyle \sum _{i=1}^{x}\displaystyle \sum _{j=1}^{i}d_{j}}
其实这个公式算出来就是b[1]+b[2]…b[x].不过要考虑怎么优化这个公式.观察一下可以发现.d[1]被计算了x次.d[2]被计算了x-1次.规律就找到了.d[i]被计算了x-i+1次.
可以写成 i = 1 x ( x i + 1 ) d i {\displaystyle \sum _{i=1}^{x}(x-i+1)d_{i}}
再转化成 ( x + 1 ) i = 1 x d i (x+1){\displaystyle \sum _{i=1}^{x}d_{i}} i = 1 x i d i {\displaystyle \sum _{i=1}^{x}id_{i}} 这个就很明确了.只需要维护d[i]和i*d[i]的前缀和就ok了.
例题

代码

#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>

using namespace std;
const int N = 2e5+10;
const int mod = 9901;

LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}

int n,k;
LL a[N],c[N],cc[N];
LL ask(LL *xx,int x){
	LL res = 0;
	for(;x;x-=x&-x) res += xx[x];
	return res;
}
void add(LL *xx,int x,int v){
	for(;x<=n;x+=x&-x) xx[x] += 1LL*v;
}
int main(){
	cin >> n >> k;
	fir(i,1,n) cin >> a[i],a[i]+=a[i-1];
	fir(i,1,k){
		char op;
		int x,l,r;
		cin >> op;
		if(op == 'Q'){
			cin >> l >> r;
			cout << a[r]-a[l-1] + (r+1)*ask(c,r)-ask(cc,r) - l*ask(c,l-1)+ask(cc,l-1) << endl;
		}
		else{
			cin >> l >> r >> x;
			add(c,l,x);
			add(c,r+1,-x);
			add(cc,l,l*x);
			add(cc,r+1,-(r+1)*x);
		}
	}
	
	return 0;
}	

6.计数问题
引入:假设现在有n个数.我们要求x这个数在前n个数里出现了多少次.我们只需要对数值做树状数组.每个数的权值是1.就可以正常查询了.
逆序对就是这类型的问题.i<j && a[i] >a[j]就构成了一个逆序对. 换句话说.就是求i之前有多少个比a[i]大的数. 不过这个问题还是不好用树状数组求解.先讲正解再讲原因. 我们应该逆过来求,i之后有多少个比a[i]小的数.这样子就能用树状数组求解了.算之前1-a[i]-1这个范围上的数有多少个就是这个位置的逆序对.
那为什么正着做不是最好的办法呢?因为正着做我们需要的是a[i]+1-n有多少个数.这个是无法直接求的.要算两次,ask(n)-ask(a[i]).所以逆着算好算.
如果数值范围太大的话.可以选择离散化.或者直接用归并排序吧.
例题
代码

#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>

using namespace std;
const int N = 2e5+10;
const int mod = 9901;

LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}

int a[N],n;
LL c[N],l[N],r[N];
LL ask(int x){
	LL res = 0;
	for(;x;x-=x&-x) res += c[x];
	return res;
}
void add(int x,int v){
	for(;x<=n;x+=x&-x) c[x] += 1LL*v;
}
int main(){
	
	while(cin >> n){
		if(!n) break;
		LL ans = 0;
		mem(c,0);
		fir(i,1,n){
			cin >> a[i];
			l[i] = ask(n)-ask(a[i]);
			add(a[i],1);
		}
		mem(c,0);
		afir(i,n,1){
			r[i] = ask(n)-ask(a[i]);
			add(a[i],1);
			ans += l[i]*r[i]*1LL;
		}
		cout << ans << " ";
		ans = 0;
		mem(c,0);
		fir(i,1,n){
			l[i] = ask(a[i]-1);
			add(a[i],1);
		}
		mem(c,0);
		afir(i,n,1){
			r[i] = ask(a[i]-1);
			add(a[i],1);
			ans += l[i]*r[i]*1LL;
		}
		cout << ans << endl;
	}
	
	return 0;
}	

7.树状数组和倍增
在这里插入图片描述
回顾一下这张图片.前面提到树状数组有一个很优美的性质.
c[i]代表的是[c[i]-lowbit[i],i]上面的和. 有没有联想到倍增算法?
倍增算法的基本模板.在满足单调性的区域里.起点p,增长长度从2^0次方开始.不断的试探未知的区域.直到找到答案.
现在我们利用树状数组进行倍增.pos = 0, p = log(n(n是数组长度))+1.判断c[pos+2 ^p]是否满足条件(结合图看为什么可以这样),如果满足的话.pos += 2 ^p,p --.这样循环进行下去就能拿到答案. 为什么可以这么做,还是因为树状数组的结构真的很优美.提前记录好了各个增长长度的一些数据.可以直接使用.
还是做例题来感受一下吧.
例题
问题转换之后其实就是给一个01序列101010111 找到第k个1的位置.
有一个很直接的二分做法.求1-mid的前缀和是否>k.就可以找到.用树状数组优化之后就是nlog2(n)的解法.
凡是二分,基本都能转化为倍增.这里就可以用刚才讲的倍增算法.nlog(n)就可以求出答案.
具体代码

#pragma GCC optimize(3)
#define LL long long
#define pq priority_queue
#define ULL unsigned long long
#define pb push_back
#define mem(a,x) memset(a,x,sizeof a)
#define pii pair<int,int>
#define pll pair<long long,long long>
#define pdd pair<double,double>
#define db double
#define fir(i,a,b) for(int i=a;i<=b;++i)
#define afir(i,a,b) for(int i=a;i>=b;--i)
#define ft first
#define vi vector<int>
#define sd second
#define ALL(a) a.begin(),a.end()
#include <bits/stdc++.h>

using namespace std;
const int N = 2e5+10;
const int mod = 9901;

LL gcd(LL a,LL b){return b == 0 ? a : gcd(b,a%b);}

int n,k;
int a[N],c[N];
int ask(int *xx,int x){
	int res = 0;
	for(;x;x-=x&-x) res += xx[x];
	return res;
}
void add(int *xx,int x,int v){
	for(;x<=n;x+=x&-x) xx[x] += 1LL*v;
}
int main(){
	cin >> n;
	fir(i,2,n) cin >> a[i];
	fir(i,1,n) add(c,i,1);
	vi ans;
	int maxlen = log(n)/log(2)+1;
	afir(i,n,1){
		int l = 1,r = n;
		int k = a[i]+1;
		int pos = 0,sum = 0;
		afir(i,maxlen,0){	
			if(pos + (1 << i) <=n && sum + c[pos + (1 << i)] < k){
				sum += c[pos+(1 << i)];
				pos += (1 << i);
			}
		}
		ans.pb(pos+1);
		add(c,pos+1,-1);
	}
	afir(i,n-1,0) cout << ans[i] << endl;
	
	return 0;
}	

8.二维树状数组(待更,我也害没学)

猜你喜欢

转载自blog.csdn.net/weixin_45590210/article/details/105891117
今日推荐