一个简单的整数问题【树状数组 + 线段树模板 + 解惑小白细节(本人小白)】

我是小白,你有的困惑我也有!
😊😊 😊😊
不求点赞,只求耐心看完,指出您的疑惑和写的不好的地方,谢谢您。本人会及时更正感谢。希望看完后能帮助您理解算法的本质
😊😊 😊😊

题目:

给定长度为 N 的数列 A,然后输入 M 行操作指令。
第一类指令形如 C l r d,表示把数列中第 l∼r 个数都加 d 。
第二类指令形如 Q x,表示询问数列中第 x 个数的值。

输入格式
第一行包含两个整数 N 和 M。

第二行包含 N 个整数 A[i]。接下来 M 行表示 M 条指令,每条指令的格式如题目描述所示。

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

数据范围
1≤N,M≤105,
|d|≤10000,
|A[i]|≤109

树状数组:

思路:

  1. 问题一:为什么要开long long 类型呢?
    因为本题最坏情况下:10w个数据,并且每个元素的最大值为1e9,那么10w*1e9 = 1e14;爆int了!
  2. 为什么插入的元素不是 add(i, a[i] ) 而是 add(i, a[i] - a[i-1]) 呢?
    因为本题并没有求所谓的区间和前缀和,而是点查询和区间更新,对于点查询,如果我们直接插入a[i]而不是a[i] - a[i-1]的话,则查询方式为:sum(x) -sum(x-1);两次(logn)的查询。但是采用 a[i] - a[i-1] 的话,则不一样了,学过差分的都知道 差分数组可以由原数组预处理出来,当你求原数组的第 i 项的时候,只需要让差分数组中的前 i 相加求和即为原数组的第 i 项的数值!所以查询第 x 项,我们直接一个 sum(x)即可!让前 x 项相加求和!
  3. 为什么是更新区间是:add (l, d) ;add (r+1, -d)呢?
    1.第一个操作区间更新:即区间里面的元素均加上某个数。这一点可以采用差分数组进行维护。能够保持区间内的元素加上某个数,区间外的元素不修改!
b 是 a的差分数组:
b[i] = a[i] - a[i-1];
区间 [l, r] 增加 3;
b[l] += 3, b[r+1] -= 3;

2.第二个操作点查询:对于差分数组而言,要是想查询原数组中的某个元素,则直接差分数组求对应的前缀和即可!

总结:对于差分数组求前缀和,我们会想到树状数组和普通的求前缀和方法。对于修改区间里面的元素,修改后又要查询原数组的某个数 == 查询差分数组的区间和,即需要动态地维护区间和。所以我们应该采用树状数组进行维护差分数组。

其实本题用线段树的话,会更合适。
各位同学有不懂的问题记得评论,我一定回复,并且添加到此处!

代码:

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 1e5 + 10;
int n, m;
int a[N];
long long c[N];

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

void add (int pos, int w)
{
    for (int i=pos; i <= n; i += lowbit(i))
        c[i] += w;
    return ;
}

long long sum (int pos)
{
    int s=0;
    for (int i=pos; i>0; i-=lowbit(i))
        s += c[i];
    return s;
}

int main()
{
    cin >> n >> m;
    for (int i=1; i <= n; i ++)
    {
        cin >> a[i];
        add (i, a[i]-a[i-1]);
    }
    
    while (m --)
    {
        char op;
        cin >> op;
        if (op == 'C'){
            int l, r, d;
            cin >> l >> r >> d;
            add (l, d);
            add (r+1, -d);  //是将r后面的-d,所以不能包含r!
        }
        else{
            int x;
            cin >> x;
            printf("%d\n", sum(x));
        }
    }
    
    return 0;
}

线段树:

思路:

  1. 建树:将区间划分成完全二叉树的形式。通过递归二分区间,直到区间里面只有一个元素,即叶子节点的时候,才开始 回溯更新 。其余时候,就不断地二分区间端点,但记得把区间的左右端点存在树叶节点里面!
struct Node{
    int l, r;
    LL sum, lazy;
}tr[N]; //树中的节点!
void build(int u, int l, int r) //树节点编号,当前这个节点的左右端点!
{
    tr[u] = {l, r};
    if(l == r){
        tr[u].sum += w[r];  //加上叶子节点权值。
        return ;
    }
    
    int mid = (l + r) >> 1;
    
    build (u<<1, l, mid);
    build (u<<1|1, mid+1, r);
    push_up(u);  //回溯更新,下面一点会讲到!
}
  1. 回溯更新:第一点中的回溯更新打了标记,这里的回溯更新也就是 p u s h u p ( ) pushup() pushup() 函数,即递归到叶子节点后,开始向上回溯,利用子节点的信息更新父节点的信息。
void pushup(int u)//当执行到这里的时候,已经回溯了,表明此时的u为父节点。
{
    tr[u].sum += tr[u<<1].sum + tr[u<<1|1].sum;
}
  1. 单点更新:由于是单点,所以该点的编号,必然是线段树中的叶子节点,既然是叶子节点,则左右端点相等,此时更新端点值 == 单点修改,但是更新后记得回溯更新 p u s h u p pushup pushup!另外注意,这里还涉及到了懒标记的查询迭代!
  2. 注意本题是区间更新哦,这里写单点更新是为了类比下:区间更新当你所要更新的区间范围覆盖了整个节点的左右端点的话,说明以该节点为根节点的子树都要进行区间更新。但是递归下去太繁琐了,所以在最上面那层打个标记,等下次路过的时候,并且还要往下访问的时候,再把懒标记带下去:
    懒标记向下迭代:
void pushdown(int u)
{
    tr[u<<1].lazy += tr[u].lazy;
    tr[u<<1].sum += (tr[u<<1].r - tr[u<<1].l + 1) * tr[u].lazy;
    tr[u<<1|1].lazy += tr[u].lazy;
    tr[u<<1|1].sum += (tr[u<<1|1].r - tr[u<<1|1].r + 1) * tr[u].lazy;
    tr[u].lazy = 0;
}
void add (int u, int l, int r, int val)
{
    if (tr[u].l >= l && tr[u].r <= r){
        tr[u].sum += (tr[u].r-tr[u].l)*val;
        tr[u].lazy += val;
        return;
    }
    
    pushdown(u);
    int mid = (tr[u].l + tr[u].r) >> 1;
    if (l <= mid) add(u<<1, l, r, val);
    if (r >= mid+1) add (u<<1|1, l, r, val);
    pushup(u);
    
}

代码:

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

typedef long long LL;
const int N = 1e5 + 10;
LL w[N];
struct Node{
    int l, r;
    LL sum, lazy;
}tr[N]; //树中的节点!
int n, m;

void pushup(int u)//当执行到这里的时候,已经回溯了,表明此时的u为父节点。
{
    tr[u].sum += tr[u<<1].sum + tr[u<<1|1].sum;
}

void pushdown(int u)
{
    tr[u<<1].lazy += tr[u].lazy;
    tr[u<<1].sum += (tr[u<<1].r - tr[u<<1].l + 1) * tr[u].lazy;
    tr[u<<1|1].lazy += tr[u].lazy;
    tr[u<<1|1].sum += (tr[u<<1|1].r - tr[u<<1|1].r + 1) * tr[u].lazy;
    tr[u].lazy = 0;
}

void build(int u, int l, int r) //树节点编号,当前这个节点的左右端点!
{
    tr[u] = {l, r};
    if(l == r){
        tr[u].sum = w[r];  //加上叶子节点权值。
        return ;
    }
    
    int mid = (l + r) >> 1;
    
    build (u<<1, l, mid);
    build (u<<1|1, mid+1, r);
    //当执行到这里的时候,已经回溯了,表明此时的u为父节点。
    pushup(u);  //回溯更新,下面一点会讲到!
}

void add (int u, int l, int r, int val)
{
    if (tr[u].l >= l && tr[u].r <= r){
        tr[u].sum += (tr[u].r-tr[u].l)*val;
        tr[u].lazy += val;
        return;
    }
    
    pushdown(u);
    int mid = (tr[u].l + tr[u].r) >> 1;
    if (l <= mid) add(u<<1, l, r, val);
    if (r >= mid+1) add (u<<1|1, l, r, val);
    pushup(u);
    
}


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

int main()
{
    cin >> n >> m;
    for(int i = 1;i <= n;++ i) cin >> w[i];
    build(1, 1, 100000);
    char op;
    for(LL i = 1, l, r, x;i <= m;++ i)
    {
        cin >> op >> l;
        if(op == 'C') 
        {
            cin >> r >> x;
            add(1, l, r, x);
        }
        else cout << query(1, l) << endl;
    }

    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值