树状数组学习

树状数组简介

树状数组,用于维护和查询前缀和,与线段树功能类似。树状数组代码短,常数和空间小,时间复杂度小,所以这也是一个十分优秀的算法。

在这里插入图片描述

a [ i ] a[i] a[i]为原数组上的点, s [ i ] s[i] s[i]为树状数组中各点,则

s [ 1 ] = a [ 1 ] s[1]=a[1] s[1]=a[1]
s [ 2 ] = a [ 1 ] + a [ 2 ] s[2]=a[1]+a[2] s[2]=a[1]+a[2]
s [ 3 ] = a [ 3 ] s[3]=a[3] s[3]=a[3]
s [ 4 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] + a [ 4 ] s[4]=a[1]+a[2]+a[3]+a[4] s[4]=a[1]+a[2]+a[3]+a[4]
s [ 5 ] = a [ 5 ] s[5]=a[5] s[5]=a[5]
s [ 6 ] = a [ 5 ] + a [ 6 ] s[6]=a[5]+a[6] s[6]=a[5]+a[6]

可以发现, s [ x ] = ∑ i = x − 2 k + 1 x a [ i ] s[x]=\sum\limits_{i=x-2^k+1}^x a[i] s[x]=i=x2k+1xa[i],其中 k k k表示 x x x的二进制中末尾连续的0的个数。


树状数组的操作

lowbit(i)

根据上文, s [ x ] = ∑ i = x − 2 k + 1 x a [ i ] s[x]=\sum\limits_{i=x-2^k+1}^x a[i] s[x]=i=x2k+1xa[i],其中 k k k表示 x x x的二进制中末尾连续的0的个数。那应该如何求 2 k 2^k 2k呢?

我们可以用 l o w b i t lowbit lowbit来求, l o w b i t ( n ) lowbit(n) lowbit(n)的值为 n n n的二进制中最后一个1的权值,也就是 2 k 2^k 2k。而 l o w b i t ( n ) lowbit(n) lowbit(n)可以由n&(-n)得出。

为什么呢?我们可以得出 n − 1 n-1 n1是将 n n n的二进制从末尾到最后一个1全部取反,然后将 n − 1 n-1 n1的各位取反得 ~n+1。n与 ~n+1 在 n n n末尾最后一个1之前都为0,在末尾最后一个1之前都不相等。所以n&(~n+1)即为n的二进制中最后一个1的权值。
因为在补码中,~ n=-n-1。所以n&(~n+1)=n&(-n)

code

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

单点修改

a [ i ] a[i] a[i]增加 v v v。由于 s [ x ] = ∑ i = x − 2 k + 1 x a [ i ] s[x]=\sum\limits_{i=x-2^k+1}^x a[i] s[x]=i=x2k+1xa[i],所以 s [ i ] , s [ i + 2 k ] , … s[i],s[i+2^k],\dots s[i],s[i+2k],都要修改。

code

void add(int i,int v){
	while(i<=n){
		s[i]+=v;i+=lowbit(i);
	}
}

时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)


区间查询

求前 i i i个数的前缀和,可以画图理解。

code

int sum(int i){
	int re=0;
	while(i){
		re+=s[i];i-=lowbit(i);
	}
	return re;
}

若要求区间 [ l , r ] [l,r] [l,r]的和,则答案为 s u m ( r ) − s u m ( l − 1 ) sum(r)-sum(l-1) sum(r)sum(l1)

时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)


例题

洛谷 P3374 【模板】树状数组

单点修改+区间查询

code

#include<bits/stdc++.h>
using namespace std;
int n,m,tp,x,y,tr[500005];
int lb(int i){
	return i&(-i);
}
void add(int i,int v){
	while(i<=n){
		tr[i]+=v;i+=lb(i);
	}
}
int find(int i){
	int re=0;
	while(i){
		re+=tr[i];i-=lb(i);
	}
	return re;
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&x);
		add(i,x);
	}
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&tp,&x,&y);
		if(tp==1) add(x,y);
		else printf("%d\n",find(y)-find(x-1));
	}
	return 0;
}

洛谷 P3372

区间修改+区间查询

我们可以用差分的思想,设 d i = a i − a i − 1 d_i=a_i-a_{i-1} di=aiai1,则 a i = ∑ j = 1 i a j a_i=\sum\limits_{j=1}^i a_j ai=j=1iaj

对于区间修改,设将区间 [ l , r ] [l,r] [l,r]都增加 v v v,则 d l + = v , d r + 1 − = v d_l+=v,d_{r+1}-=v dl+=v,dr+1=v即可,这样就变成了单点修改。

对于区间查询, s u m i = ∑ j = 1 i a j = ∑ j = 1 i ∑ k = 1 j d k = ∑ j = 1 i d j ∗ ( i − j + 1 ) = ( i + 1 ) ∑ j = 1 i d j − ∑ j = 1 i ( j ∗ d j ) sum_i=\sum\limits_{j=1}^ia_j=\sum\limits_{j=1}^i\sum\limits_{k=1}^jd_k=\sum\limits_{j=1}^i d_j*(i-j+1)=(i+1)\sum\limits_{j=1}^i d_j-\sum\limits_{j=1}^i(j*d_j) sumi=j=1iaj=j=1ik=1jdk=j=1idj(ij+1)=(i+1)j=1idjj=1i(jdj)

也就是说,我们只要用两个树状数组维护 d i d_i di i ∗ d i i*d_i idi的前缀和,就能求出 a i a_i ai的和。

code

#include<bits/stdc++.h>
using namespace std;
int n,m,tp,x,y,a[100005];
long long k,tr[100005][2];
int lb(int i){
	return i&(-i);
}
void add(int i,long long v,int z){
	while(i<=n){
		tr[i][z]+=v;i+=lb(i);
	}
}
long long find(int i,int z){
	long long re=0;
	while(i){
		re+=tr[i][z];i-=lb(i);
	}
	return re;
}
long long sum(int i){
	return (i+1)*find(i,0)-find(i,1);
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		x=a[i]-a[i-1];
		add(i,x,0);
		add(i,x*i,1);
	}
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&tp,&x,&y);
		if(tp==1){
			scanf("%lld",&k);
			add(x,k,0);
			add(y+1,-k,0);
			add(x,k*x,1);
			add(y+1,-k*(y+1),1);
		}
		else printf("%lld\n",sum(y)-sum(x-1));
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值