基本树状数组

简介

树状数组是一种支持在 log ⁡ n \log n logn 的时间内完成 单点修改区间查询 的数据结构,并且它的 代码量极小

事实上,树状数组能解决的问题是线段树能解决的问题的子集:树状数组能做的,线段树一定能做;线段树能做的,树状数组不一定可以。然而,树状数组的 代码要远比线段树短,时间效率常数也更小,因此很有学习价值。

有时,在差分数组和辅助数组的帮助下,树状数组还可解决更强的 区间加单点值区间加区间和 问题。

首先你得知道,普通的树状数组维护的信息一定要满足 结合律 并且 可差分 ,如加法、乘法、异或等。

为什么是这样呢?因为事实上,树状数组维护的是一个类似前缀和的东西,所以维护的信息自然就要满足 结合律 并且 可差分

查询

先考虑查询一个前缀区间 [ 1 , x ] [1,x] [1,x]

首先有一个显然的结论,任意一个数都可以表示成若干个 2的整数次幂 相加的形式。

由此,查询时我们考虑把 [ 1 , x ] [1,x] [1,x] 拆成若干个 长度为2的整数次幂的区间 合并的形式。

现在问题是,怎么划分这些 长度为2的整数次幂的区间

我们先考虑怎么找到这些区间的长度,这显然就简单了,转换为 二进制 就一目了然了。

举个例子, n = 10 n=10 n=10 时,转换成二进制就是 1010 ,很快能找到两个长度 1 0 2 10_2 102 100 0 2 1000_2 10002 x 2 x_2 x2 这表示在二进制下的 x x x)。

所以我们只要在 [ 1 , n ] [1,n] [1,n] 内酌情划分2个长度为 1 0 2 10_2 102 100 0 2 1000_2 10002 的区间就好了。

树状数组是这样划分的,它会把二进制的 从右往左看 ,每看到一个 1 就按照往左划分一个长度为这个 1 对应的位权的区间。

如,当 n = 11 n=11 n=11 时,它是这样划分成了这下三个区间 [ 11 , 11 ] , [ 9 , 10 ] , [ 1 , 9 ] [11,11],[9,10],[1,9] [11,11],[9,10],[1,9]

那么怎样找到这些被划分的区间呢?

t r x tr_x trx 管辖区间 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [xlowbit(x)+1,x] ,其中 l o w b i t ( x ) lowbit(x) lowbit(x) 表示 x x x二进制下从右往左看第一个 1 的位权

怎么计算 l o w b i t ( x ) lowbit(x) lowbit(x) 呢?由位运算知识可得 lowbit(x)=x&-x

原理如下,摘自 oiwiki

x 的二进制所有位全部取反,再加 1,就可以得到 -x 的二进制编码。例如,6 的二进制编码是 110,全部取反后得到 001,加 1 得到 010

设原先 x 的二进制编码是 (...)10...00,全部取反后得到 [...]01...11,加 1 后得到 [...]10...00,也就是 -x 的二进制编码了。这里 x 二进制表示中第一个 1x 最低位的 1

(...)[...] 中省略号的每一位分别相反,所以 x & -x = (...)10...00 & [...]10...00 = 10...00,得到的结果就是 lowbit

看完以上内容,你应该知道怎么查询了,从最右边的区间往左跳一下合并不就好了嘛。

其中 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [xlowbit(x)+1,x] 的左边一个区间不就在 t r x − l o w b i t ( x ) tr_{x-lowbit(x)} trxlowbit(x) 里嘛。

至于边界就是 x = 0 x=0 x=0

int getsum(int x){ //前缀区间和[1,x]
    int res = 0;
    for(; x > 0; x -= lowbit(x)) res += tr[x];
    return res;
}

现在考虑查询一个区间 [ l , r ] [l,r] [l,r] ,因为维护的是 前缀区间并且满足结合律,所以结果就是 getsum(r)-getsum(l-1)

修改

考虑将 a a a 数组的第 x x x 个数加上某个数。

我们的目标是快速正确地维护 t r tr tr 数组。为保证效率,我们只需修改 管辖了 a [ x ] a[x] a[x] 的所有的 t r [ y ] tr[y] tr[y] ,因为其他的 t r [ y ] tr[y] tr[y] 显然 没有发生变化

首先我们知道, t r [ x ] tr[x] tr[x] 肯定包含 a [ x ] a[x] a[x] ,而且 t r [ k ] ( k < x ) tr[k](k<x) tr[k](k<x) 肯定不包含 a [ x ] a[x] a[x]

所以可以确定如果 t r [ k ] tr[k] tr[k] 要包含 a [ x ] a[x] a[x] ,那么 x ≤ k x\le k xk

既然都已经知道 t r [ x ] tr[x] tr[x] 包含 a [ x ] a[x] a[x] 了,那么对于 t r [ k ] ( k > x ) tr[k](k>x) tr[k](k>x)你都在 x x x 的右边了 ,你管辖区间的长度至少得大于 l o w b i t ( x ) lowbit(x) lowbit(x) 吧(不理解自己画图)。

显然,往右看,第一个这样的 k = x + l o w b i t ( x ) k=x+lowbit(x) k=x+lowbit(x)

仔细思考,这相当于一个 二进制进位的过程

你现在应该知道怎么修改了, x x x 往左跳再修改一下不就好了嘛。 其中边界显然就是 x > n x>n x>n

void update(int x, int k){ //将第x个元素加上k。
    for(; x <= n; x += lowbit(x)) tr[x] += k;
}

建树

当然你可以用 update n log ⁡ n n\log n nlogn 建树,这里介绍两种 O ( n ) O(n) O(n) 建树法。

以维护前缀和为例

方法一:直接按照 t r tr tr 的定义用前缀和优化。

方法二(类似单点修改):

因为 tr[i+lowbit(i)] 一定包含 tr[i] (原理自行思考qwq)。

  for (int i = 1; i <= n; ++i) {
    tr[i] += a[i];
    int j = i + lowbit(i);
    if (j <= n) tr[j] += tr[i];
  }

树?

Q:所以,这和树有甚么关系???

A:好像没有什么关系

硬要扯,就是这样 (借用oiwiki的图):

树状数组.png

参考资料

oiwiki

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值