学树状数组?这篇就够(入门)了!
有一天,小明给了我三个问题其实是我自己出的啦
(1)
有一个机器,支持两种操作,在区间[1,10000]上进行。
操作 A \rm A A:把位置 x \rm x x的值+ k \rm k k
操作 B \rm B B:询问区间 [ l , r ] \rm [l,r] [l,r]所有数字之和
区间的初始值全部为 0 \rm 0 0
现在你要充当这个机器,操作 A \rm A A和操作 B \rm B B会被穿插着安排给你,
要求对于所有操作 B \rm B B,给出正确的答案。
怎样做才能最节省精力?
(2)
有一个机器,支持两种操作,在区间 [ 1 , 10000 ] [1,10000] [1,10000]上进行。
操作 A \rm A A:把区间 [ l , r ] [l,r] [l,r]的值全都+ x \rm x x
操作 B \rm B B:询问 x \rm x x位置的值。
区间的初始值全部为 0 \rm 0 0
现在你要充当这个机器,操作 A \rm A A和操作 B \rm B B会被穿插着安排给你,
要求对于所有操作 B \rm B B,给出正确的答案。
怎样做才能最节省精力?
三个问题中操作的数量都可以认为是10000这个数量级
你可以动用的工具有:无限墨水的笔,一张足够大的纸,你的大脑(没多大内存的~)。
注意:
- 举个例子,进行这种类似的操作:
从一行任意打乱的数字中找一个数字
不能认为一瞬间就可以找到,在这里所花费的精力和数字的总数具有线性关系。- 我们认为将数据转换为二进制不需要任何时间。
对于问题
1
\rm 1
1,如果我们每种操作都暴力进行,
那么显然总的时间复杂度为
O
(
m
A
+
n
∗
m
B
)
\rm O(mA+n*mB)
O(mA+n∗mB),
n
\rm n
n表示区间长度,
m
A
\rm mA
mA表示操作
A
\rm A
A执行的次数,
m
B
\rm mB
mB表示操作
B
\rm B
B执行的次数。
那么有没有一种更加轻松的办法呢?
我们将引入一种数据结构,叫做树状数组。
在介绍树状数组之前,需要先介绍一下二进制的按位运算,这里只需要用到两种:
按位与运算,符号&: 两个数字都为1时,得1,否则得0.
那么 1 1 1& 1 1 1 = = = 1 1 1, 0 0 0& 1 1 1 = = = 0 0 0, 1 1 1& 0 0 0 = = = 0 0 0, 0 0 0& 0 0 0 = = = 0 0 0
3 3 3& 11 11 11的值是多少呢?我们把它化成二进制
两个数字分别为 0011 0011 0011和 1011 1011 1011,然后对应的,每位之间进行与运算
0011 ~~~0011 0011
& 1011 1011 1011
— — — — — — —————— ——————
0011 ~~~0011 0011
所以答案是 0011 0011 0011,即十进制的 3 3 3。
接下来再介绍一下按位非运算,符号~,运算方法是0变1,1变0
比如 ~ 9 9 9的值,就是~ 1001 1001 1001(二进制),得到 0110 0110 0110。
那么按位运算就说完了。
最后为了方便理解后面的内容,还得介绍一个计算机的特点。
在计算机中,我们操作的变量通常都有一个固定的位数,
比如 c \rm c c++中 i n t \rm int int 32 32 32_ t \rm t t 类型的变量,它用 32 32 32位的二进制数来存储一个整数。
在这个范围下,
~ 1 1 1 =~ 00000000000000000000000000000001 00000000000000000000000000000001 00000000000000000000000000000001= 11111111111111111111111111111110 11111111111111111111111111111110 11111111111111111111111111111110
另外, n \rm n n位的二进制数进行运算,一旦向第 n \rm n n+ 1 1 1位有进位,会直接舍去这个进位,
如四位二进制数 1111 1111 1111+ 0001 0001 0001,答案是 0000 0000 0000而不是 10000 10000 10000。
有了这么多铺垫,要开始正题啦~
现在就要介绍一个非常神奇的函数,它叫做lowbit。
l o w b i t ( x ) \rm lowbit(x) lowbit(x)= x \rm x x& ( ( (( ((~ x ) \rm x) x)+ 1 ) 1) 1)(为了少引入补码的概念,我们这里稍微麻烦了一下,其实 x \rm x x&- x \rm x x就行)
它的作用是什么呢?
它只保留"从低位向高位数,第一个数字1"作为运算结果
比如二进制数 00011100 00011100 00011100,结果就是 00000100 00000100 00000100,也就是 4 4 4。
11111001 11111001 11111001,结果就是 00000001 00000001 00000001,也就是 1 1 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 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 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 \rm i i个位置管理 [ i − l o w b i t ( i ) + 1 , i ] \rm [i-lowbit(i)+1,i] [i−lowbit(i)+1,i]这一段区间,示意图如下:
怎么看每个数字管理多少?
只要顺着数字往下画竖线,碰到的第一根横线所覆盖的范围就是它能管理的范围。
我们每次执行操作 A \rm A A(把位置 x \rm x x的值+ k \rm k k),只需要把"能管理到 x \rm x x的所有位置"都+ k \rm k k就行
那么怎样快速找到哪些位置能管理到x呢?
答案还是 l o w b i t \rm lowbit lowbit
我们先更新 x \rm x x,然后把 x \rm x x赋给一个新值, x + l o w b i t ( x ) \rm x+lowbit(x) x+lowbit(x),那么新值依然可以管理到 x \rm x x ,这样依次类推直到
x \rm x x> 10000 10000 10000即可。
比如 x = 2 \rm x=2 x=2,那么首先把 2 2 2的值 + k \rm +k +k,这不用说。
然后 x \rm x x的新值= x \rm x x+ l o w b i t ( x ) \rm lowbit(x) lowbit(x)= 2 2 2+ l o w b i t ( 2 ) \rm lowbit(2) lowbit(2)= 4 4 4,对着上面的示意图看看,会发现 4 4 4确实能管理到 2 2 2,那么把 4 4 4的位置+ k \rm k k
然后再来一遍, x \rm x x= 4 4 4+ l o w b i t ( 4 ) = 8 \rm lowbit(4)=8 lowbit(4)=8,发现 8 8 8还是能管理到 2 2 2,继续给 8 8 8这个位置 + k \rm +k +k,就这样依次类推下去
直到x = 16384 =16384 =16384时,超过10000了,操作完成。
这样操作之后,树状数组里每一位当前存的值可能并不是该位置的实际值,为了方便区分,在下文中我们把实际值叫做"原数组的值",当前值就叫做"树状数组的值"。
可以证明,对于任意一个 x \rm x x属于[1,10000]我们最多进行 l o g \rm log log ( 2 , 10000 ) (2,10000) (2,10000)次操作,就可以完成操作A
那么把操作A变复杂,从 O ( 1 ) \rm O(1) O(1)变到 O ( l o g n ) \rm O(logn) O(logn)能换来什么好处?
答案就是,可以把操作 B \rm B B的时间复杂度降低成 l o g \rm log log级别的
询问区间 [ L , R ] \rm [L,R] [L,R]的和 s u m ( L , R ) \rm sum(L,R) sum(L,R)。我们只需要求出 s u m ( 1 , R ) \rm sum(1,R) sum(1,R)和 s u m ( 1 , L − 1 ) \rm sum(1,L-1) sum(1,L−1),
然后 s u m ( 1 , R ) \rm sum(1,R) sum(1,R)- s u m ( 1 , L − 1 ) \rm sum(1,L-1) sum(1,L−1)就是 s u m ( L , R ) \rm sum(L,R) sum(L,R)了
那么对于任意的 x \rm x x, s u m ( 1 , x ) \rm sum(1,x) sum(1,x)怎么求呢?
我们把最终得到的答案存在 a n s \rm ans ans变量中,执行下面的操作:
(1) a n s \rm ans ans初始化为 0 0 0
(2) a n s \rm ans ans加上 x \rm x x位置的值
(3)给 x \rm x x赋予新值 x \rm x x- l o w b i t ( x ) \rm lowbit(x) lowbit(x)
(4)如果 x > 0 \rm x>0 x>0则跳回操作 ( 2 ) (2) (2),否则结束算法。
举个例子介绍一下:
一开始我们还是停留在树状数组第 x \rm x x位置上(比如 x \rm x x= 6 \rm 6 6吧),答案一开始为 0 \rm 0 0。
还记得吗,我们在进行"给原数组第 x \rm x x位置的数增加k"这个操作时,把"能管理到 x \rm x x的所有位置"都增加了k。
那么,对于任意一个位置,树状数组里的值就是"它能管理到的所有位置上,原数组的值之和"。
因此我们给答案加上树状数组第 x \rm x x位置的值,这里就得到了 s u m ( 5 , 6 ) \rm sum(5,6) sum(5,6),因为 6 \rm 6 6能管理 [ 5 , 6 ] \rm [5,6] [5,6]
然后给 x \rm x x减去 l o w b i t ( x ) \rm lowbit(x) lowbit(x),得到 4 \rm 4 4。再加上 x \rm x x位置的值,也就是 s u m ( 1 , 4 ) \rm sum(1,4) sum(1,4),因为 4 \rm 4 4能管理 [ 1 , 4 ] \rm [1,4] [1,4]
再让 x \rm x x= x \rm x x- l o w b i t ( x ) \rm lowbit(x) lowbit(x),得到0,由于不再大于0,算法终止,得到答案。
这时答案恰好是 s u m ( 1 , 6 ) \rm sum(1,6) sum(1,6),哈哈~
依然可以证明,最多只需要进行 l o g \rm log log级别次数的查询。
这样我们进行操作B的时间复杂度也是 l o g \rm log log级别了。
至此,树状数组就说完了,问题1也成功得到解决,时间复杂度 O ( ( m A + m B ) ∗ l o g n ) \rm O((mA+mB)*logn) O((mA+mB)∗logn)。
在 10000 \rm 10000 10000这个数量级下明显比之前的 O ( m A + ( m B ∗ n ) ) \rm O(mA+(mB*n)) O(mA+(mB∗n))小得多。
而且,位运算的常数非常小,因此整个算法执行速度会很快。
问题2怎么办?用差分的方法,区间 [ l , r ] \rm [l,r] [l,r]所有值+ k \rm k k改成"位置 l \rm l l加上 k \rm k k,位置 r \rm r r+ 1 \rm 1 1减去k"
查询的时候直接查询 s u m ( 1 , x ) \rm sum(1,x) sum(1,x)就行,不理解的话可以自己构造一组数据尝试一下。
注:本篇博客选自《树状数组详细讲解,不会算法也能看懂哦~》,有删减,已经过优快云的优化处理