You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

7.7 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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

一、题目描述

给定一个长度为 N 的数列 A,以及 M条指令,每条指令可能是以下两种之一: C l r d,表示把 A[l],A[l+1],…,A[r] 都加上 dQ l r,表示询问数列中第 lr个数的和。

对于每个询问,输出一个整数表示答案。 输入格式 第一行两个整数 N,M

第二行 N个整数 A[i]

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

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

每个答案占一行。

数据范围

1≤N,M≤105,|d|≤10000,|A[i]|≤10^9

二、树状数组

树状数组解决的问题 操作对象 例题
单点修改,区间查询 前缀和 点我
区间修改,单点查询 一个差分数组 点我
区间修改,区间和查询 两个差分数组 本题

区间修改,单点查询

如果是区间修改,单点查询。只需用树状数组维护一个差分数组b,假设查询位置x,那么\displaystyle \sum_{i=1}^{x}b_i就是x位置上的变化后的值。

区间修改+区间和查询

考虑引入区间查询。首先最暴力想,假设查询[1,r]。那么[1,r]的答案=\displaystyle \sum_{i=1}^{r}\sum_{j=1}^{i}b_j 不妨举个特例,更直观些。假设查询[1, 4]。那么ans=(b_1)+(b_1+b_2)+(b_1+b_2+b_3)+(b_1+b_2+b_3+b_4)=4b_1+3b_2+2b_3+1b_4

换成查询[1, r]。那么

\large ans = r*b_1+(r-1)*b_2+(r-3)*b_3+...+1*b_r
\\=(r+1-1)b_1+(r+1-2)b_2+(r+1-3)b_3+…+(r+1-r)b_r \\= (r+1)\sum_{i=1}^{r}b_i-\sum_{i=1}^{r}i*b_i$$
### 三、树状数组
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1000010;

// 树状数组
typedef long long LL;
#define lowbit(x) (x & -x)
LL c1[N], c2[N];
void add(LL x, LL d) {
    for (LL i = x; i < N; i += lowbit(i)) c1[i] += d, c2[i] += x * d;
}

LL sum(LL x) {
    LL res = 0;
    for (LL i = x; i; i -= lowbit(i)) res += (x + 1) * c1[i] - c2[i];
    return res;
}

int n, m;
LL a[N];

int main() {
    // 加快读入
    ios::sync_with_stdio(false), cin.tie(0);
#ifndef ONLINE_JUDGE
    freopen("243.in", "r", stdin);
#endif
    cin >> n >> m;

    for (int i = 1; i <= n; i++) {
        cin >> a[i];
        add(i, a[i] - a[i - 1]); // 保存基底是差分数组的树状数组
    }

    while (m--) {
        string op;
        int x, y, d;
        cin >> op;
        if (op[0] == 'Q') {
            cin >> x >> y;
            printf("%lld\n", sum(y) - sum(x - 1));
        } else {
            cin >> x >> y >> d;
            add(x, d), add(y + 1, -d); // 维护差分
        }
    }
    return 0;
}
```

#### 代码细节解读

```cpp {.line-numbers}
void add(LL x, LL v) {
    for (LL i = x; i < N; i += lowbit(i)) c1[i] += v, c2[i] += x * v;
}

LL sum(LL x) {
    LL res = 0;
    for (LL i = x; i; i -= lowbit(i)) res += (x + 1) * c1[i] - c2[i];
    return res;
}
```

#### 概念说明
- ① $b[i]=a[i]-a[i-1]$ : 原数组的差分数组
  
- ② $c_1[]$:**差分数组$b[]$对应的, 用于快速同步修改,快速统计分析 用的 树状数组**,利用树状数组前缀和的特性,快速获取到区间修改后的单点值。

- ③ $c_2[]$维护的是$i * b[i]$的前缀和


![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/BlogImages/{year}/{month}/{md5}.{extName}/202308171405886.png)

#### 修改流程
当我们给$a[i] \sim a[j]$加上$d$时,对应$c_1[]$就是差分的 **双点** 修改:
```cpp {.line-numbers}
add(x,d),add(y+1,-d);
```

对应的内部逻辑就是辅助数组$c_2[]$,它是差分数组$i*b[i]$的前缀和,原数组修改一下,它就跟着修改一下:
```cpp {.line-numbers}
  for (LL i = x; i < N; i += lowbit(i)) c1[i] += v, c2[i] += x * v;
```
而在查询时,按推出的公式查询即可
```cpp {.line-numbers}
for (LL i = x; i; i -= lowbit(i)) res += (x + 1) * c1[i] - c2[i];
```

  

### 四、线段树+区间修改+懒标记+区间和查询

```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const int N = 100010;

int n, m;
int a[N];
struct Node {
    int l, r;
    LL sum, tag; // 区间总和,修改的数值(懒标记)
} tr[N << 2];

// 向祖先节点更新统计信息
void pushup(int u) {
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum; // 向父节点更新sum和
}

// 父节点向子节点传递懒标记
void pushdown(int u) {
    auto &root = tr[u], &ls = tr[u << 1], &rs = tr[u << 1 | 1];
    if (root.tag) { // 如果存在懒标记
        // tag传递到子段子段的sum和需要按 区间长度*root.tag 进行增加
        ls.tag += root.tag, ls.sum += (LL)(ls.r - ls.l + 1) * root.tag;
        rs.tag += root.tag, rs.sum += (LL)(rs.r - rs.l + 1) * root.tag;
        // 清除懒标记
        root.tag = 0;
    }
}

// 构建
void build(int u, int l, int r) {
    tr[u] = {l, r};              // 标记范围
    if (l == r) {                // 叶子
        tr[u] = {l, r, a[l], 0}; // 区间内只有一个元素l(r),区间和为a[l],不需要记录向下的传递tag
        return;
    }

    int mid = (l + r) >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); // 左右儿子构建
    pushup(u);                                            // 通过左右儿子构建后,向祖先节点反馈统计信息变化
}

// 以u为根在区间[l,r]之间全都增加v
void modify(int u, int l, int r, int v) {
    if (tr[u].l >= l && tr[u].r <= r) {               // 如果区间完整命中
        tr[u].sum += (LL)(tr[u].r - tr[u].l + 1) * v; // 总和增加 = 区间长度*v
        tr[u].tag += v;                               // 懒标记+v
        return;
    }

    pushdown(u); // 如果自己身上有旧的tag数值在递归前需要将原tag值pushdown到子孙节点去
    int mid = (tr[u].l + tr[u].r) >> 1;
    if (l <= mid) modify(u << 1, l, r, v);    // 与左区间有交集
    if (r > mid) modify(u << 1 | 1, l, r, v); // 与右区间有交集
    pushup(u);                                // 将结果的变更更新到祖先节点
}

// 关键的查询操作
LL query(int u, int l, int r) {
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;

    // 记住原则有懒标记的区间修改都是先pushdown消除掉懒标记再分裂
    pushdown(u);

    int mid = tr[u].l + tr[u].r >> 1;
    LL sum = 0;
    if (l <= mid) sum = query(u << 1, l, r);
    if (r > mid) sum += query(u << 1 | 1, l, r);

    // 左查+ 右查 = 总和
    return sum;
}

int main() {
    // 加快读入
    ios::sync_with_stdio(false), cin.tie(0);
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    // 构建线段树
    build(1, 1, n);

    char op;
    int l, r, d;
    while (m--) {
        cin >> op >> l >> r;
        if (op == 'C') {
            cin >> d;
            modify(1, l, r, d); // 区间修改
        } else
            printf("%lld\n", query(1, l, r)); // 区间查询
    }
    return 0;
}
```