浅谈树状数组

时间戳

对于区间更改和查询问题,之前我是一直用线段树解决的,这种结构真的很优美很强大,至于树状数组这个东西,我看到“树状数组能做的线段树都可以做,线段树能做的树状数组却做不了”类似的话以后就没当回事。
再往后是做过几道优化查询的问题,用线段树T了,改成树状数组过了,时间效率高出线段树数倍(丝毫不夸张,尽管复杂度相同),才开始关注这个结构,事实上线段树的常数是相当大的,如果题目只是优化查询,不涉及lazy标记等动态性较强问题,还是建议使用树状数组的。
之后又碰到了二维问题,二维线段树不会写。。。幸好问题是涉及异或的,用树状数组过了,发现这个结构其实还是满强大的,易实现(尽管不易理解),高效率,省空间。。。
趁热做个小总结吧。


一维树状数组


基本用法:单点更新、区间查询

代码网上很好找,大把多。。。这里只说一些需要注意的地方。
树状数组的区间是从1开始的,而且是左闭右闭区间[1, n],用起来相当方便。
add(x, val, n)是对a[x]增加val的操作。
get(x)是对a[1]到a[x]的所有值求和,即a[1, x]。
注意这边x都是>=1的。

也就是说树状数组解决的基本问题是单点更新和区间查询,当然如果你想查询[l ,r]的和也容易,if(l == 1) return get(r); else return get(r) - get(l - 1); 即可。


用法延伸:区间更新,单点查询

有个关于前缀和的问题,当我们接受连续多组区间更改,仅在最后进行多组区间查询(求和)的时候,我们可以均摊到每次操作O(1)内解决,这就是利用差分。
对序列a的每次区间[l, r]的更改,只记录d[l] += val,d[r + 1] -= val即可,因为更改是连续的,当所有更改完毕以后,我们对d求一个前缀和,并且d[i]就是a[i]的delta值,和a[i]求和就得到了序列a的最终状态。再对序列a求一遍前缀和sum,我们就可以应对连续的多组查询了。
这其实是一个离线算法,要求一次性处理完多组更新,之后再处理多组查询,对于更新查询交替出现的情况就捉襟见肘了,但是树状数组可以解决这个问题。

树状数组可以利用差分做到区间更新和单点查询,当你要对区间[l, r]进行更改时,可以add(l, val, n); add(r + 1, -val, n); 当要查询单点的时候get(x)就得到了变更的delta值,加以原值就是最终结果。
树状数组可以很好的维护前缀和,这是它最大的威力。


二维树状数组


一维的拓展

树状数组可以拓展至二维情况,并且在二维区间内解决相同类型的问题:
add(x, y, val, n)可以对a[x][y]进行增减,get(x, y)可以对对角线为(1, 1)、(x, y)的矩阵求和,如果需要对对角线为(x0, y0)、(x1, y1)的矩阵求和(x0 <= x1、y0 <= y1),只需要return get(x1, y1) - get(x0 - 1, y1) - get(x1, y0 - 1) + get(x0 - 1, y0 - 1)即可。这里的参数为0也无所谓,因为会返回0。

当然区间更新和单点查询也可以达到。
对对角线为(x0, y0)、(x1, y1)的矩阵进行更改,需要add(x0, y0, val, n); add(x0, y1 + 1, -val, n); add(x1 + 1, y0, -val, n); add(x1 + 1, y1 + 1, val, n); 即可,画个图还是很好理解的。单点查询只需get(x, y)。
很好很强大,这个结构我打包带走。


异或问题结合

先总结一下基本功能,树状数组,无论一维还是二维,擅长的问题有两类:一类是单点更新,区间求和;一类是区间更新,单点查询,这里的更新我们指的是加减法。接下来可以看到对于异或这种特殊运算,树状数组的表现更加强大。

现在给出一个问题,有一个序列,初始值均为0,需要支持两种操作,一种xor l r val,使区间[l, r]的每个元素对val取异或;另一种操作query l r输出区间[l, r]所有元素的异或值。

这里的功能是区间更新和区间查询,线段树很好做,打个lazy标记就行,树状数组解决得了么?当然可以。

(我们先考虑一个问题,如果把区间查询功能query l r改成单点查询query x,即查询第x个元素的值,树状数组可做么?这个当然是可以的,跟加减没区别,换下符号就ok。)

考虑一个子问题,求区间[1, x]内所有元素异或值之和。
对于区间更改,一个直观的想法是对[l, r]进行xor操作,只对区间两端点l和r+1取异或,查询的时候get(x)即可。但是这样是不对的,异或操作和加减法不同,当你对l位置增加一个异或元素,它只会影响到[l, n]当中和l位置奇偶性相同的位置,对于x(l <= x <= r),如果[l, x]中元素个数为偶数,则异或效应抵消,反之生效。也就是x和l奇偶性相同时,区间[l, x]中元素个数为奇数,异或因子个数也是奇数,这时候这个异或因子才对答案有影响。如果x > r呢,如果r和l的奇偶性相同,则异或无效,如果l和r奇偶性不同,如果x > r,则一定会算上这个异或因子的。所以关键就在于[1, x]中每类异或因子的个数。

具体做法是这样的,c[2][maxn],在add(x, val, n)内,只对c[x&1][i]进行异或,get(x)内,也只对c[x&1][i]进行异或,结合上述的说明可以发现这种做法适用于所有情况。

十分巧妙,因为异或运算的性质,异或同一个值两次与没有异或等同,所以我们只需要在异或因子的个数的奇偶性上下工夫,记录一个[1, x]区间对每个异或因子的贡献,进而解决问题。

我们看看同样的问题拓展至二维怎么解决,xor x0 y0 x1 y1 val对整个矩阵的元素均取异或,query x0 y0 x1 y1求整个矩阵元素的异或值。
对add(x, y, val, n):
在add操作时,显然一个与x奇偶性相同与y奇偶性相同的矩阵区间(1, 1)、(x’, y’)与(1, 1)、(x, y)矩阵区间的交集含有奇数个元素,所以相应的异或因子贡献也是奇个的(奇数 x 奇数 = 奇数)。
所以只要分别对(x, y)这一数对的奇偶性分类就可以了,应为4个树状数组c[2][2][maxn][maxn]。


小结

事实上树状数组的原理不是十分好理解,它的英文名叫做BIT,“二进制索引树”,它利用索引的二进制对区间划分,巧妙得进行合并。
lowbit(x)即为x&-x,它取到的是x二进制最右侧的以1开头,其后都是0的一个串,当x -= lowbit(x)时,得到的新x是前向区间的索引,x += lowbit(x)时,得到的是后向区间的索引,由于任何一个整数的二进制最多有logn个这样的区间,所以无论求和还是维护都可在logn内解决。
盗一发度娘的图

尽管树状数组功能有限,但是对于加速查询这样的工具性功能已经是完全没问题了。更复杂的操作或者运算可能需要二维线段树/主席树去解决。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值