树状数组的基本应用:
O ( log n ) O(\log n) O(logn)单点修改、区间查询
原理:
设区间 [ 1 , R ] [1,R] [1,R],对区间右端点 R R R做二进制拆分,有:
R = 2 x 1 + 2 x 2 + ⋯ + 2 x k R=2^{x_1}+2^{x_2}+\cdots+2^{x_k} R=2x1+2x2+⋯+2xk
假设 x 1 ∼ x k x_1\sim x_k x1∼xk严格单调递减,那么可以把区间 [ 1 , R ] [1,R] [1,R]拆分成 log R \log R logR个区间
[ 1 , 2 x 1 ] , [ 2 x 1 + 1 , 2 x 1 + 2 x 2 ] , ⋯ , [ 2 x 1 + 2 x 2 + ⋯ + 2 x k − 1 + 1 , 2 x 1 + 2 x 2 + ⋯ + 2 x k ] [1,2^{x_1}],\\ [2^{x_1}+1,2^{x_1}+2^{x_2}],\\ \cdots, \\ [2^{x_1}+2^{x_2}+ \cdots + 2^{x_{k-1}}+1,2^{x_1}+2^{x_2}+\cdots+2^{x_k}] [1,2x1],[2x1+1,2x1+2x2],⋯,[2x1+2x2+⋯+2xk−1+1,2x1+2x2+⋯+2xk]
可以发现每个区间的长度就等于每个区间结尾的 lowbit \text{lowbit} lowbit,所以可以建立一个数组 t r tr tr,保存区间 [ R − lowbit ( R ) + 1 , R ] [R-\text{lowbit}(R)+1,R] [R−lowbit(R)+1,R]的和,也就是树状数组。
查询区间和:
对区间 [ L , R ] [L,R] [L,R]只需要求出 ∑ i = 1 R − ∑ i = 1 L − 1 \sum_{i=1}^{R}-\sum_{i=1}^{L-1} ∑i=1R−∑i=1L−1。
所以目标只要计算区间 [ 1 , i ] [1,i] [1,i]的和:设 i i i的二进制下的最后一位 1 1 1是第 k k k位,那么只需要求出 k − 1 k-1 k−1个子节点的和加上 t r [ i ] tr[i] tr[i],访问每个子节点只需要减去 lowbit ( i ) \text{lowbit}(i) lowbit(i),一共 log i \log i logi次,所以时间复杂度为 O ( log n ) O(\log n) O(logn)
代码:
int ask(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
单点修改
假设令 a i a_i ai增加 c c c,考虑只有 t r [ i ] tr[i] tr[i]及其祖先节点保存 a i a_i ai的值,所以只需要每次加上 lowbit ( i ) \text{lowbit}(i) lowbit(i),就可以一直修改祖先节点,最多 log n \log n logn次,所以时间复杂度为 O ( log n ) O(\log n) O(logn)
代码:
void update(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
树状数组的扩展应用:
1. 1. 1.求某个数前面或后面有几个数比它大或小
分析: 令 t r [ x ] tr[x] tr[x]定义为 x x x出现的次数,那么 ∑ i = L R t r [ i ] \sum_{i=L}^{R} tr[i] ∑i=LRtr[i]就表示在区间 [ L , R ] [L,R] [L,R]中出现的数有多少个,那么相当于在 x x x的数值范围上建立一个树状数组。所以求逆序对时可以倒序统计 i i i之后比 a i a_i ai小的数,每次将 t r [ a i ] + 1 tr[a_i]+1 tr[ai]+1
代码:
#include <bits/stdc++.h>
#define int long long
#define lowbit(x) x & -x
using namespace std;
const int N = 1e6 + 5;
int n, a[N], tr[N], res;
int ask(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
void update(int x, int c) {
for (int i = x; i <= N; i += lowbit(i)) tr[i] += c;
}
signed main() {
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = n; i; i --) {
res += ask(a[i] - 1);
update(a[i], 1);
}
cout << res << endl;
}
分析: 与逆序对一样,在取值范围建立树状数组。求比 a i a_i ai小直接用 a s k ( a i − 1 ) ask(a_i-1) ask(ai−1),求比 a i a_i ai大的数可以用 a s k ( n ) − a s k ( a i ) ask(n)-ask(a_i) ask(n)−ask(ai)这一前缀和技巧处理。
代码:
#include <bits/stdc++.h>
#define int long long
#define lowbit(x) x & -x
using namespace std;
const int N = 2e5 + 5;
int n, a[N], tr[N], res1, res2, high[N], low[N];
void update(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int ask(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
signed main() {
cin >> n;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++) {
high[i] = ask(n) - ask(a[i]);
low[i] = ask(a[i] - 1);
update(a[i], 1);
}
memset(tr, 0, sizeof tr);
for (int i = n; i; i --) {
res1 += high[i] * (ask(n) - ask(a[i]));
res2 += low[i] * ask(a[i] - 1);
update(a[i], 1);
}
cout << res1 << " " << res2 << endl;
}
2. 2. 2.区间修改,单点查询
分析: 可以利用差分的思想,在区间 [ L , R ] [L,R] [L,R]加上某一个数 c c c,那么就是在差分数组 b b b上将 b L + c , b R + 1 − c b_L+c,b_{R+1}-c bL+c,bR+1−c,所以可以用树状数组维护 a i a_i ai的差分。
代码:
#include <bits/stdc++.h>
#define int long long
#define lowbit(x) x & -x
using namespace std;
const int N = 1e5 + 5;
int n, m, a[N], tr[N], l, r, x, d;
char op;
int ask(int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
void update(int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++) update(i, a[i] - a[i - 1]);
while (m --) {
cin >> op;
if (op == 'Q') {
cin >> x;
cout << ask(x) << endl;
} else if (op == 'C') {
cin >> l >> r >> d;
update(l, d), update(r + 1, -d);
}
}
}
3. 3. 3. 区间修改,区间查询
分析: 区间修改可以用差分维护,那么如果查询区间 [ 1 , R ] [1,R] [1,R],就等价于求
∑ i = 1 R ∑ j = 1 i b j = ∑ i = 1 R ( R − i + 1 ) ∗ b i = ( R + 1 ) ∑ i = 1 R b i − ∑ i = 1 R i ∗ b i \sum_{i=1}^{R}\sum_{j=1}^{i}b_j=\sum_{i=1}^{R}(R-i+1)*b_i=(R+1)\sum_{i=1}^{R}b_i-\sum_{i=1}^{R}i*b_i i=1∑Rj=1∑ibj=i=1∑R(R−i+1)∗bi=(R+1)i=1∑Rbi−i=1∑Ri∗bi
所以只需要再增加一个树状数组维护 i ∗ b i i*b_i i∗bi的前缀和即可。
代码:
#include <bits/stdc++.h>
#define int long long
#define lowbit(x) x & -x
using namespace std;
const int N = 1e5 + 5;
int n, m, a[N], l, r, d, tr1[N], tr2[N];
char op;
void update(int tr[], int x, int c) {
for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}
int ask(int tr[], int x) {
int res = 0;
for (int i = x; i; i -= lowbit(i)) res += tr[i];
return res;
}
int sum(int x) {
return ask(tr1, x) * (x + 1) - ask(tr2, x);
}
signed main() {
cin >> n >> m;
for (int i = 1; i <= n; i ++) cin >> a[i];
for (int i = 1; i <= n; i ++) update(tr2, i, i * (a[i] - a[i - 1])), update(tr1, i, a[i] - a[i - 1]);
while (m --) {
cin >> op;
if (op == 'Q') {
cin >> l >> r;
cout << sum(r) - sum(l - 1) << endl;
} else if (op == 'C') {
cin >> l >> r >> d;
update(tr1, l, d), update(tr2, l, d * l);
update(tr1, r + 1, -d), update(tr2, r + 1, (r + 1) * -d);
}
}
}