写在前面
话说挑个阴间时间发就没人看了罢(
这篇文章真的废了我很多心血,接下来几个月上学,可能没有很多时间写博客,所以难免会出现一些错误和讲得不够详细的地方,欢迎大家评论指出!
1 为什么要用树状数组
1.1 树状数组的用处
在做题的时候,经常会遇到“单点修改+区间查询”“区间修改+单点查询”“求逆序对个数”之类的问题。
如果只用一般的思路去写,很容易写出 的做法——对于每个询问,遍历询问区间进行修改/查询。
但是当数据量加大的时候,就只能用树状数组优越的小常数以及 的时间复杂度进行优化。
1.2 树状数组?线段树?
另一种数据结构“线段树”与树状数组的时间复杂度大体相似,但我不会线段树
线段树的常数比树状数组略大一些,而且它的代码量更大,空间复杂度更大(树状数组线段树
)
不过它能解决的问题比树状数组多一点,但是树状数组也可以解决大部分线段树问题才怪
2 树状数组原理
2.1 思想
树状数组本质其实就是在维护一个前缀和/差分。
在“单点修改+区间查询”和求逆序对的时候用的就是前缀和的思想,“区间修改+单点查询”也要用到差分思想,所以不会前缀和和差分的点这里
2.2 树?数组?
为什么把“数组”和“树”两个相反的词放在一起呢?
其实树状数组从实现上看是一个数组,它的定义是:
1) int lowbit(int x) 返回x转为二进制后最后一个1的位置所代表的数值.
e.g. =
所以返回
=
2) 定义tree数组为a数组的树状数组 tree[i]=a[i-lowbit(i)+1]+a[i-lowbit(i)+2]+...+a[i]
即
为什么说这是个树呢?请看VCR:
图中t[i]就是前面的tree[i],tree[i]对应绿框表示tree[i]是哪些a[j]的和
不难发现如下事实
(唯二难点)
2.3 更新:修改
由上面的式子,我们可以得出以下结论:
当更新a[i]时,需且仅需更新tree[i],tree[i+lowbit(i)],tree[i+lowbit(i)+lowbit(i+lowbit(i))],...
这个很明显可以用循环来写,单点修改就是这么实现的;
当问题变为区间修改时,我们更新[L,R]区间,就是更新差分数组b的第L项和第(R+1)项,所以
此时将tree数组用来维护原数组的差分数组b就可以只更新b[L]和b[R+1]
方法同上个粗体字
2.4 前缀:查询
大家还记得树状数组的定义吗?
定义tree数组为a数组的树状数组 tree[i]=a[i-lowbit(i)+1]+a[i-lowbit(i)+2]+...+a[i]
那么这个定义如何帮助我们呢?
(真)不难发现
tree[i]+tree[i-lowbit(i)]+tree[i-lowbit(i)-lowbit(i-lowbit(i))]+... = a[1]+a[2]+...+a[i] (唯二难点)
也可以用循环来写,这就是前缀和;
查询[L,R]的区间和就是a[1~R]前缀减去a[1~L-1]前缀
而“区间修改+单点查询”呢?由于维护的是差分数组,所以a[i]就是b[1~i]前缀
3 代码实现
知识点:“单点查询+区间修改”“单点修改+区间查询”
3.1 lowbit函数
3.1.1 前置知识
3.1.2 具体实现
考虑一个数 x 在内存空间中被表示为以下二进制数:
其中的1就是lowbit函数应该返回的数值,最前面的0是符号位,表示正数
那么它的反码就是以下负数
其中前面省略部分与原码完全相反
补码(反码-1)
前面省略部分也与原码完全相反
考虑此时原码位与补码的结果:
这就是lowbit函数的底层实现
代码如下:
int lowbit(int x) {
return x & (-x);
}
3.2 更新
3.2.1 单点更新
上面原理部分已经讲过了,比较简单
代码如下:(下面几个代码都有点微抽象,敬请谅解)
void upd(int x, int num) { // 给x这个位置的数加上num
for(; x <= n; tree[x] += num, x += lowbit(x));
}
3.2.2 区间更新(配单点查询)
就是差分数组单点更新(
代码如下:
void upd(int l, int r, int num) { // 给[l,r]区间的数加上num
for(; l <= n; tree[l] += num, l += lowbit(l));
for(r ++; r <= n; tree[r] -= num, r += lowbit(r));
}
3.3 查询
3.2.1 区间查询(配单点更新)
查询[L,R]区间和就是R前缀减去L-1前缀
代码如下:
int ask(int x, int ret = 0) { // a[1]到a[x]的前缀(我可以好好声明ret的,但我拒绝(笑
for(; x; ret += tree[x], x -= lowbit(x));
return ret;
}
int query(int l, int r) { // 查询[l,r]区间和
return ask(r) - ask(l - 1);
}
3.2.2 单点查询
就是上面的ask(x),我不重复写了
4 一些树状数组的小拓展
4.1 二维树状数组
4.1.1 单点修改+区间查询
在2.1 思想那节,我说过树状数组本质上就是去维护前缀和/差分。当问题变为二维数组时,我们其实就是在维护二维前缀和/差分。此时:
tree[i][j]表示a[i-lowbit(i)+1][j-lowbit(j)+1]到a[i][j]共(i-lowbit(i))(j-lowbit(j))个元素的和
那么更新和查询代码都要有略微修改:
void upd(int x, int y, int num) { // (x,y)元素增加num
for(; x <= n; x += lowbit(x))
for(; y <= n; tree[x][y] += num, y += lowbit(y));
}
int ask(int x, int y, int ret = 0) { // 二维前缀和
for(; x; x -= lowbit(x))
for(; y; ret += tree[x][y], y -= lowbit(y));
return ret;
}
int query(int lux, int luy, int rbx, int rby) { // 二维前缀和典型套路(容斥原理)
return (lux --, luy --, ask(rbx, rby) - ask(rbx, luy) - ask(lux, rby) + ask(lux, luy));
}
4.1.2 区间修改+单点查询
这里涉及到二维差分
区间修改只需要修改四至点,单点查询仍然是求差分数组前缀和
void add(int x, int y, int num) {
for(; x <= n; x += lowbit(x))
for(; y <= n; tree[x][y] += num, y += lowbit(y));
}
void upd(int lux, int luy, int rbx, int rby, int num) {
add(lux, luy, num);
add(lux, ++ rby, -num);
add(++ rbx, luy, -num);
add(rbx, rby, num);
}
4.2 求逆序对
这里tree[i]表示当前已考虑范围中有多少个在(i-lowbit(i), i]区间的值,保持了树状数组的模样
但是如何不重复地求出逆序对个数呢?考虑以下推导:
对于a[i],a[j]<=a[i]且j<=i的节点个数为ask(a[i]);
对于a[i],a[j]>a[i]且j<=i的节点个数为i-ask(a[i]),即以a[i]为第二项的逆序对数量
那么总的逆序对数量即为
代码如下:
// . . .
void upd(int x) {
for(; x <= n; tree[x] ++, x += lowbit(x));
}
int ask(int x, int ret = 0) {
for(; x; ret += tree[x], x -= lowbit(x));
return ret;
}
int main() {
// . . .
for(int i = 1; i <= n; i ++) {
cin >> a[i];
upd(a[i]); // 思考为什么要在更新答案之前把a[i]加入考虑范围内
ans += i - ask(a[i]);
}
cout << ans;
}
4.3 区间修改+区间查询
4.3.1 一维
这部分用公式来讲比较显而易见
首先考虑区间修改,肯定要用tree数组来维护差分数组:
设d[]表示原数组的差分数组,tree[i]表示d的树状数组
那么区间修改就和上面3.2.2 区间更新(配单点查询)的代码无异了
区间查询比较难想,我们用公式来推:
观察到所用的前缀和一个是d[i],一个是i*d[i],用两个树状数组维护这两个量即可
代码如下:
int tree1[N], tree2[N]; // tree1 存储 d[i]; tree2 存储i*d[i]
void upd_single(int x, int num) { // 单点进行更新
for(int i = x; i <= n; tree1[i] += num, tree2[i] += x * num, i += lowbit(i));
}
void upd(int l, int r, int num) { // 区间更新
upd_single(l, num);
upd_single(++ r, -num);
}
int ask(int x, int ret = 0) { // 查询前缀和
for(int i = x; i; ret += (x + 1) * tree1[i] - tree2[i], i -= lowbit(i));
return ret;
}
int query(int l, int r) { // 查询[l,r]区间和
return ask(r) - ask(l - 1);
}
4.3.2 二维
偶不会
模仿上面一维的思路,先考虑区间修改(同4.1.2 区间修改+单点查询一致)
再考虑前缀和:
所!以!需要维护j*d[i][j],i*d[i][j],i*j*d[i][j]和d[i][j]的树状数组
代码如下:(有待填坑)