【经典算法题】一个简单的整数问题2

【经典算法题】一个简单的整数问题2

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

问题描述

在这里插入图片描述

解法一

分析

  • 考点树状数组。考点详解网址:树状数组

  • 对于最原始的树状数组存在两个操作:单点加,求区间和(即a[x]+=c, query[L~R]);

  • AcWing 242. 一个简单的整数问题的操作正好反过来:区间加,求单点和(即a[L~R]+=c, query[x]);

  • 本题的操作更进一步:区间加,求区间和(即a[L~R]+=c, query[L~R]);

  • 对于区间加,我们仍然可以使用差分的思想,将原数组a转化成差分数组b,则:

    (1) a [ L , R ] + = c    ⟺    b [ L ] + = c , b [ R + 1 ] − = c a[L,R]+=c \iff b[L]+=c, b[R+1]-=c a[LR]+=cb[L]+=c,b[R+1]=c

    (2) a [ x ] = ∑ b [ i ] , 1 ≤ i ≤ x a[x] = \sum b[i], 1 \le i \le x a[x]=b[i],1ix

  • 如何将原数组a转换为差分数组呢?转化过程如下,这里必须要求数据从a[1]开始,a[0]=0:

b [ 1 ] = a [ 1 ] − a [ 0 ] b [ 2 ] = a [ 2 ] − a [ 1 ] . . . b [ n ] = a [ n ] − a [ n − 1 ] b[1] = a[1] - a[0] \\ b[2] = a[2] - a[1] \\ ... \\ b[n] = a[n] - a[n - 1] b[1]=a[1]a[0]b[2]=a[2]a[1]...b[n]=a[n]a[n1]

  • 这样,对于数组a的区间加法可以转化成对数组b的单点加;但是对数组a求区间和我们就要考虑一下如何求解了。
  • 求数组a的区间和,只需要求出数组a的前缀和即可,即求出:

∑ i = 1 x a i \sum_{i=1}^{x} a_i i=1xai

又因为: a [ i ] = ∑ b [ j ] , 1 ≤ j ≤ i a[i] = \sum b[j], 1 \le j \le i a[i]=b[j],1ji,所以有:
∑ i = 1 x a i = ∑ i = 1 x ∑ j = 1 i b i = ( b 1 ) + ( b 1 + b 2 ) + . . . + ( b 1 + b 2 + . . . + b x ) \sum_{i=1}^{x} a_i = \sum_{i=1}^{x} \sum_{j=1}^{i} b_i = (b_1)+(b_1+b_2)+...+(b_1+b_2+...+b_x) i=1xai=i=1xj=1ibi=(b1)+(b1+b2)+...+(b1+b2+...+bx)
如下图(蓝色的是我们需要求解的部分,红色的是我们补上的内容,则蓝色和=全部和-红色和):

在这里插入图片描述

则有:
∑ i = 1 x a i = ( ∑ i = 1 x b i ) × ( x + 1 ) − ( b 1 + 2 × b 2 + . . . + x × b x ) \sum_{i=1}^{x} a_i = \Bigl(\sum_{i=1}^{x} b_i \Bigr) \times (x+1) - (b1 + 2 \times b_2 + ... + x \times b_x) i=1xai=(i=1xbi)×(x+1)(b1+2×b2+...+x×bx)
因此,我们在操作的同时维护两个前缀和即可,分别是: ∑ b i \sum b_i bi ∑ i × b i \sum i \times b_i i×bi

  • 另外数组a中的数据最大为 1 0 9 10^9 109,操作次数为 1 0 5 10^5 105,每次最大加上1000,因为是区间和,所以可能会超过int的范围,因此需要使用long long存储结果。

代码

  • C++
#include <iostream>

using namespace std;

typedef long long LL;

const int N = 100010;

int n, m;  // 数列长度、操作个数
int a[N];  // 原数组
LL tr1[N];  // 维护a对应的差分数组b的前缀和
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]);
    
    // 使用差分数组b对树状数组初始化
    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 == 'C') {
            scanf("%d", &d);
            // b[l] += d, tr2维护的是b[i] * i的前缀和
            // 因此b[l]增加d, 则(b[l] + d) * l增加了l*d
            add(tr1, l, d), add(tr2, l, l * d);
            // b[r + 1] -= d
            add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
        } else {
            printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
        }
    }
    
    return 0;
}

解法二

分析

  • 考点线段树。考点详解网址:线段树

  • 本题对应的是区间加,区间查询问题,可以转化为单点加,区间查询的问题,具体可以参考:树状数组。这里使用线段树解决这个问题。

  • 本题需要用到线段树五个操作中最复杂的一个,即pushdown:把当前父节点的修改信息下传到子节点,也被称为懒标记(延迟标记)。

  • 对于区间修改,最坏的情况下,时间复杂度是 O ( n ) O(n) O(n)的,比如将整个区间修改,这是我们不能接受的,因此pushdown操作应运而生。其核心思想是懒标记,即当树中某个区间已经完全被我们修改的区间包含了,就不再递归下去,直接返回,同时在该节点标记上需要加上一个数。对于本题来说,下面是懒标记的具体用法。

struct Node {
    int l, r;  // 区间左右端点
    int sum;  // 如果考虑当前节点及子节点上的所有标记,其区间[l, r]的总和就是sum
    int add;  // 懒标记,表示需要给以当前节点为根的子树中的每一个节点都加上add这个数(不包含当前节点)
}

在这里插入图片描述

  • 通过这样的操作,修改的时间复杂度也变成了 O ( l o g ( n ) ) O(log(n)) O(log(n))了。

  • 这样做之后,我们的查询操作(query)也要跟着变化,如下图:

在这里插入图片描述

这个操作对应到代码上是(当前节点是root,左孩子是left,右孩子是right):

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;
    }
}
  • 修改(modify)操作,如果当前考察的整个区间都要加上一个数,则可以直接加上,就不需要进行pushdown操作了;否则也要进行类似于上面的pushdown操作。
void modify(int u, int l, int r, int d) {
    if (tr[u].l >= l && tr[u].r <= r) {  // 当前节点对应的区间完全在[l, 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);
    }
}

代码

#include <iostream>

using namespace std;

typedef long long LL;

const int N = 100010;

int n, m;  // 数列长度、操作个数
int a[N];  // 输入的数组
struct Node {
    int l, r;
    LL sum;  // 如果考虑当前节点及子节点上的所有标记,其区间[l, r]的总和就是sum
    LL add;  // 懒标记,表示需要给以当前节点为根的子树中的每一个节点都加上add这个数(不包含当前节点)
} tr[N * 4];

// 由子节点的信息,来计算父节点的信息
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, a[l], 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);
    }
}

// 将a[l~r]都加上d
void modify(int u, int l, int r, LL 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);
    }
}

// 返回a[l~r]元素之和
LL query(int u, int l, int r) {
    
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    
    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() {
    
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++) scanf("%d", &a[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;
}

解法三

分析

  • 考点分块

  • 首先将该区间分为 n \sqrt{n} n 个部分。

  • 给一个给定区间加上一个数,可以将该给定区间分成若干部分,一共两类:(1)完全被覆盖的部分;(2)前后未完全被覆盖的部分,如下图:

在这里插入图片描述

  • 例如,上图中有两个部分被完全覆盖。

  • 对于每个区间,记录区间和sum、区间被增加的值add(类似于线段树中的懒标记)。

  • 上述两类:完全被覆盖的部分,直接将区间对应的sum值累加到答案上即可;对于未完全覆盖的部分,暴力每个这个部分中的所有数据,将每个数据的实际值累加到答案上即可。

  • 本题最多有 1 0 5 10^5 105 个数据,开根号大约是316.22,这里M=350,表示段数。

代码

#include <iostream>
#include <cstring>
#include <cmath>

using namespace std;

typedef long long LL;

const int N = 100010, M = 350;

int n, m, len;
LL add[M], sum[M];  // sum存储的就是这一段区间的和
int w[N];

int get(int i) {
    return i / len;
}

void change(int l, int r, int d) {
    if (get(l) == get(r)) {  // 说明[l..r]在段内, 直接暴力
        for (int i = l; i <= r; i++) w[i] += d, sum[get(i)] += d;
    } else {
        int i = l, j = r;
        while (get(i) == get(l)) w[i] += d, sum[get(i)] += d, i++;  // 处理左侧不完整区间
        while (get(j) == get(r)) w[j] += d, sum[get(j)] += d, j--;  // 处理右侧不完整区间
        for (int k = get(i); k <= get(j); k++) sum[k] += len * d, add[k] += d;
    }
}

LL query(int l, int r) {
    LL res = 0;
    if (get(l) == get(r)) {  // 段内直接暴力
        for (int i = l; i <= r; i++) res += w[i] + add[get(i)];
    } else {
        int i = l, j = r;
        while (get(i) == get(l)) res += w[i] + add[get(i)], i++;
        while (get(j) == get(r)) res += w[j] + add[get(j)], j--;
        for (int k = get(i); k <= get(j); k++) res += sum[k];
    }
    return res;
}

int main() {
    
    scanf("%d%d", &n, &m);
    len = sqrt(n);
    for (int i = 1; i <= n; i++) {
        scanf("%d", &w[i]);
        sum[get(i)] += w[i];
    }
    
    char op[2];
    int l, r, d;
    while (m--) {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'C') {
            scanf("%d", &d);
            change(l, r, d);
        } else printf("%lld\n", query(l, r));
    }
    
    return 0;
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值