树状数组
前置知识
令 \(\operatorname{lowbit}(x)\) 表示 \(x\) 在二进制下最后一个 \(1\) 的位权(例如 \(\operatorname{lowbit}(6)=\operatorname{lowbit}(110_2)=2\)),那我们该怎么计算呢?
注意:\(\operatorname{lowbit}(x)\) 并不是指位数 \(k\),而是位权 \(2^k\)。
首先我们得知道计算机内部是如何存储数字的(这里以八位二进制为例)。
位号 | \(\color{red}7\) | \(6\) | \(5\) | \(4\) | \(3\) | \(2\) | \(1\) | \(0\) |
---|---|---|---|---|---|---|---|---|
位权 | \(\color{red} -2^7\) | \(2^6\) | \(2^5\) | \(2^4\) | \(2^3\) | \(2^2\) | \(2^1\) | \(2^0\) |
注意在这里最高位上的位权是负的。
举个例子:\(-20\) 在计算机中是 \(11110110_2\)。
可以发现,任何一个数 \(x\) 和 \(-x\) 在计算机中的值作位与运算刚好等于 \(\operatorname{lowbit}(x)\)。
可以使用构造证明。因为 \(x+(-x)=0\),也就是让他们刚好进位到最高位,再进位到前面不存在的那位,最终才会得到 \(0\),也就是说,\(-x\) 的第 \(i\) 位 \(=(x\) 的第 \(i\) 位 \(+\) 上一位的进位 \() \bmod 2\),所以只有 \(\operatorname{lowbit} (x)\) 那一位上 \(x\) 和 \(-x\) 都为一,因为在 \(\operatorname{lowbit} (x)\) 的那一位之前都没有进位。
代码
int lowbit(int x) {
return x & -x;
}
树状数组
简介
树状数组是一种可以 \(O(N)\) 建树,\(O(\log N)\) 单点修改前缀查询的数据结构,但是由于只能查询前缀,所以要维护的信息必须有逆运算并且满足结合律。树状数组能做的线段树都能做,但线段树能做的树状数组不一定能做,不过树状数组相对来说码量较少,常数也更小。
在树状数组中,每个节点都代表一个区间,节点 \(i\) 对应的区间为 \([i-\operatorname{lowbit} (i)+1,i]\),树状数组可以将一段前缀分解成不超过 \(\log N\) 段来求解。
例如当 \(N=8,A=\{3,1,4,1,5,9,2,6\}\) 时树状数组如下:
每个节点都代表一个区间,其中最下面一排为 \(A\),红色为和,绿色为管辖区间,黑色为编号。
建树
首先的问题就是:\(i\) 的父亲(即最小的一个不为 \(i\) 的将 \(i\) 包含的节点)到底是多少?
先证明一些性质。
令 \(l(x)=x-\operatorname{lowbit}(x)+1\),即 \(x\) 的左端点,\(c(x)=[l(x),x]\),即 \(x\) 对应的区间。
性质一
证:对于 \(x \le y\),要么有 \(c(x)\) 和 \(c(y)\) 不交,要么有 \(c(x)\) 包含于 \(c(y)\)。
假设 \(x\) 和 \(y\) 相交,则有 \(l(y) \le x \le y\)。
设 \(y=s \cdot 2^{k+1} + 2^k\),则 \(l(y)=s \cdot 2^{k+1}+1\)。
\(\therefore x\) 可以表示为 \(s \cdot 2^{k+1}+b\),其中 \(1 \le b \le 2^k\)。
\(\therefore \operatorname{lowbit} (x)=\operatorname{lowbit} (b)\)。
又 \(\because b-\operatorname{lowbit}(b) \ge 0\)。
\(\therefore l(x)=x-\operatorname{lowbit}(x)+1=s \cdot 2^{k+1}+b-\operatorname{lowbit}(b)+1 \ge s \cdot 2^{k+1}+1=l(y)\),即 \(l(y) \le l(x) \le x \le y\)。
\(\therefore\) 如果 \(c(x),c(y)\) 相交,则 \(c(x)\) 一定包含于 \(c(y)\)。
性质二
证:\(c(x)\) 真包含于 \(c(x + \operatorname{lowbit}(x))\)。
设 \(y=x+\operatorname{lowbit}(x)\),\(x=s \cdot 2^{k+1}+2^{k}\)
\(\therefore y = (s+1)\cdot 2^{k+1},l(x)=s\cdot 2^{k+1}+1\)
又 \(\because \operatorname{lowbit}(y) \ge 2^{k+1}\)。
\(\therefore l(y)=(s+1) \cdot 2^{k+1}-\operatorname{lowbit}(y)+1 \le s \cdot 2^{k+1}+1=l(x)\),即 \(l(y) \le l(x) \le x \le y\)。
\(\therefore c(x)\) 真包含与 \(c(x + \operatorname{lowbit}(x))\)。
性质三
证:对于任意 \(x < y < x + \operatorname{lowbit}(x)\),有 \(c(x)\) 与 \(c(y)\) 不交。
设 \(x=s \cdot 2^{k+1}+2^{k}\),则 \(y=x+b=s \cdot 2^{k+1}+2^{k}+b\),其中 \(1 \le b < 2^{k}\)。
不难发现 \(\operatorname{lowbit}(y)=\operatorname{lowbit}(b)\)。
又 \(\because b - \operatorname{lowbit}(b)\ge0\)。
\(\therefore l(y)=y-\operatorname{lowbit}(y)+1=x+b-\operatorname{lowbit}(b)+1>x\),即 \(l(x) \le x < l(y) \le y\)。
\(\therefore c(x) 与 c(y)\) 不交。
结论
由于上面这些性质,我们很容易得到 \(i\) 的父亲就是 \(i+\operatorname{lowbit}(i)\)。
所以直接累加答案即可。
时间复杂度 \(O(N)\)。
代码
for(int i = 1; i <= n; ++i) {
tr[i] += a[i];
tr[i + lowbit(i)] += tr[i];
}
前缀查询
由于第 \(i\) 个节点的管辖区间是 \([i-\operatorname{lowbit}(i)+1,i]\),所以如果令 \(g(i)=\sum \limits_{i=1}^i A_i\),则 \(g(i)\) 还可以写作 \(g(i)=f_i+g(i-\operatorname{lowbit}(i))\)。
由于在二进制下最多有 \(\log N\) 位,每次减去 \(\operatorname{lowbit}(i)\),所以总时间复杂度为 \(O(\log N)\)。
代码
int getsum(int p) {
int sum = 0;
for(; p; sum += tr[p], p -= lowbit(p)) {
}
return sum;
}
getsum(r) - getsum(l - 1);
单点修改
单点修改也很简单,由于 \(i\) 的父亲是 \(i+\operatorname{lowbit}(i)\),所以将修改的节点的所有祖先累加答案即可。
由于每次 \(+\operatorname{lowbit}(i)\),至少使 \(\operatorname{lowbit}(i)\times 2\),即进一位,所以单词修改时间复杂度 \(O(\log N)\)。
代码
void update(int p, int x) {
for(; p <= n; tr[p] += x, p += lowbit(p)) {
}
}
update(p, x);
区间修改、单点查询
实际上就是把树状数组当成差分数组罢了。
代码
for(int i = 1; i <= n; ++i) {
cin >> a[i];
update(i, a[i]), update(i + 1, -a[i]);
}
update(l, x), update(r + 1, -x);
getsum(p);
代码
区间查询、单点修改
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 200001;
int n, q, a[MAXN], tr[MAXN];
int lowbit(int x) {
return x & -x;
}
void build() {
for(int i = 1; i <= n; ++i) {
tr[i] += a[i];
tr[i + lowbit(i)] += tr[i];
}
}
void update(int p, int x) {
for(; p <= n; tr[p] += x, p += lowbit(p)) {
}
}
int getsum(int p) {
int sum = 0;
for(; p; sum += tr[p], p -= lowbit(p)) {
}
return sum;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
}
build();
cin >> q;
for(int i = 1, op, p, x, l, r; i <= q; ++i) {
cin >> op;
if(op == 1) {
cin >> p >> x;
update(p, x);
}else {
cin >> l >> r;
cout << getsum(r) - getsum(l - 1) << "\n";
}
}
return 0;
}
区间修改、单点查询
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 200005;
int n, q, a[MAXN], tr[MAXN];
int lowbit(int x) {
return x & -x;
}
void update(int p, int x) {
for(; p <= n; tr[p] += x, p += lowbit(p)) {
}
}
int getsum(int p) {
int sum = 0;
for(; p; sum += tr[p], p -= lowbit(p)) {
}
return sum;
}
int main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) {
cin >> a[i];
update(i, a[i]), update(i + 1, -a[i]);
}
cin >> q;
for(int i = 1, op, p, x, l, r; i <= q; ++i) {
cin >> op;
if(op == 1) {
cin >> l >> r >> x;
update(l, x), update(r + 1, -x);
}else {
cin >> p;
cout << getsum(p) << "\n";
}
}
return 0;
}