树状数组详细讲解,不会算法也能看懂哦

有一天,小明给了我三个问题(其实是我自己出的啦~)

(1)有一个机器,支持两种操作,在区间[1,10000]上进行。

操作A:把位置x的值+k

操作B:询问区间[l,r]所有数字之和

区间的初始值全部为0

现在你要充当这个机器,操作A和操作B会被穿插着安排给你,

要求对于所有操作B,给出正确的答案。

怎样做才能最节省精力?

(2)有一个机器,支持两种操作,在区间[1,10000]上进行。

操作A:把区间[l,r]的值全都+x

操作B:询问x位置的值。

区间的初始值全部为0

现在你要充当这个机器,操作A和操作B会被穿插着安排给你,

要求对于所有操作B,给出正确的答案。

怎样做才能最节省精力?

(3)有一个机器,支持两种操作,在区间[1,10000]上进行。

操作A:把区间[l,r]的值全都+x

操作B:询问区间[l,r]所有数字之和

区间的初始值全部为0

现在你要充当这个机器,操作A和操作B会被穿插着安排给你,

要求对于所有操作B,给出正确的答案。

怎样做才能最节省精力?

三个问题中操作的数量都可以认为是10000这个数量级

你可以动用的工具有:无限墨水的笔,一张足够大的纸,你的大脑(没多大内存的~)。

注意:

1.举个例子,进行这种类似的操作:

从一行任意打乱的数字中找一个数字

不能认为一瞬间就可以找到,在这里所花费的精力和数字的总数具有线性关系。

2.我们认为将数据转换为二进制不需要任何时间。

对于问题1,如果我们每种操作都暴力进行,

那么显然总的时间复杂度为O(mA+n*mB),n表示区间长度,

mA表示操作A执行的次数,mB表示操作B执行的次数。

那么有没有一种更加轻松的办法呢?

我们将引入一种数据结构,叫做<树状数组>。

在介绍树状数组之前,需要先介绍一下二进制的按位运算,这里只需要用到两种:

按位与运算,符号&: 两个数字都为1时,得1,否则得0.

那么1&1=1,0&1=0,1&0=0,0&0=0

3&11的值是多少呢?我们把它化成二进制

两个数字分别为0011和1011,然后对应的,每位之间进行与运算

  0011

& 1011

——————
  0011

所以答案是0011,即十进制的3。

接下来再介绍一下按位非运算,符号~,运算方法是0变1,1变0

比如9的值,就是1001(二进制),得到0110。

那么按位运算就说完了。

最后为了方便理解后面的内容,还得介绍一个计算机的特点。

在计算机中,我们操作的变量通常都有一个固定的位数,

比如c++中 int32_t 类型的变量,它用32位的二进制数来存储一个整数。

在这个范围下,

1=00000000000000000000000000000001=11111111111111111111111111111110

另外,n位的二进制数进行运算,一旦向第n+1位有进位,会直接舍去这个进位,

如四位二进制数1111+0001,答案是0000而不是10000。

有了这么多铺垫,要开始正题啦~

现在就要介绍一个非常神奇的函数,它叫做lowbit

lowbit(x)=x&((~x)+1) (为了少引入补码的概念,我们这里稍微麻烦了一下,其实x&-x就行)

它的作用是什么呢?

它只保留"从低位向高位数,第一个数字1"作为运算结果

比如二进制数00011100,结果就是00000100,也就是4。

11111001,结果就是00000001,也就是1

不信的话可以验证一下。

那么这种运算对我们的算法有什么帮助呢?

首先我们来解决一下问题1。

先列举出从1~32的lowbit,

1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 16 1 2 1 4 1 2 1 8 1 2 1 4 1 2 1 32

我们让第i个位置管理[i-lowbit(i)+1,i]这一段区间,示意图如下:

怎么看每个数字管理多少?

只要顺着数字往下画竖线,碰到的第一根横线所覆盖的范围就是它能管理的范围。
在这里插入图片描述
我们每次执行操作A(把位置x的值+k),只需要把"能管理到x的所有位置"都+k就行

那么怎样快速找到哪些位置能管理到x呢?

答案还是lowbit

我们先更新x,然后把x赋给一个新值,x+lowbit(x),那么新值依然可以管理到x,这样依次类推直到

x>10000即可。

比如x=2,那么首先把2的值+k,这不用说。

然后x的新值=x+lowbit(x)=2+lowbit(2)=4,对着上面的示意图看看,会发现4确实能管理到2,那么把4的位置+k

然后再来一遍,x=4+lowbit(4)=8,发现8还是能管理到2,继续给8这个位置+k,就这样依次类推下去

直到x=16384时,超过10000了,操作完成。

这样操作之后,树状数组里每一位当前存的值可能并不是该位置的实际值,为了方便区分,在下文中我们把实际值叫做"原数组的值",当前值就叫做"树状数组的值"。

可以证明,对于任意一个x属于[1,10000]我们最多进行log(2,10000)次操作,就可以完成操作A

那么把操作A变复杂(从O(1)变到O(logn))能换来什么好处?

答案就是,可以把操作B的时间复杂度降低成log级别的

询问区间[L,R]的和sum(L,R)。我们只需要求出sum(1,R)和sum(1,L-1),

然后sum(1,R)-sum(1,L-1)就是sum(L,R)了

那么对于任意的x,sum(1,x)怎么求呢?

我们把最终得到的答案存在ans变量中,执行下面的操作:

(1)ans初始化为0

(2)ans加上x位置的值

(3)给x赋予新值 x-lowbit(x)

(4)如果x>0则跳回操作(2),否则结束算法。

举个例子介绍一下:

一开始我们还是停留在树状数组第x位置上(比如x=6吧),答案一开始为0。

还记得吗,我们在进行"给原数组第x位置的数增加k"这个操作时,把"能管理到x的所有位置"都增加了k。

那么,对于任意一个位置,树状数组里的值就是"它能管理到的所有位置上,原数组的值之和"。

因此我们给答案加上树状数组第x位置的值,这里就得到了sum(5,6),因为6能管理[5,6]

然后给x减去lowbit(x),得到4。再加上x位置的值,也就是sum(1,4),因为4能管理[1,4]

再让x=x-lowbit(x),得到0,由于不再大于0,算法终止,得到答案。

这时答案恰好是sum(1,6),哈哈~

依然可以证明,最多只需要进行log级别次数的查询。

这样我们进行操作B的时间复杂度也是log级别了。

至此,树状数组就说完了,问题1也成功得到解决,时间复杂度O((mA+mB)*logn)。

在10000这个数量级下明显比之前的O(mA+(mB*n))小得多。

而且,位运算的常数非常小,因此整个算法执行速度会很快。

问题2怎么办?用差分的方法,区间[l,r]所有值+k改成"位置l加上k,位置r+1减去k"

查询的时候直接查询sum(1,x)就行,不理解的话可以自己构造一组数据尝试一下。

问题3怎么办?稍微复杂一点

用两个树状数组,分别叫做d和s

进行A操作时,d维护差分,s维护x*d[x]。

update(d,l,x);update(d,r+1,-x);

update(s,l,xl);update(s,r+1,-x(r+1));

进行B操作时

sum(L,R)=sum(1,R)-sum(1,L-1)

sum(1,L-1)=L*query(d,L-1)-query(s,L-1)

sum(1,R)=(R+1)*query(d,R)-query(s,R)

最后附上我自己封装的c++树状数组模板~

首先是简洁版,适合比赛现场手写,非常简短

int tree[100010],n=100000;
void add(int x,int num)
{
	for(;x<=n;x+=x&-x)
		tree[x]+=num;
} 
int sum(int x)
{
	int answer =0;
	for(;x>0;x-=x&-x)
		answer+=tree[x];
	return answer;
} 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值