树状数组(树状数组的基本用法与操作)

什么是树状数组?树状数组简单的来说就是将一个数组模拟树形结构。

树状数组有什么用?树状数组可以将求和的操作从O(n)操作简化为O(logn)。

如图所示,横线下方为a数组表示为初试数据;上方为数组c,利用树形结构存储a数组内的数据。我们列举出来的这些:

c[1]=a[1]

c[2]=a[1]+a[2]

c[3]=a[3]

c[4]=a[1]+a[2]+a[3]+a[4]

c[5]=a[5]

c[6]=a[5]+a[6]

c[7]=a[7]

c[8]=a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]

......

当我们列举出来这些数据的时候,我们发现:

①所有下标为奇数的c数组都为对应的a数组的值。

②所有下标为2的k次幂的c数组都为对应a数组2的k次幂的前缀和。

③其它下标为偶数的c数组都是由该偶数前所有的值的和,减去该下标对应的最大的2的k次幂的值的和。

即c[i] =a[i - 2^{k}+1] + a[i -2^{k}+2] + ... + a[i];   //k为i的二进制中从最低位到高位连续零的长度(该偶数的2进制数的最低位1的位置再-1。

当我们计算7的前缀和,即我们需要计算sum=c[7]+c[6]+c[4];

即我们可以拓展为SUM = C[i] + C[i-2^{k1}] + C[(i - 2^{k1}) - 2^{k2}] + .....;//k1k为i的二进制中从最低位到高位连续零的长度,k2为i-2^{k1}后从最低位到高位连续零的长度。

我们引入lowbit(x)函数,此函数操作为x&(-x)。这里的-x在计算机中是以补码的形式计算的。此函数的作用为:当x为0时,返回为0;当x为奇数时,返回1;当x为偶数时,且为2的m次方时,返回x;当x为偶数,却不为2的m次方的形式时,返回2^{k},其中k为二进制中从最低位到高位连续零的长度(该偶数的2进制数的最低位1的位置再-1)。

注意事项:树状数组能够有效的解决单点更新,区间查询的过程。

题目引入:树状数组

代码:

#include<iostream>
#include <stdio.h>
#include<string.h>
using namespace std;
int a[1000001];
int c[1000001]; 
int lowbit(int i){
	return i&(-i);
}
void update(int i,int x,int n){
	while(i<=n){
		c[i]+=x;
		i+=lowbit(i);
	}
}
int downdate(int i){
	int sum=0;
	while(i>0){
		sum+=c[i];
		i-=lowbit(i);
	}
	return sum;
}
int main(){
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		update(i,a[i],n);
	}
	int x,y,z;
	for(int i=1;i<=m;i++){
		cin>>x>>y>>z;
		if(x==1)update(y,z,n);
		else cout<<downdate(z)-downdate(y-1)<<endl;
		
	}
	return 0;
}

区间更新,单点查询

我们使用差分数组来表示,即D[i]=a[i]-a[i-1](i>=1);//建议看这里,有比较详细的介绍和说明

区间更新:

例如我们需要更新区间[2,6],将[2,6]区间内的每一个都加上x,x可正可负;

则:

D[2]=(a[2]+x)-(a[1])  =a[2]-a[1]+x=D[2]+x

D[3]=(a[3]+x)-(a[2]+x)=a[3]-a[2]  =D[3]

D[4]=(a[4]+x)-(a[3]+x)=a[4]-a[3]  =D[4]

D[5]=(a[5]+x)-(a[4]+x)=a[5]-a[4]  =D[5]

D[6]=(a[6]+x)-(a[5]+x)=a[6]-a[5]  =D[6]

D[7]=(a[7])  -(a[6]+x)=a[7]-a[6]-x=D[7]-x

即若更新[2,6],将区间[2,6]区间内的每一个都加上x,则就相当于D[2]+x,D[7]-x。

我们推广到一般情况,如果更新区间[a,b],且将[a,b]区间内加上x,则相当于D[a]+x,D[b+1]-x。

单点查询:

若想查询a[n],则根据累加法:

a[n]=D[n]+a[n-1]

a[n-1]=D[n-1]+a[n-2]

...

a[3]=D[3]+a[2]

a[2]=D[2]+a[1]

a[1]=D[1]+a[0]

a[0]=0

a[n]=\sum_{k=1}^{n}D[k]

这种情况我们则需要用树状数组了。

即用D数组来建立一个树状数组。

区间更新,区间查询

对于求[1,n]区间内的总和sum,我们有如下处理:

已知a[i]=\sum_{k=1}^{i}D[k]\sum_{i=1}^{n}a[i]=\sum_{i=1}^{n}\sum_{j=1}^{i}D[j]=n*(D[1])+(n-1)*D[2]+...+1*D[n]

=n*(D[1]+D[2]+...+D[n])-(0*D[1]+2*D[2]+...+(n-1)*D[n])

=n*\sum_{i=1}^{n}D[i]-\sum_{i=1}^{n}(i-1)*D[i]

这样我们得到了两个\sum,即可用于树状数组。

我们令c数组为(i-1)*D[i]。

所以,我们设置两个树状数组sum1[i]<-D[i]和sum2[i]<-c[i]。

区间更新:

若更新区间[a,b],并且在区间同时+x,对于sum1,我们可以在a处向上更新x,在b+1处更新-x;对于sum2,我们可以在a处向上更新(a-1)*x在b+1处向上更新b*-x;

区间查询:

若查询区间[l,r],我们可以找到区间[0,l-1]的值,向下查找sum1-sum2,找到[0,r],向下查找sum1-sum2;

代码:

#include<bits/stdc++.h>
int n,m;
int a[50005] = {0};
int sum1[50005];    //(D[1] + D[2] + ... + D[n])
int sum2[50005];    //(1*D[1] + 2*D[2] + ... + n*D[n])
int lowbit(int x){
    return x&(-x);
}
void updata(int i,int k){
    int x = i;    //因为x不变,所以得先保存i值
    while(i <= n){
        sum1[i] += k;//更新sum1
        sum2[i] += k * (x-1);//更新sum2
        i += lowbit(i);
    }
}
int getsum(int i){        //求前缀和
    int res = 0, x = i;
    while(i > 0){
        res += x * sum1[i] - sum2[i];//这里因为sum1没有乘以n,所以在出结果时成
        i -= lowbit(i);
    }
    return res;
}
int main(){
    cin>>n;
    for(int i = 1; i <= n; i++){
        cin>>a[i];
        updata(i,a[i] - a[i-1]);   //输入初值的时候,也相当于更新了值
    }
    //[x,y]区间内加上k
    updata(x,k);    //A[x] - A[x-1]增加k
    updata(y+1,-k);  //A[y+1] - A[y]减少k
    //求[x,y]区间和
    int sum = getsum(y) - getsum(x-1);
    return 0;
}

当然,对于区间查询,区间更新的问题,我么还可以使用线段树来处理。

树状数组求逆序对数

我们求逆序对数除了归并算法求,还可以树状数组求逆序对数。

步骤:

  • 离散化:这里所谓离散化就是将一个序列的相对大小表示出来。目的是为了方便数据存储。对于重复的数据,我们就依据数据的先后顺序来处理

例如序列:6 25 9 63 2

序列的离散化结果为2 4 3 5 1

例如序列5 60 5 3 2 3

序列的离散化结果为4 6 5 2 1 3

  • 树状数组求逆序对数

我们怎样求逆序对数呢?

我们可以依次从后向前遍历,以遍历的时间为序列的顺序,向前查找并且更新。

例如此序列逆序为1 5 3 4 2,则第一次寻找1前面的数据有多少比1小(向下寻找),然后把1向上更新+1;第二次寻找5前面的数据,发现c[4]=1,即5前面有1个比5小的数,然后将5向上更新+1。

题目引入:树状数组求逆序对

代码:

#include<bits/stdc++.h>
using namespace std;
struct node{
	long long sum;
	long long j;
	long long k;
};
long long sum=0;
node a[50000005];
long long c[50000005];
long long lowbit(long long x){
	return x&(-x);
}
bool cmp(const node xx,const node yy){
	if(xx.sum==yy.sum&&xx.j<yy.j)return true;
	if(xx.sum<yy.sum)return true;
	else return false;
}
bool cmp1(const node xx,const node yy){
	if(xx.j<yy.j)return true;
	else return false;
}
int main(){
	long long n;
	cin>>n;
	for(long long i=1;i<=n;i++){
		cin>>a[i].sum;
		a[i].j=i;
	}
	sort(a+1,a+1+n,cmp);
	for(long long i=1,x=1;i<=n;i++){
		a[i].k=x;
		x++;
	}
	sort(a+1,a+1+n,cmp1);
	for(long long i=n;i>=1;i--){
		long long x=a[i].k;
		long long y=x;
		while(y>0){
			sum+=c[y];
			y-=lowbit(y); 
		}
		while(x<=n){
			c[x]++;
			x+=lowbit(x);
		}
	}
	cout<<sum;
	return 0;
} 

  • 14
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值