树状数组

树状数组

树状数组是相比于数组计算前缀和更优的算法

原理

如果一个数组a有八个数,它对应的树状数组c可以表示成这样:

c1=a1;
c2=a1+a2;
c3=a3;
c4=a1+a2+a3+a4;
以此类推。。。。。。 很难说出他们的关系,但是如果把它们变为二进制

c0001=a0001
c0010=a0001+a0010
c0011=a0011
c0100=a0001+a0010+a0011+a0100
你会发现,将每一个二进制,去掉所有高位1,只留下最低位的1,然后从那个数一直加到1,看一看是不是这样。
在这里插入图片描述
这个操作可以写一个lowerbits函数来完成
这里用了按位与的位运算和计算机编码中的补码(看注释吧)

int lowbits(int x)
{
	return x&(-x);//so,这里,我们用到的是补码
	//所求数的补码与它复数的补码按位与,得到的是补码最右边的1所代表的数 
}
//原码 :符号位加对应二进制数 
// 正整数的原码,反码,补码都一样
//负正数的原码是,反码是原码除符号位外按位取反,补码是反码加1;
//但实际上是这样的
// 负数的补码等于他的原码自低位向高位,尾数的第一个‘1’及其右边的‘0’保持不变,左边的各位按位取反,符号位不变。

对于某一数的更新,由树状数组的结构决定了我们只要更新它的父段直到顶端即可。

void update(int p,int k)
{
	while(p<=n)
	{
		c[p]+=k;
		p+=lowbits(p);
	}
}

相关操作

求和

int getsum(int p)
{
	int res=0;
	while(p>=1)
	{
		res+=c[p];
		p-=lowbits(p);
	}
	return res;
}//这里求的是第一个数a[1]到该数a[p]的区间和

附模板题的ac代码
https://www.luogu.org/problem/P3374

#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <string>
#include <cstring>
#include <queue>

using namespace std;
#define ll long long
#define maxn 2000005

ll a[maxn],c[maxn];
int m,n;

int lowbits(int x)
{
	return x&(-x);
}
void update1(int p,ll k)
{
	while(p<=n)
	{
		c[p]+=k;
		p+=lowbits(p);
	}
}
ll getsum(ll p)
{
	int res=0;
	while(p>=1)
	{
		res+=c[p];
		p-=lowbits(p);
	}
	return res;
}
int main()
{
	while(~scanf("%d %d",&n,&m))
	{
		memset(c,0,sizeof(c));
		for(int i=1;i<=n;++i)
		{
			scanf("%d",&a[i]);
			update(i,a[i]);	
		}
		ll flag,x,y;
		for(int i=0;i<m;++i)
		{
			scanf("%lld %lld %lld",&flag,&x,&y);
			if(flag==1)
			{
				update(x,y);
			}
			if(flag==2)
			{
				printf("%lld\n",getsum(y)-getsum(x-1));
			}
		}
	}
	return 0;
}
 

树状数组的第二个操作:差分

所以,差分是啥?

设数组a[]={1,6,8,5,10},那么差分数组b[]={1,5,2,-3,5}
也就是说b[i]=a[i]-a[i-1];(a[0]=0;),那么a[i]=b[1]+…+b[i];(这个很好证的)。
假如区间[2,4]都加上2的话,
a数组变为a[]={1,8,10,7,10},b数组变为b={1,7,2,-3,3}。
发现了没有,b数组只有b[2]和b[5]变了,因为区间[2,4]是同时加上2的,所以在区间内b[i]-b[i-1]是不变的。
所以对区间[x,y]进行修改,只用修改b[x]与b[y+1]。
即:b[x]=b[x]+k,b[y+1]=b[y+1]-k
那么实现起来就非常简单了。。。
// https://www.luogu.com.cn/problem/P3368 模板题ac代码:

//ex树状数组,差分思想 
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <string>
#include <cstring>
#include <queue>

using namespace std;
#define ll long long
#define maxn 530000

ll a[maxn],b[maxn],c[maxn];
int n,m;

ll lowbit(ll x)
{
	return x&(-x);
}
void update(ll x,ll k)
{
	while(x<=n)
	{
		c[x]+=k;
		x+=lowbit(x);
	}
}
ll getsum(int x)
{
	ll res=0;
	while(x>=1)
	{
		res+=c[x];
		x-=lowbit(x);
	}
	return res;
}
int main()
{
	cin>>n>>m;
	a[0]=0;
	for(int i=1;i<=n;++i)
	{
		scanf("%d",&a[i]);
		update(i,a[i]-a[i-1]);
	}
	for(int i=1;i<=m;++i)
	{
		int flag,x,y,k;
		scanf("%d",&flag);
		if(flag==2)
		{
			scanf("%d",&x);
			printf("%lld\n",getsum(x));
		}
		if(flag==1)
		{
			scanf("%d %d %d\n",&x,&y,&k);
			update(y+1,-k);
			update(x,k);
		}
	}
	return 0;
}

总的来说,在只有求区间和以及单点修改操作的时候,树状数组还是非常好用的,就那么几行,也好记。但对于复杂一点的问题,比如区间修改,区间覆盖,区间加法、乘法,区间最大最小值等等问题的时候还是需要使用线段树。所以不要过度依赖树状数组。。。这玩意其实用法很单一。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值