树状数组区间更新

Posted kevinyao-blog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树状数组区间更新相关的知识,希望对你有一定的参考价值。

概述

这篇文章前半部分主要研究树状数组的区间更新 单点求值,后半部分研究区间更新 区间求和

前置知识

树状数组的基本知识以及单点更新区间求和,差分的思想。

区间更新,单点求和

分析

回顾一下最简单的树状数组的功能:快速求出一个数列中某个数的前缀和,以及修改一个位置上的数。
现在我们要利用这两个功能实现:快速求出某个数列中某个数的值,以及修改一个区间上的数。

结合差分的知识,将上下两种功能进行比对,不难发现:快速求出某个数列中某个数的值,就是在该数列的差分数列中求出前缀和(差分和前缀和是互逆操作);而区间更新则是差分最擅长的,只需要进行两次单点修改即可。

代码

模板题是洛谷P3368 树状数组2。
在原本的树状数组代码中做一些小改动(已经标注出),得到:

#include<bits/stdc++.h>


using namespace std;


int n;
int c[1000010];


void rev(int st, int fi, int x); // 运用差分进行区间修改
int query(int loc); // 查询单点值(不是前缀和!)
void _pointRev(int loc, int x);
inline int lowbit(int x);


int main(void) {
    int m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        int x;
        scanf("%d", &x);
        rev(i, i, x);
    }
    for (int i = 1; i <= m; ++i) {
        int f, st, fi, x;
        scanf("%d", &f);
        switch(f) {
            case 1:
            scanf("%d%d%d", &st, &fi, &x);
            rev(st, fi, x);
            break;
            case 2:
            scanf("%d", &st);
            printf("%d
", query(st));
        }
    }
    return 0;
}


void rev(int st, int fi, int x) {
    _pointRev(st, x); // 首加
    if (fi < n) { // 有可能是末尾,严谨一点
        _pointRev(fi + 1, -x); // 尾后一位减
    }
    return;
}

int query(int loc) {
    int sum = 0;
    for (int i = loc; i > 0; i -= lowbit(i)) {
        sum += c[i];
    }
    return sum;
}

void _pointRev(int loc, int x) {
    for (int i = loc; i <= n; i += lowbit(i)) {
        c[i] += x;
    }
    return;
}

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

区间更新,区间求和

分析

区间求和要通过前缀和来进行(树状数组也只能维护前缀和)。而区间更新又必须使用差分,用普通的方法就行不通了。我们来列出公式进行一下变形(num[]表示原数组,c[]表示差分数组):

num[1]+num[2]+num[3]+...+num[n]
=(c[1]) + (c[1]+c[2]) + (c[1]+c[2]+c[3]) + ... + (c[1]+c[2]+c[3]+...+c[n])
=c[1]*n + c[2]*(n-1) + c[3]*(n-2) + ... + c[n]*1
=c[1]*(n-0) + c[2]*(n-1) + c[3]*(n-2) + ... + c[n]*(n-(n-1))
=(c[1] + c[2] + c[3] + ... + c[n]) * n - c[1]*0 - c[2]*1 - c[3]*2 - ... - c[n] * (n-1)
=(c[1] + c[2] + c[3] + ... + c[n]) * n - (c[1]*(1-1) + c[2]*(2-1) + c[3] * (3-1) + ... + c[n] * (n-1))

最终得到的公式需要维护一个对于c[i]的普通前缀和以及一个对于c[i]*(i-1)的前缀和变体,两个树状数组就可以满足了。

代码

模板题是洛谷P3372 线段树1

#include<bits/stdc++.h>


using namespace std;


typedef long long LL;


int n;
LL c1[100010], c2[100010];


void revise(int st, int fi, LL x);
LL query(int st, int fi);
void _pointRevise(int loc, LL x);
LL _pointQuery(int loc);
inline int lowbit(int x);


int main(void) {
    int m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) {
        LL t;
        scanf("%lld", &t);
        revise(i, i, t);
    }
    for (int i = 1; i <= m; ++i) {
        int opt, x, y;
        LL k;
        scanf("%d", &opt);
        switch(opt) {
            case 1:
                scanf("%d%d%lld", &x, &y, &k);
                revise(x, y, k);
                break;
            case 2:
                scanf("%d%d", &x, &y);
                printf("%lld
", query(x, y));
                break;
        }
    }
    return 0;
}


void revise(int st, int fi, LL x) {
    _pointRevise(st, x);
    if (fi < n) {
        _pointRevise(fi + 1, -x);
    }
    return;
}

LL query(int st, int fi) {
    return _pointQuery(fi) - ((st > 1) ? _pointQuery(st - 1) : 0);
}

void _pointRevise(int loc, LL x) {
    for (int i = loc; i <= n; i += lowbit(i)) {
        c1[i] += x;
        c2[i] += x * (loc - 1);
    }
    return;
}

LL _pointQuery(int loc) {
    LL sum = 0;
    for (int i = loc; i > 0; i -= lowbit(i)) {
        sum += c1[i] * loc - c2[i];
    }
    return sum;
}

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

以上是关于树状数组区间更新的主要内容,如果未能解决你的问题,请参考以下文章

树状数组从入门到弃疗

树状数组区间更新

树状数组区间更新

51nod_1199 树的先跟遍历+区间更新树状数组

树状数组模板(持续更新)

树状数组区间修改,区间更新:差分数组的运用