[数据结构] 树状数组

树状数组是一种数据结构,常用于维护序列的前缀和,具有高效的单点修改和区间查询操作,复杂度为O(logn)。通过lowbit操作进行区间拆分,每个节点的和表示子树叶节点的值总和。除了基本操作,还能通过巧妙转化解决区间修改、单点查询的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

745 …

lowbit操作

对于 l o w b i t lowbit lowbit的定义为"非负整数 n n n在二进制表示下最低位的1及其后面所有的0"所构成的数值,例如:当 n = 12 n=12 n=12时, 12 = ( 1100 ) 2 12=(1100)_2 12=(1100)2,所以 l o w b i t ( 12 ) = ( 100 ) 2 = 4 lowbit(12)=(100)_2=4 lowbit(12)=(100)2=4

那么如何实现 l o w b i t lowbit lowbit操作呢?

先将n取反,此时第k为变为1,第0~k-1位变为1,再令n=n+1,此时第k为1,0 ~ k-1为0。

进行完上面的操作后,n的第 k + 1 k+1 k+1位恰好与原来相反所以 n & (   n + 1 ) n\&(~n+1) n&( n+1)仅有第k位为1。

因此: l o w b i t ( n ) = n   & (   n + 1 ) = n   & ( − n ) lowbit(n)=n\ \&(~n+1)=n\ \&(-n) lowbit(n)=n &( n+1)=n &(n)

听完概念,来举个例子:

对于整数12,它的二进制表示为 12 = ( 1100 ) 2 12=(1100)_2 12=(1100)2,将它取反变为 ( 0011 ) 2 (0011)_2 (0011)2,加1变为 ( 0100 ) 2 (0100)_2 (0100)2,可以观察到此时的二进制数为 ( 0100 ) 2 (0100)_2 (0100)2,与原数 ( 1100 ) 2 (1100)_2 (1100)2相比,恰好在 ( 1100 ) 2 (1100)_2 (1100)2最低位的为1。

树状数组的概念与性质

设想一下,在对一个较大的连续线性范围进行统计时,我们可以基于上述思想将它进行拆分。

将n拆分为: n = 2 p 1 + 2 p 2 + . . . + 2 p m n=2^{p_1}+2^{p_2}+...+2^{p_m} n=2p1+2p2+...+2pm

其中 p 1 < p 2 < p 3 < . . . < p m p_1<p_2<p_3<...<p_m p1<p2<p3<...<pm

所以我们可以将此区间分为

1. 长 度 为 p 1 的 区 间 [ 1 , 2 p 1 ] 1.长度为p_1的区间[1,2^{p_1}] 1.p1[1,2p1]

2. 长 度 为 p 2 的 区 间 [ 2 p 2 + 1 , 2 p 1 + 2 p 2 ] 2.长度为p_2的区间[2^{p_2}+1,2^{p_1}+2^{p_2}] 2.p2[2p2+1,2p1+2p2]

…………

m . 长 度 为 2 p m 的 区 间 [ 2 p 1 + 2 p 2 + . . . 2 p m − 1 + 1 , 2 p 1 + 2 p 2 + . . . + 2 p m ] m.长度为2^{p_m}的区间[2^{p_1}+2^{p_2}+...2^{p_{m-1}}+1,2^{p_1}+2^{p_2}+...+2^{p_m}] m.2pm[2p1+2p2+...2pm1+1,2p1+2p2+...+2pm]

如何找到这些区间呢?

就用到我们上面讲的lowbit操作了。

好了,一个新的数据结构就产生了,树状数组便是基于上述思想的一种数据结构,基本用于维护序列的前缀和(当然也有进阶操作),给你一个序列a,建立一个数组t,其中t[x]保存序列a中的区间, [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [xlowbit(x)+1,x]中所有数的和。

在这里插入图片描述
图中最下面是序列的每个叶节点,代表 a [ 1 , n ] a[1,n] a[1n]

该数据结构满足以下性质

1.数组t[x]保存以它为根的子树中所有叶节点的值的和。

2.每个节点t[x]的子节点个数为 l o w b i t ( x ) lowbit(x) lowbit(x)的位数。

3.除树根外,每个节点t[x]的父节点是 t [ x + l o w b i t ( x ) ] t[x+lowbit(x)] t[x+lowbit(x)]

4.树的深度为 O ( l o g   n ) O(log\ n) O(log n)

基本运用

树状数组基本支持两个操作:

单点修改

对于任意一个节点x,其祖先最多有 l o g   n log \ n log n个,我们在修改时逐一修改祖先即可。

void add(int x,int y)
{
	for(;x<=n;x+=lowbit(x)) t[x]+=y;
}

区间查询

在查询时按上面方法分为 l o g   n log\ n log n个小区间

int ask(int x)
{
	int ans=0;
	for(;x>=1;x+=lowbit(x)) ans+=t[x];
	return x;
}

若要查询区间 [ l , r ] [l,r] [l,r]所有数的和,需计算 a s k ( r ) − a s k ( l − 1 ) ask(r)-ask(l-1) ask(r)ask(l1)

以上两种操作复杂度皆为 O ( log ⁡ n ) O(\log n) O(logn)

扩展运用

了解了树状数组的基本运用,来看一道 变式题

简单来说就是将树状数组所擅长的"单点修改,区间查询"变为了"区间修改,单点查询"

对于这两条指令,我们仔细想一想怎样把这样的问题转化为树状数组所擅长的?

新建一个数组b,对于每次 [ l , r ] [l,r] [l,r]的加上 d d d的操作,转化为在 a [ l ] a[l] a[l]上加上 d d d,在 b [ r + 1 ] b[r+1] b[r+1]上减去 d d d

此时观察b 数组的前缀和,如图所示:

在这里插入图片描述
可以发现,对于数组b的前缀和,他在 [ l , r ] [l,r] [l,r]区间执行了加上 d d d的操作。运用这样的方法即可将此题转化为树状数组。

#include <bits/stdc++.h>
using namespace std;
const int MAXN=500086;
int n,m,a[MAXN],b[MAXN];
int lowbit(int k)
{
    return k & -k;
}
void add(int x,int k)
{
    while(x<=n)
    {
        b[x]+=k;
        x+=lowbit(x);
    }
}
int sum(int x)
{
    int ans=0;
    while(x!=0)
    {
        ans+=b[x];
        x-=lowbit(x);
    }
    return ans;
}
int main()
{
	freopen("tree.in","r",stdin);
	freopen("tree.out","w",stdout);
  	scanf("%d%d",&n,&m);
  	for(int i=1;i<=n;i++)
  	{
  		scanf("%d",&a[i]);	
	}
	memset(b,0,sizeof(0));
	for(int i=1;i<=m;i++)
	{
		int o;
		scanf("%d",&o);
		if(o==1)
		{
			int l,r,k;
			scanf("%d%d%d",&l,&r,&k);
			add(l,k);
			add(r+1,-k);
		}
		else {
			int j;
			scanf("%d",&j);
			printf("%d\n",a[j]+sum(j));
		}
	}
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值