线段树 / 树状数组:一个简单的整数问题2(区间修改 / 求区间和)

题目链接
题意:给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:
1、“C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d。
2、“Q l r”,表示询问 数列中第 l~r 个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数N,M。
第二行N个整数A[i]。
接下来M行表示M条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
数据范围
1≤N,M≤105,
|d|≤10000,
|A[i]|≤1000000000
输入样例:
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
思路1 (线段树)
用线段树来进行区间修改的话就需要用到懒标记法来实现啦。

代码实现:

#include
#include
#include
using namespace std;
typedef long long ll;
const int N = 100010;
int n, m;
int w[N];
struct node
{
    int l, r;
    //sum:如果只考虑当前节点和子节点上的所以标记,当前区间和是多少
    //给当前节点的所有儿子加上add
    ll sum, add;
}tr[N << 2];

void pushup(int u)
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
//用两个子节点信息更新父节点信息
void pushdown(int u)
{
    auto &root = tr[u], &left = tr[u << 1], &right = tr[u << 1 | 1];
    //如果当前节点已经被标记
    if(root. add){
        left.add += root.add;
        left.sum += (ll)(left.r - left.l + 1) * root.add;
        right.add += root.add;
        right.sum += (ll)(right.r - right.l + 1) * root.add;
        root.add = 0;
    }
}

void build(int u, int l, int r)
{
    if(l == r) tr[u] = {l, r, w[r], 0};
    else{
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid);
        build(u << 1 | 1, mid + 1, r);
        pushup(u);
    }
}
//修改,将[l,r]的区间加上d
void modify(int u, int l, int r, int d)
{
    if(tr[u].l >= l && tr[u].r <= r){
        tr[u].sum += (ll)(tr[u].r - tr[u].l + 1) * d;
        tr[u].add += d;
    }
    else{
        //当前区间太大,需要分裂
        pushdown(u);
        int mid = tr[u].l + tr[u].r >> 1;
        if(l <= mid) modify(u << 1, l, r, d);
        if(r > mid) modify(u << 1 | 1, l, r, d);
        pushup(u);
    }
}

ll query(int u, int l, int r)
{
    if(tr[u].l >= l && tr[u].r <= r)
        return tr[u].sum;
    pushdown(u);
    ll sum = 0;
    int mid = tr[u].l + tr[u].r >> 1;
    if(l <= mid) sum = query(u << 1, l, r);
    if(r > mid) sum += query(u << 1 | 1, l, r);
    return sum;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i ++)
        scanf("%d", &w[i]);
    build(1, 1, n);
    char op[2];
    int l, r, d;
    while(m -- ){
        scanf("%s%d%d", op, &l, &r);
        if(*op == 'C'){
            scanf("%d", &d);
            modify(1, l, r, d);
        }
        else printf("%lld\n", query(1, l, r));
    }
    return 0;
}

思路2 (树状数组)
区间的修改可以用差分的思维来做,让b[l]+=d,b[r+1]-=d; 而对于求前缀和,我们可以推导出一个公式(用两个前缀和来维护):
线段树 / 树状数组:一个简单的整数问题2(区间修改 / 求区间和)_第1张图片
就可以把原来的数组,经过差分操作去维护两个树状数组,一个维护di,一个维护 di×i 这样的话,我们在区间修改的过程中,就可以在两个树状数组中去查询得到前缀和.
代码实现:

#include
#include
#include
using namespace std;
typedef long long ll;
const int N = 100010;

int n, m;
int a[N];
ll tr1[N];//维护b[i]差分数组的前缀和
ll tr2[N];//维护b[i] * i的前缀和

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

void add(ll tr[], int x, ll c)
{
    for(int i = x; i <= n; i += lowbit(i))
        tr[i] += c;
}

ll sum(ll tr[], int x)
{
    ll res = 0;
    for(int i = x; i ; i -= lowbit(i))
        res += tr[i];
    return res;
}

ll prefix_sum(int x)
{
    return sum(tr1, x) * (x + 1) - sum(tr2, x);
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; i ++)
        scanf("%d", &a[i]);
    for(int i = 1; i <= n; i ++){
        int b = a[i] - a[i - 1];
        add(tr1, i, b);
        add(tr2, i, (ll)b * i);
    }
    while(m -- ){
        char op[2];
        int l, r, d;
        scanf("%s%d%d", op, &l, &r);
        if(*op == 'Q'){
            printf("%lld\n",prefix_sum(r) - prefix_sum(l - 1));
        }
        else{
            scanf("%d", &d);
            //a[l] += d
            add(tr1, l, d);
            add(tr2, l, l * d);
            //a[r + 1] -= d
            add(tr1, r + 1, -d);
            add(tr2, r + 1, (r + 1) * -d);
        }
    }
    return 0;
}

你可能感兴趣的:(数据结构)