树状数组的基本应用:

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 x1xk严格单调递减,那么可以把区间 [ 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++2xk1+1,2x1+2x2++2xk]

可以发现每个区间的长度就等于每个区间结尾的 lowbit \text{lowbit} lowbit,所以可以建立一个数组 t r tr tr,保存区间 [ R − lowbit ( R ) + 1 , R ] [R-\text{lowbit}(R)+1,R] [Rlowbit(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=1Ri=1L1

所以目标只要计算区间 [ 1 , i ] [1,i] [1,i]的和:设 i i i的二进制下的最后一位 1 1 1是第 k k k位,那么只需要求出 k − 1 k-1 k1个子节点的和加上 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.求某个数前面或后面有几个数比它大或小

AcWing 788 逆序对的数量

分析: 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;
}

AcWing 241 楼兰图腾

分析: 与逆序对一样,在取值范围建立树状数组。求比 a i a_i ai小直接用 a s k ( a i − 1 ) ask(a_i-1) ask(ai1),求比 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.区间修改,单点查询

AcWing 242 一个简单的整数问题

分析: 可以利用差分的思想,在区间 [ 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+1c,所以可以用树状数组维护 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. 区间修改,区间查询

AcWing 243 一个简单的整数问题2

分析: 区间修改可以用差分维护,那么如果查询区间 [ 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=1Rj=1ibj=i=1R(Ri+1)bi=(R+1)i=1Rbii=1Ribi

所以只需要再增加一个树状数组维护 i ∗ b i i*b_i ibi的前缀和即可。

代码:

#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);
        }
    }
}