Loading

树状数组

简介

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

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

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

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

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

查询

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

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

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

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

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

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

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

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

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

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

\(tr_x\) 管辖区间 \([x-lowbit(x)+1,x]\) ,其中 \(lowbit(x)\) 表示 \(x\)二进制下从右往左看第一个 1 的位权

怎么计算 \(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-lowbit(x)+1,x]\) 的左边一个区间不就在 \(tr_{x-lowbit(x)}\) 里嘛。

至于边界就是 \(x=0\)

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

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

修改

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

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

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

所以可以确定如果 \(tr[k]\) 要包含 \(a[x]\) ,那么 \(x\le k\)

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

显然,往右看,第一个这样的 \(k=x+lowbit(x)\)

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

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

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

建树

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

以维护前缀和为例

方法一:直接按照 \(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

posted @ 2023-04-16 13:18  szy_dxf  阅读(35)  评论(0)    收藏  举报