浅谈树状数组入门

本人第一次写博客,主要以我学习树状数组时的思维为时间线推进,如有不足请见谅

作用

树状数组是一种支持单点修改、区间查询的数据结构。码量很小,占用空间小,效率也较高,是入门维护区间和数据结构的不二之选

空间复杂度O(n) ;修改、查询复杂度O(log n)。

怎么做到

先看一道模板例题:https://www.luogu.com.cn/problem/P3374

显然,这里完全可以开一个普通数组,用最朴素的循环暴力求解,然后得到TLE声一片~ 10^5的数量级哪里是那么能好过的?那我们看看有什么可以优化的地方:大家也应该看出来,就比如当依次询问[1,3][1,4]区间的和时,会有一段重复计算的区间。当询问次数变多时,这种重复计算的劣势就体现的相当明显。

那么怎么办?这里就要介绍一种结构——树状数组啦!

问大家一个问题,若要计算[1,6]的区间和,你想到的办法是什么?\sum_{i=1}^{n}a_{i}绝对是最常规的方法,那如果我告诉你\sum_{i=1}^{4}a_{i}\sum_{i=5}^{6}a_{i}的值呢?先不要问为什么是这两段区间和,我们很容易理解这二者相加就是要求的答案,而看到这,树状数组的基本原理便已可以知晓——我们将原始数组拆成不多于log n的区间,对于每一个树状数组元素c[x],满足:1.其管辖的区间右端就是x 2.区间向左“延伸”的长度为lowbit(x)。不知道lowbit是什么?不着急,听我细细道:

lowbit

将一个整数转化为2进制,你就获得了一个0/1串,从最低位往高位找,一直找到第1个1出现为止,然后打住!将你所找的从最低位到第一个1出现的这一段截取的0/1串所获得的数再转化为十进制,就是我们所谓的lowbit啦!举个例子,123的二进制是1111011,截取的0/1串为1,因此lowbit(123)=1,48的二进制是110000,截取的0/1串为10000,转化为10进制为16,因此lowbit(48)=16,实际计算,我们一般向下面代码一样算lowbit,至于为什么,位运算学过吗?学过的应该都明白,没学过的可以先去了解一下

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

一般地,树状数组元素c[x]管辖的区间就是[x-lowbit(x)+1,x],为什么规律那么奇奇怪怪的?这就牵扯到树状数组的一大妙处了!试试把c[1]一直到c[8]所代表的区间分别算出来?发现了什么?贴一张图感受一下c[x]数组相互的关系,可以连成如下的“父子关系图”(可以理解为一棵树),其中每个父节点的权值是它所有子节点的和,而每个c[x]的父节点就是c[x+lowbit(x)]

 单点修改

假设一棵如上图所示的数已经建好,考虑怎样对一个元素的权值进行修改(最常见的是让其加上一个值)。实际上,如果改动的是a[x],我们要改动的只是管辖范围包含x的c,打个比方,改动a[5],那么对于上面的树状数组,我们修改的是这几个元素——c[5],c[6],c[8]

 根据c[x]管辖区间的规律,a[x]一定在c[x]的管辖范围内,然后不断往父节点上跳,一直到顶(也就是到最大的c[n]),这样,我们就完成了对a[x](其实是树状数组)的修改,还是以改a[5]为例,它的改动顺序为:    c[5]->c[6]->c[8]

void update(int x,int y){//树状数组中x位置 加上y
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=y;
	}
}

建树

为什么树状数组的建树不放第一个讲,是因为我认为它的建树比较奇葩:常规建树方法就是将每个a[i]的值都单点加入树状数组,这样建树是靠单点修改一个个改过去的,我还是第一次见

区间查询

我们注意到一个事实:\sum_{i=n}^{m}a_{i}=\sum_{i=1}^{m}a_{i}-\sum_{i=1}^{n-1}a_{i},为什么要提到这点?因为树状数组只能求区间[1,x]的和。那么查询[1,x]区间和到底怎么理解?首先,需要有c[x]的权值是肯定的,然后一直往左跳,跳到c[x]维护区间的左端点-1作为右端点这样一个区间的c[y](易知y=x-lowbit(x)),以x>0为循环条件,把跳到的位置所含的元素累加起来,最后得到的就是[1,x]的区间和啦

还是以查询[1,5]为例

 代码按照思路打,也是十分清晰了

int sum(int x){//求区间a[1……x]的和 
	int sumn=0;
	for(int i=x;i>0;i-=lowbit(i)) sumn+=c[i];
	return sumn;
}

然后这道题就基本上捋顺了,贴完整代码

​
#include<bits/stdc++.h>
using namespace std;
int c[500005],n;
int lowbit(int x){
	return x & (-x);
}
void update(int x,int y){//树状数组中x位置 加上y
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=y;
	}
}
int sum(int x){//求区间a[1……x]的和 
	int sumn=0;
	for(int i=x;i>0;i-=lowbit(i)) sumn+=c[i];
	return sumn;
}
int main()
{
	int m,a;cin>>n>>m;
	int start,end,k;
	for(int i=1;i<=n;i++){//建树
		scanf("%d",&a);
		update(i,a);
	}
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",&k,&start,&end);
		if(k==2) printf("%d\n",sum(end)-sum(start-1));
		else update(start,end);
	}
	return 0;
}

​

几个注意事项:

1.数组老问题,若要维护的元素有n个,c数组至少要开n+1个,因为树状数组是从下标1存的

2.注意[x,y]的区间和是[1,y]的区间和减去[1,x-1]的区间和,不是减[1,x]的

3.树状数组仅能解决2类问题:区间查询+单点修改 或者 区间修改+单点查询(用差分处理即可),且要满足结合律并可差分(如+,*,xor),其它问题用它效率感人或根本做不了。那么既有区间查询又有区间修改,该怎么做?线段树是也!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值