树状数组 修改及查询操作

树状数组 修改及查询操作

常见问题:高效率地查询和维护前缀和(或区间和)。

如果数列为静态的,预处理前缀和即可。

如果数列为动态的,改变任意一个元素 \(a_k\) 的值,都会影响后续前缀和的值。

树状数组可以有效解决此类问题。


lowbit操作

lowbit就是对于十进制数 \(x\) ,有

int lowbit(x) {
    return x & -x;
}

比如 lowbit(20)

	1  0  1  0  0   <-   20
 &  0  1  1  0  0   <-  -20 = 20各位取反 + 1
 ------------------
    0  0  1  0  0   <-   lowbit(20) = 4

可以发现,lowbit操作就是找到 \(x\) 的二进制数的最后一个1,其余全抹成0

该操作是为了爬链,下面的图展示了树状数组修改和查询。

参考:【董晓算法 C81【模板】树状数组 点修+区查 区修+点查】https://www.bilibili.com/video/BV17N4y1x7c6?vd_source=d99da713618691ba36ec8e0d718ce6e7


单点修改 + 区间查询

P3374 【模板】树状数组 1

题目描述

如题,已知一个数列,你需要进行下面两种操作:

  • 将某一个数加上 \(x\)

  • 求出某区间每一个数的和

输入格式

第一行包含两个正整数 \(n,m\),分别表示该数列数字的个数和操作的总个数。

第二行包含 \(n\) 个用空格分隔的整数,其中第 \(i\) 个数字表示数列第 \(i\) 项的初始值。

接下来 \(m\) 行每行包含 \(3\) 个整数,表示一个操作,具体如下:

  • 1 x k 含义:将第 \(x\) 个数加上 \(k\)

  • 2 x y 含义:输出区间 \([x,y]\) 内每个数的和

输出格式

输出包含若干行整数,即为所有操作 \(2\) 的结果。

输入输出样例 #1

输入 #1

5 5
1 5 4 2 3
1 1 3
2 2 5
1 3 -1
1 4 2
2 1 4

输出 #1

14
16

说明/提示

【数据范围】

对于 \(30\%\) 的数据,\(1 \le n \le 8\)\(1\le m \le 10\)
对于 \(70\%\) 的数据,\(1\le n,m \le 10^4\)
对于 \(100\%\) 的数据,\(1\le n,m \le 5\times 10^5\)

数据保证对于任意时刻,\(a\) 的任意子区间(包括长度为 \(1\)\(n\) 的子区间)和均在 \([-2^{31}, 2^{31})\) 范围内。

样例说明:

故输出结果14、16

代码

// 树状数组单点修改
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int n, m;
const int N = 5e5 + 5;
int a[N];
int s[N];

int lowbit(int x) {
    return x & -x;
}

void update(int x, int k)// 更新单点
{
    while (x <= n) {s[x] += k; x += lowbit(x);}
}

int sum(int x)// 输出前缀和
{
    int t = 0;
    while (x) {t += s[x], x -= lowbit(x);}
    return t;
}


int main()
{
    cin >> n >> m;
    int op, x, y;
    // 初始化
    for (int i = 1; i <= n; i++) {
        int a; cin >> a;
        update(i, a);
    }
    // m次操作
    for (int i = 1; i <= m; i++) {
        cin >> op >> x >> y;;
        if (op == 1) update(x, y);
        else cout << sum(y) - sum(x - 1) << endl;
    }


    return 0;
}

区间修改 + 单点查询

[242 AcWing] 一个简单的整数问题

题目描述

给定长度为 \(N\) 的数列 \(A\),然后输入 \(M\) 行操作指令。

第一类指令形如 C l r d,表示把数列中第 \(l \sim r\) 个数都加 \(d\)

第二类指令形如 Q x,表示询问数列中第 \(x\) 个数的值。

对于每个询问,输出一个整数表示答案。

输入格式

第一行包含两个整数 \(N\)\(M\)

第二行包含 \(N\) 个整数 \(A[i]\)

接下来 \(M\) 行表示 \(M\) 条指令,每条指令的格式如题目描述所示。

输出格式

对于每个询问,输出一个整数表示答案。

每个答案占一行。

数据范围

\(1 \le N,M \le 10^5\),
\(|d| \le 10000\),
\(|A[i]| \le 10^9\)

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2

输出样例:

4
1
2
5

思路

我们知道直接对数组 \(a[i]\) 求前缀和是求区间和,那么对差分数组 \(D[i]\) 求前缀和就是求 \(a[i]\) 本身。

比如说给定区间 $[l, r] $,在区间内每个数加上 \(d\),那么就直接把差分数组 \(D[l]\) 加上 \(d\)\(D[r+1]\) 减去 \(d\)

这样既保证了区间内的修改,有保证了区间外的稳定性。

但可以发现,没有用到差分数组,直接修改这一个区间的前缀和,实际一样的效果。

最后单点查询,直接输出该点所对应差分数组的前缀和就行。

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
int a[N], s[N];
int n, m;

int lowbit(int x)
{
    return x & -x;
}

void update(int x, int d)
{
    while (x <= n) s[x] += d, x += lowbit(x);
}

int sum(int x)
{
    int t = a[x];// 注意这里t初始为a[x]
    while (x) t += s[x], x -= lowbit(x);
    return t;
}

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    while (m--) {
        char c; cin >> c;
        if (c == 'C') {
            int l, r, d; cin >> l >> r >> d;
            update(l, d);
            update(r + 1, -d);
        }
        else {
            int x; cin >> x;
            cout << sum(x) << endl;
        }
    }
    return 0;
}

区间修改 + 区间查询

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

题目描述

给定一个长度为 \(N\) 的数列 \(A\),以及 \(M\) 条指令,每条指令可能是以下两种之一:

  1. C l r d,表示把 \(A[l],A[l+1],…,A[r]\) 都加上 \(d\)
  2. Q l r,表示询问数列中第 \(l \sim r\) 个数的和。

对于每个询问,输出一个整数表示答案。

输入格式

第一行两个整数 \(N,M\)

第二行 \(N\) 个整数 \(A[i]\)

接下来 \(M\) 行表示 \(M\) 条指令,每条指令的格式如题目描述所示。

输出格式

对于每个询问,输出一个整数表示答案。

每个答案占一行。

数据范围

\(1 \le N,M \le 10^5\),
\(|d| \le 10000\),
\(|A[i]| \le 10^9\)

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
Q 4 4
Q 1 10
Q 2 4
C 3 6 3
Q 2 4

输出样例:

4
55
9
15

思路

这里需要公式推导。

给定一个数 \(k\) ,要求计算 \(a_1 \sim a_k\) 所有数之和。

\[\begin{align} \nonumber & \ a_1+a_2+\cdots +a_k & \\[3mm]\nonumber = & \ D_1+(D_1+D_2 )+(D_1+D_2+D_3)+\cdots +(D_1+D_2+\cdots +D_k) &\\[3mm]\nonumber = &\ kD_1 + (k - 1)D_2+(k - 2)D_3+\cdots +(k-(k-1))D_k &\\[3mm]\nonumber =&\ k(D_1+D_2+\cdots+D_k)-(D_2+2D_3+\cdots+(k-1)D_k)&\\[1mm]\nonumber =&\ k\sum_{i=1}^{k}D_i-\sum_{i=1}^{k}(i-1)D_i&\\\nonumber \end{align} \]

这里我们需要两个树状数组维护,称为二阶树状数组

\(k\sum_{i=1}^{k}D_i\) 称为 \(s_1\) 数组,\(\sum_{i=1}^{k}(i-1)D_i\) 成为 \(s_2\) 数组。

在更改区间时,两个树状数组同时更新。

注意 \(s_2\) 的更新,更新左端点 \(l\) 时,加上 \((l-1)\times d\);更新右端点 \(r\) 时,减去 \((r+1-1)\times d=r\times d\)

同时注意维护的单点是 \(l\)\(r+1\)

查询时输出 \(a_1 \sim a_r\) 所有数之和 \(-\) \(a_1 \sim a_{l-1}\) 所有数之和。

!!!(更新区间是 \(l\)\(r+1\) ,最后输出是 \(l-1\)\(r\),一定注意)

代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
typedef long long ll;
#define lowbit(x) (x & -x)
int n, m;
ll s1[N], s2[N]; // 二阶树状数组
void update1(ll x, ll d) {while (x <= n) {s1[x] += d, x += lowbit(x);}}
void update2(ll x, ll d) {while (x <= n) {s2[x] += d, x += lowbit(x);}}
ll sum1(ll x) {auto t = 0ll; while (x) {t += s1[x], x -= lowbit(x);} return t;}
ll sum2(ll x) {auto t = 0ll; while (x) {t += s2[x], x -= lowbit(x);} return t;}

int main()
{
    cin >> n >> m;
    ll old = 0, a;
    for (int i = 1; i <= n; i++) {
        cin >> a;
        update1(i, a - old); // 差分1
        update2(i, (i - 1) * (a - old));
        old = a;
    }
    while (m--) {
        char op; cin >> op;
        if (op == 'C') {
            int l, r, d; cin >> l >> r >> d;
            update1(l, d);
            update1(r + 1, -d);
            update2(l, d * (l - 1));
            update2(r + 1, -d * r); // d * r = d * (r + 1 - 1)
        }
        else {
            int l, r; cin >> l >> r;
            cout << r * sum1(r) - (l - 1) * sum1(l - 1) - (sum2(r) - sum2(l - 1)) << endl;
        }
    }
    
    return 0;
}
posted @ 2025-02-25 09:22  AKgrid  阅读(32)  评论(0)    收藏  举报