树状数组详解(单点修改和区间查询)

本文详细介绍了树状数组这种数据结构,它用于解决数列的单点修改和区间查询问题。相较于暴力和前缀和方法,树状数组在保持O(1)的单点修改复杂度的同时,将区间查询复杂度降低到O(log2n)。文章通过实例解释了树状数组的工作原理,包括lowbit的实现,最后提供了完整的C++代码示例。

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

引入

我们来看下面的例题:(洛谷 P3374 【模板】树状数组 1

已知一个数列,你需要进行下面两种操作:
1.将某一个数加上 x x x
2.求出某区间每一个数的和

我们有哪些方法呢?

  1. 暴力。单点修改的时间复杂度为 O ( 1 ) O(1) O(1),区间查询的复杂度为 O ( n ) O(n) O(n)
  2. 前缀和。单点修改的时间复杂度为 O ( n ) O(n) O(n),区间查询的复杂度为 O ( 1 ) O(1) O(1)

我们可以看到,两种做法都有一个操作是 O ( 1 ) O(1) O(1)的,但也都有一个操作是 O ( n ) O(n) O(n)的。

那有没有什么两全其美的做法呢?当然有了。有的人可能就会喊了:线段树!

线段树固然好,但它也有它的缺点:算法常数大,编写难度高

那就轮到我们的主角登场了:树状数组

树状数组

树状数组顾名思义,不是树,而是树状的数组。树状数组常数小,编写难度小,且能够解决线段树所能解决的部分问题。

是的,是部分问题。能用线段树解决的问题,不一定能用树状数组解决。而能用树状数组解决的问题,全都能被线段树解决。

但是,由于上述的优点,树状数组仍然是一种十分重要的数据结构。

我们来看一下树状数组的结构:
树状数组
红色的格子代表树状数组,白色的长条是为了便于理解。
我们用数组 c [ ] c[] c[]来存储树状数组, a [ ] a[] a[]来存储原始数据。
我们可以看到,每个序号都有它的红色格子。我们以序号 4 4 4为例,来分析一下:序号 4 4 4对应的红色格子以及左边的长条覆盖了下面 1 1 1 ~ 4 4 4的位置,表示 c [ 4 ] c[4] c[4]存储的是 a [ 1 ] a[1] a[1] ~ a [ 4 ] a[4] a[4]的和。
c[4]存储的是a[1]~a[4]的和

至于 c [ ? ] c[?] c[?],存储 a [ ? ] a[?] a[?] a [ ? ] a[?] a[?]的和,这是怎么算出来的,这是树状数组的关键,留到最后讲。

区间查询、单点修改

这样,我们就可以实现区间查询了。比如说,我们要求 [ 3 , 7 ] [3,7] [3,7]内每个数的和,利用前缀和的思想,可以用 [ 1 , 7 ] [1,7] [1,7]的和减去 [ 1 , 2 ] [1,2] [1,2]的和。 [ 1 , 2 ] [1,2] [1,2]的和,即为 c [ 2 ] c[2] c[2]的值, [ 1 , 7 ] [1,7] [1,7]的和,即为 c [ 4 ] + c [ 6 ] + c [ 7 ] c[4]+c[6]+c[7] c[4]+c[6]+c[7]

至于单点修改,若我们要修改a[2]的值,因为只有 c [ 2 ] c[2] c[2] c [ 4 ] c[4] c[4] c [ 8 ] c[8] c[8]中有 a [ 2 ] a[2] a[2],所以我们只需要修改 c [ 2 ] c[2] c[2] c [ 4 ] c[4] c[4] c [ 8 ] c[8] c[8]的值就可以了。

这样,我们就做到了 O ( l o g 2 n ) O(log_2 n) O(log2n)区间查询单点修改

lowbit的实现

所以,只剩下最重要的部分了,也就是 c [ ] c[] c[]存储和的问题。

注意,这个地方有点难理解,而且会用到二进制,做好思想准备。

这里,我们会用到一个叫 l o w b i t lowbit lowbit的东西。所谓 l o w b i t lowbit lowbit,就是“求二进制下最低位 1 1 1的运算”。

不明白?不要紧。让我们来模拟一下。以 6 6 6为例:

( 6 ) 10 (6)_{10} (6)10的二进制为 ( 110 ) 2 (110)_2 (110)2
6的二进制为110

从右往左数,数到第一个 1 1 1
从右往左数,数到第一个1

第一个 1 1 1及其右边的部分为 10 10 10
第一个1及其右边的部分为10

( 10 ) 2 (10)_2 (10)2的十进制是 ( 2 ) 10 (2)_{10} (2)10
10的十进制是2

所以 c [ 6 ] c[6] c[6]储存的就是包括 6 6 6向左数的 2 2 2个数,即 a [ 5 ] a[5] a[5] a [ 6 ] a[6] a[6]
包括6向左数的2个数

对照一下树状数组的结构图,是不是?

l o w b i t lowbit lowbit是怎样实现的呢?

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

就这么简单。那原理呢?
我们知道,计算机中的负数是用补码存储的,补码就是反码 + 1 +1 +1,一个正数转变为负数,就是原码转化为补码的过程。我们以 10010 10010 10010为例:
原码 10010 10010 10010
反码 01101 01101 01101(原码取反)
补码 01110 01110 01110(反码 + 1 +1 +1,表示负数)
可以看到,补码的前一部分与原码相反,后一部分与原码相同,而相同的那一部分就是我们要求的 l o w b i t lowbit lowbit
这样,我们就解决了最后一个问题。

完整代码

洛谷 P3374 【模板】树状数组 1

#include<iostream>
#include<cstdio>
using namespace std;
int n,q;
int op,x,y;
int a[1000010];
int c[1000010];
int lowbit(int x)
{
	return x&-x;
}
void update(int x,int k)
{
	for(;x<=n;x+=lowbit(x))
		c[x]+=k;
}
int query(int x)
{
	int res=0;
	for(;x>=1;x-=lowbit(x))
		res+=c[x];
	return res;
}
int main()
{
	scanf("%d%d",&n,&q);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		update(i,a[i]);
	}
	for(int i=1;i<=q;i++)
	{
		scanf("%d%d%d",&op,&x,&y);
		if(op==1)
			update(x,y);
		else
			printf("%d\n",query(y)-query(x-1));
	}
	return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值