学习笔记——树状数组

树状数组

树状数组,就是用数组来模拟树形结构。

可以方便的实现单点更新,单点查询,区间查询,区间更新

常数比线段树小很多,也比线段树好打,但是实际上树状数组能解决的问题线段树都可以解决。

介绍树状数组

假如我们有一个数组,要实现logn级别的区间查询,那么该怎么办呢。

我们考虑将这个数组建在一棵树上,每一个节点代表一段区间(这点在我线段树的blog里讲解了),树状数组就是由此产生的。

这就是一个树状数组(原图

在这里插入图片描述

下面的是原数组,用a[i]代替,树状数组设为c[i]

那么显而易见

c [ 1 ] = a [ 1 ] c[1]=a[1] c[1]=a[1]

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

c [ 3 ] = a [ 3 ] c[3]=a[3] c[3]=a[3]

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

c [ 5 ] = a [ 5 ] c[5]=a[5] c[5]=a[5]

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

c [ 7 ] = a [ 7 ] c[7]=a[7] c[7]=a[7]

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

如果转化为2进制,我们就能很快的找到树状数组组织数据的方式:
c [ 01 ] = a [ 01 ] c[01]=a[01] c[01]=a[01]
c [ 11 ] = a [ 11 ] c[11]=a[11] c[11]=a[11]
c [ 10 ] = a [ 01 ] + a [ 10 ] c[10]=a[01]+a[10] c[10]=a[01]+a[10]
c [ 110 ] = a [ 101 ] + a [ 110 ] c[110]=a[101]+a[110] c[110]=a[101]+a[110]
c [ 1000 ] = a [ 0001 ] + … … + a [ 1000 ] c[1000]=a[0001]+……+a[1000] c[1000]=a[0001]++a[1000]

在这里插入图片描述

我们发现了什么呢

假设一个数 i i i,转二进制后从最低位开始往高位数,一共有 k k k个0,那么
c [ i ] = ∑ i − 2 k + 1 i a [ i ] c[i]=\sum_{i-2^k+1}^ia[i] c[i]=i2k+1ia[i]

那么看来,知道从低位到高位有多少个0是很重要的,我们不会去数0,我们选择寻找最低位的1.

lowbit

先人给了我们一个有力的公式
(lowbit返回的是 2 k 2^k 2k

int lowbit(x){return x&(-x);}

要解释这个公式的内容,我们要先学习计算机存储负数的特性。在存储负数时,计算机会使用补码,补码就是原码取反后加一

譬如原码是正数8->00001000
补码是00001000,不变

原码是负数–8,我们先计算8的二进制00001000
那么负数的补码就是11110111+1=11111000

0的补码是00000000

x=0

如果原来的x=0,那么-x=0,按位与之后结果是0(这种情况在树状数组中并不会出现)。

x为奇数

原来的x如果是一个奇数,其二进制为????????1

(?代表未知)

那么其补码

¿¿¿¿¿¿¿0+1=¿¿¿¿¿¿¿1

(¿代表?取反,易知? &¿=0)

那么按位与后,得到的结果是00000001,就是 2 0 = 1 2^0=1 20=1

x为偶数

偶数我们分两部分讨论,第一种偶数是2的整数次幂。
则有 x = 2 m ( m ∈ N ) x=2^m(m∈N) x=2m(mN)
那么x可以表示为 001000000 ( m 个 0 ) 001000000(m个0) 001000000m0
其补码为
110111111 ( m 个 1 ) + 1 = 1100000 ( m 个 0 ) 110111111(m个1)+1=1100000(m个0) 110111111m1+1=1100000m0

那么按位与的结果就是
0010000 ( m 个 0 ) , 2 m 0010000(m个0),2^m 0010000m02m

如果不是2的整数次幂,那么就是一个奇数乘以2的整数次幂

x = y ∗ 2 m x=y*2^m x=y2m
相当于一个奇数左移m位,那么不难得到x&(-x)的结果依旧是lowbit。

更新与使用树状数组

单点修改与区间查询

依旧看这张图片,其中绿色的是更新过程

在这里插入图片描述
每一次更新a[i],都要更新与他有关的c[i],实际上就是把 i i i的最低位1加上一个1.

比如更新 c [ 5 ] 就 是 c [ 101 ] , 我 们 连 着 就 要 更 新 c [ 110 ] , c [ 1000 ] , c [ 10000 ] c[5]就是c[101],我们连着就要更新c[110],c[1000],c[10000] c[5]c[101]c[110],c[1000],c[10000]

void update(int x,int k){
	while(x<=n){
		tree[x]+=k;
		x+=lowbit(x);
	}
}

假如我们要查询区间[b,c]的值,那么我们可以用[1,c]减去[1,b-1]的值。
查询从1到某一点的值实际就是每次去掉最低位的1.

∑ i = 1 c a [ 1111 ] = c [ 1111 ] + c [ 1110 ] + c [ 1100 ] + c [ 1000 ] \sum_{i=1}^ca[1111]=c[1111]+c[1110]+c[1100]+c[1000] i=1ca[1111]=c[1111]+c[1110]+c[1100]+c[1000]

int query(int x){
	int ans=0;
	while(x!=0){
		ans+=tree[x];
		x-=lowbit(x);
	}
	return ans;
}

到这里,你已经可以完成模板1了

Luogu P3374

区间修改,单点查询

如果我们要对一个区间内所有的数都加,如果我们一个一个修改的话比暴力还要慢,这个时候该怎么办呢

我们考虑维护原数组的差分值,如果想了解差分可以看我的另一篇文章、这样一来,我们只需要修改树状数组内的两个值,就可以方便快捷的完成修改操作。

至于单点查询,假如查询i的值,我们只需要求树状数组内[1-i]的和就可以。

模板2 Luogu P3368
这里给出模板题代码

#include<bits/stdc++.h>
#define ri register int
using namespace std;
int n,m;
long long tree[500050];
long long lowbit(int x)
{
	return x&(-x);
}
void update(int pos,long long x)
{
	while(pos<=n)
	{
		tree[pos]+=x;
		pos+=lowbit(pos);
	}
}
long long query(int pos)		//单点询问 
{
	long long ans=0;
	while(pos)
	{
		ans+=tree[pos];
		pos-=lowbit(pos);
	}
	return ans;
}
int main()
{
	scanf("%d%d",&n,&m);
	long long last=0;			//差分值初始化 
	for(ri i=1;i<=n;i++)
	{
		long long x;
		scanf("%lld",&x);
		update(i,x-last);
		last=x;
	}
	while(m--)
	{
		int x;
		scanf("%d",&x);
		if(x==1)				//区间修改 
		{
			int l,r,k;
			scanf("%d%d%d",&l,&r,&k);
			update(l,k);
			update(r+1,-k);
		}
		else
		{
			int y;
			scanf("%d",&y);
			printf("%lld\n",query(y));
		}
	}
}

区间修改+区间查询

我们差分后的数组求 [ 1 − i ] [1-i] [1i]的和,实际上求得的就是a[i]的值,如果我们要求 ∑ i = l r a [ i ] \sum_{i=l}^ra[i] i=lra[i]的值,该怎么办呢。

我们按照以前的思路,先求 ∑ i = 1 r \sum_{i=1}^r i=1r的值,减去 ∑ i − 1 l − 1 a [ i ] \sum_{i-1}^{l-1}a[i] i1l1a[i]就可以了

有下面的式子( d [ i ] d[i] d[i]是差分数组的值)

∑ i = 1 r a [ i ] = ∑ i = 1 r d [ i ] + ∑ i = 1 r − 1 d [ i ] + … … + ∑ i = 1 1 d [ i ] \sum_{i=1}^ra[i] =\sum_{i=1}^rd[i]+\sum_{i=1}^{r-1}d[i]+……+\sum_{i=1}^1d[i] i=1ra[i]=i=1rd[i]+i=1r1d[i]++i=11d[i]
= ∑ i = 1 r ( r − i + 1 ) d [ i ] =\sum_{i=1}^r(r-i+1)d[i] =i=1r(ri+1)d[i]
= n ∑ i = 1 n d [ i ] − ∑ i = 1 n ( i − 1 ) ∗ d [ i ] =n\sum_{i=1}^nd[i]-\sum_{i=1}^n(i-1)*d[i] =ni=1nd[i]i=1n(i1)d[i]

那么我们实际上只需要维护两个前缀和,
sum1[i] = D[i],sum2[i] = D[i]*(i-1);

源代码链接

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;
        sum2[i] += k * (x-1);
        i += lowbit(i);
    }
}

int getsum(int i){        //求前缀和
    int res = 0, x = i;
    while(i > 0){
        res += x * sum1[i] - sum2[i];
        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;
}

The end

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值