<前言>
本次依旧是高手训练专题解析。
但与以往不同的是,这次会附上树状数组基础内容。
本篇为基础内容。
<树状数组>
在此贴出大哥的blog,有更加全面、系统的介绍。
什么是树状数组?怎么用树状数组?树状数组有什么应用?
什么是树状数组?
树状数组 是一种数据结构, 可以在(O(log_2 n ))时间内完成 **修改、查询 **等序列操作。
它可以处理区间、单点加&查询,以及前(后)缀最值。
它利用了(lowbit())运算的一些性质。
观察(1...10)的(mathrm{lowbit})序列:
序号 | (lowbit) | 二进制 |
---|---|---|
1 | 1 | 1 |
2 | 2 | 10 |
3 | 1 | 11 |
4 | 4 | 100 |
5 | 1 | 101 |
6 | 2 | 110 |
7 | 1 | 111 |
8 | 8 | 1000 |
9 | 1 | 1001 |
10 | 2 | 1010 |
容易发现(lowbit)就是在求最低位的1所代表的数。
而我们可以通过补码的性质快速求(lowbit)
#define lowbit(x) (x & (-x))
那么我们可以根据这个性质搞出一棵树(森林),使一些点对应一段区间。
大概长这样:((mathrm{x})连向(mathrm{y})的边代表(mathrm{y + lowbit(y) = x}))
那么我们通过改变一个值的大小来代表改变一段区间的(子树)所有数的大小。
比如点(x)对应(可以理解为一种代表)([x - mathrm{lowbit(x)} + 1, x])一段区间内的数。对这一段区间进行区间加的时候只需要修改x就行了。
那么对于序列({a_i}) ,我们需要维护一个({c_i}),定义如下
即(c_n)为所有子树内(a_i)的和。
如何使用树状数组
我们需要掌握修改、查询等操作
修改
我们进行修改操作的时候,可以选择是进行前缀修改还是后缀修改。
本质上都是利用了树状数组的性质,所以按照习惯来吧。
修改操作之前说过只需要修改某些特定的点就行了。
比如修改([1, 9]) 一段区间。
我们可以先([1, N])修改,再([9 + 1, N])逆修改。
修改之后的树形态:
我们只修改了少数点,而且个数是(mathrm{O(log_2 n)})级别的,数量越大优势越明显。
(mathrm{Code:})
inline void inc(int x, int v) {for(; x <= MAX
N; x += lowbit(x)) c[x] += v;}
查询
那么有人就要问了,这个修改后的形态这么鬼畜,改怎么获得正确的查询信息呢?
我们发现(lowbit)的另一个快乐的性质:
比如上面那个栗子,无论9还是10都可以通过这个运算到达8.
那不是很爽么。单次修改中某个点若在范围内,则其同级子树必有一个被修改。直接通过(lowbit)累计即可。
容易发现
我们可以在(O(log_2n))时间内完成查询。
(mathrm{Code:})
inline int ask(int x) {
int sum = 0;
for (; x; x -= lowbit(x)) sum += c[x];
return sum;
}
一些拓展
比如前缀(后缀)最值,可以通过树状数组快速维护。
代码实现十分简单,只需要将("+") ("-")改成("max") ("min")
Just like:
inline void inc(int x, int v) {for (; x <= MAXN; x += lowbit(x)) c[x] = max(c[x], v);}
//操作这里min也是一样的
inline int ask(int x) {
int maxn = -1;
for (; x; x -= lowbit(x)) maxn = max(maxn, c[x]); //注释同上
return maxn;
}
二维操作&各种基本功能实现详见Pαrsnip的blog
还有比如什么树状数组上二分(倍增)、一些小Trick、简单代替线段树等黑科技。
尽在cz的树状数组黑科技讲义.
树状数组应用
一维/二维/(你想写高维可能也没什么)的 区间/单点修改,区间/单点查询的 区间四则运算/前缀(后缀)最值。
二维偏序,逆序对等,事实上很多偏序问题树状数组是首选。
当然更多的应用无处不在与各个地方。
题目基本是让你求一个序列中所有连续子序列的某个贡献值。
<后记>
为树状数组专题备用基础讲解的blog。