树状数组是一种非常好打优美的数据结构,适用性十分广泛,而且常数非常小。
(自己花五分钟做的图)
下面蓝色的是我们的原数组,红色的即是我们的 $tree$ 数组。
树状数组的主旨是单点修改,查询前缀和,它的核心是 $\rm lowbit$
如 $\rm lowbit((111)_2) = (001)_2,\rm lowbit((110)_2) = (010)_2$
即 $\rm lowbit(7) = 1,\rm lowbit(6) = 2$
仔细观察原树,不难发现:
$$tree[i]=\sum\limits_{j=i-lowbit(i)+1}^i a[j]$$
于是我们所求的前缀和可以由 $tree$ 数组得出
$$sum[i] = tree[i] + tree[i - lowbit(i)] + tree[(i - lowbit(i)) - lowbit(i - lowbit(i))] ...$$
举个栗子:
$$sum[(111)_2] = tree[(111)_2] + tree[(110)_2] + tree[(100)_2]$$
这里需要仔细看几遍才能理解。
于是查询前缀和的操作就写好了。
1 | ll query(int p) // 查询前缀和 |
接下来考虑单点修改
当 $a[i]$ 改变的时候,所有包含 $i$ 这个位置的 $tree[j]$ 均要改变。
树状数组是一棵树!
于是我们只需要找到 $tree[i]$ 的父亲,以及它的父亲的父亲爷爷...逐个更新
而再次仔细观察,我们惊奇的发现:
节点 $i$ 的父亲编号即为 $i + \rm lowbit(i)$ !
1 | void add(int p, ll x) // 单点修改 |
单点修改 + 区间查询
知道前缀和很容易计算出区间和。
区间 $[l,r]$ 的和即为 $sum[r] - sum[l-1]$ 。
1 | void add(int p, ll x) |
区间修改 + 单点查询
这里我们要用到神奇的差分。
新建 $d$ 数组。
$$d[1]=a[1],d[i]=a[i]-a[i-1] (2\le i \le n)$$
可以得出:
$$a[p]=\sum\limits_{i=1}^{p}d[i]$$
这是啥?不就是前缀和嘛!
而当区间 $[l,r]$ 全部加上 $x$ 时,我们只需将 $d[l]$ 加上 $x$ ,$d[r + 1]$ 减去 $x$ 就好了。
1 | void add(int p, ll x) |
区间修改 + 区间查询
这是最常用的算法,第一次看到的时候真的惊艳到我了。
$$sum[p]=\sum\limits_{i=1}^p a[i] = \sum\limits_{i=1}^p \sum\limits_{j=1}^i d[j]$$
仔细看,发现 $d[1]$ 加了 $p$ 次, $d[2]$ 加了 $p-1$ 次, $d[3]$ 加了 $p-2$ 次...
于是可以继续推:
$$sum[p]=\sum\limits_{i=1}^p d[i] \times (p - i + 1)$$
$$=(p+1)\times\sum\limits_{i=1}^p d[i] - \sum\limits_{i=1}^p d[i] \times i$$
于是我们只需要同时维护 $\sum\limits_{i=1}^p d[i]$ 与 $\sum\limits_{i=1}^p d[i] \times i$ 便可以了。
1 | void add(int p, ll x) |