一个简单的整数问题2【{树状数组+差分(举例证明)} + 思路推敲代码(线段树) 】

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

题目描述:

一、差分 + 树状数组:

分析题意可知:题目要求我们进行两个操作,分别是

  1. 区间更新;
  2. 区间查询;

而对于这两个操作,从最常见的来看,区间查询操作,我们可以使用前缀和,树状数组,而选择哪一种,取决于题目是否有单点修改的操作,一般单点更新的前提下,首选 树状数组;实际上我都用的是树状数组,求前缀和也是!毕竟代码也不复杂

另外对于另一个操作,区间更新,虽然不是单点更新,但是直觉告诉我,就是树状数组;而对于区间更新,要使得区间里面的每个元素,都同时加上某个数的话,最快的做法,显然是差分!那么,不禁想到,既然要修改原数组,那么就等于修改它的差分数组,因为对于一个区间性修改而言,差分数组更适合,对于个别元素的修改,显然直接索引原数组进行修改即可! 所以说这里我们采用树状数组维护原数组的差分数组,从而达到区间更新的目的!

但是两个操作分开来看显然是没有问题的,但是这里你就要注意了,区间更新和区间查询分开来看,没有什么问题,但是两个操作是在同一个问题中,同一个对象,同一个序列。而区间查询之前针对的是原数组,区间更新针对的是原数组的差分数组。这就矛盾了,你想查询原数组的和,采用树状数组的求前驱方式:

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

而区间更新维护的是差分数组 b b b,这就很矛盾了,所以说,更新的是差分数组,而查询差分数组的话,得到的是原数组的某个元素更新之后的值,而不是区间和。所以这里我们需要考虑树状数组的维护的底层数组到底是谁!

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

既然可以由差分数组求前缀和得出原数组,而原数组更新的话效率太慢了,并且差分数组能够快速更新原数组,求一次前缀和是原数组的元素值,那假如我求两次差分数组的前缀和呢?那不就是原数组的前缀和了吗?

在这里插入图片描述
很显然,这就是我们要求的区间和,将各个 a i a_i ai 相加,等价于各个恒等式右边的 b i b_i bi 相加求和!但是很明显,直接求不好求,这样去推公式的话,也很不好推导,但是有的同学可能会说,这不是挺规律的吗?一共x项,那么总和就是:
竖着看
( x − 1 + 1 ) ∗ b 1 + ( x − 2 + 1 ) ∗ b 2 + . . . . + ( x − i + 1 ) ∗ b i + . . . ( x − x + 1 ) b x (x-1+1)*b_1 + (x-2+1)*b_2 + ....+ (x-i+1)*b_i +...(x-x+1)b_x (x1+1)b1+(x2+1)b2+....+(xi+1)bi+...(xx+1)bx
这是 1 − x 的区间和 1-x的区间和 1x的区间和,存在很明显的一个公式就是:
每项 b i b_i bi要加多少项 = ( x − i + 1 ) ∗ b i (x-i+1)*b_i (xi+1)bi

若要求的是 3~6的和呢?你这个公式就不规律了!
在这里插入图片描述

所以说,所求对象没有问题,但是不好求!
直接求不行,那我就间接求呗:直接采用容斥原理里面的补集思想!
在这里插入图片描述
很明显,我们要求的是黑色部分,现在补了红色部分,可不可以这么思考:
黑色部分 = 整体部分 - 红色部分!从而间接求我们目标对象!

那么红色部分怎么求呢?
红色部分的规律很明显啦:
( b 1 ∗ 1 + b 2 ∗ 2 + . . . . + b i ∗ i ) + . . . b x ∗ x (b_1*1 + b_2*2 + .... + b_i*i) + ... b_x*x (b11+b22+....+bii)+...bxx
在这里插入图片描述
求整体部分和:
我们可以按列来求啊,很明显啊,由于多加了一行,所以每一列都是 x + 1 x+1 x+1 项,所以将每一列相加,不就是整体的和了吗??
在这里插入图片描述

故可得目标区域和为:
在这里插入图片描述
现在再来观察这个公式,不难发现有两部分需要我们求和,
一部分是: b i ∗ i b_i * i bii
另一部分是: b i b_i bi
而一个树状数组只能维护一个序列,显然我们的树状数组最初维护的是一个 b i b_i bi 序列。那么对于: b i ∗ i b_i * i bii 序列该如何维护呢?所以另外开一个树状数组进行维护呗,即底层不再是: b i b_i bi,而是 b i ∗ i b_i * i bii
但是我们不是有区间更新操作吗?我们在 b i b_i bi 序列上进行了更新,那么 i ∗ b i i*b_i ibi怎么办呢?那它怎么更新呢?哈哈,其实不受影响的,区间更新的是: b i b_i bi,把 b i b_i bi换一下不就行了,比如: b 1 + 3 b1 + 3 b1+3 b 1 ′ b_1' b1,这是在 b i b_i bi序列里面,然后跳到我们的 b i ∗ i b_i * i bii序列里面, ( b 1 + 3 ) ∗ i (b_1 + 3) * i (b1+3)i ⇒ 项数不变呗,还是 i i i 项,但是变的是: b 1 ′ ∗ i b_1' * i b1i,只是数值变化了,是该乘以变化后的数值呗。所以该做法可行!

代码:

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long LL;

const int N = 1e5  + 10;
LL a[N];
LL tr1[N], tr2[N];
int n, m;

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

void add (LL tr[], int pos, LL d)
{
    for (int i=pos; i<=n; i += lowbit(i))
        tr[i] += d;
}

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

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

int main()
{
    scanf("%d%d", &n, &m);
    
    for (int i=1; i <= n; i ++)
        cin >> a[i];
    
    for (int i=1; i <= n; i ++)
    {
        add (tr1, i, a[i]-a[i-1]);
        add (tr2, i, (LL)(a[i]-a[i-1])*i);
    }
        
    while (m -- )
    {
        char op[2];
        int l, r;
        scanf("%s%d%d", &op, &l, &r);
        
        if (*op == 'C'){ //更新!
            int d;
            cin >> d;
            // += d;
            add (tr1, l, d); add (tr2, l, (LL)l*d);
            // -= d;
            add (tr1, r+1, -d); add (tr2, r+1, (LL)(r+1)*(-d));
        }
        else {
            cout << query(r) - query(l-1) << endl;
        }
    }
    
    return 0;
}

二、线段树:

思路推敲代码 – 线段树的建立过程!

  1. 先建立线段树: b u i l d ( ) ; build(); build();
const int N = 1e5 + 10;
struct Node{
    当前节点所在的区间的左右端点
    int l, r;
    当前节点的区间和,更新标记
    LL sum, add;
}tr[4*N];
int n, m;
int w[N];

参数:当前节点的编号,当前节点所在区间的左右端点!
void build(int u, int l, int r)
{
    当前节点为叶子节点:
    if (l == r)
    {
        tr[u] = {l, r, w[r], 0};
        return ;
    }
    否则:先把当前节点的左右区间端点存进去
    tr[u] = {l,r}; 
    划分出当前节点的中间节点,去递归建立左右子树!
    int mid = l + r >> 1;
    递归建立左子树:
    build(u<<1, l, mid)
    递归建立右子树:
    build(u<<1|1, mid+1, r);
    递归到了叶子节点,然后会开始回溯,可是由于叶子节点的区间和赋值为w[u];
    则需要我们去更新其父节点的区间和 = 左右节点的区间和相加!
    pushup(u);
}
  1. 实现 p u s h u p pushup pushup 函数:
    p u s h u p pushup pushup 的作用是:当某个子区间发生更新的时候,其包含它的父区间也必然会发生更新,所以说要向上更新呗!
    比如:[2, 4]区间里的元素和增加了 15,那么我们本题涉及到了:查询某个区间的元素和,那么包含 [2, 4]的区间 [1, 6],则请问它的区间和,不需要更新吗?
void pushup(int root)
{
    tr[root].sum = tr[root<<1].sum + tr[root<<1|1].sum
}

  1. 区间更新: m o d i f y ( ) modify () modify(),即要修改某个区间内的元素和,这里提前告知下,更新的是区间和,而不是什么单点更新,并且题目都告知了,是某个区间里面的所有元素都加上 d d d,那么加了多少个 d d d 呢?取决于区间的长度: r − l + 1 r-l + 1 rl+1;等价于 整个区间和增加了 : ( r − l + 1 ) ∗ d (r-l+1)*d (rl+1)d

参数:当前的节点编号,所要更新的区间的左右端点!增加多少值!
void modify (int sum, int l, int r, int v)
{
    如果当前节点所在的区间被目标区间所覆盖:
    if (tr[u].l >= l && tr[u].r <= r)
    {
        tr[u].sum += (LL)(r-l+1)*v;
        tr[u].add += v;
    }
    else
    {
        如果当前所在的节点是打过修改标记的,说明之前它的子节点并没有进行标记修改,
        而现在又要处理其子节点了,子节点是因为之前节省时间没有去递归修改的,所以现在要
        pushdown一下!临时修改!
        pushdown(u);
        int mid = tr[u].r + tr[u].l >> 1;
        if (l <= mid) modify (u<<1, l, r, v);
        if (r > mid) modify (u<<1|1, l, r, v);
        pushup(u);
    }
    
    return ;
}

  1. 向下更新:
    线段树的懒标记:本来我们要修改某段区间的,这段区间在某个节点上,然后该节点又藏得比较深,我们需要一个一个节点去找,就很累,所以我们可以在包含该目标节点的父节点上,打一个懒标记,然后下次路过的时候,再去修改它,避免重复的搜索!所以说处理懒标记的时候,才去传承处理!
    每次处理完记得将之前放置的懒标记给标记为0!否则下次路过,本来已经解决了的事,又去解决一遍吗?
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;
    }
}
  1. 区间查询:
    和步骤4中的路过 ⇒ 处理一下,一样,查询的时候,路过懒标记的时候,记得顺便将它带走为:pushdown(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;
}

代码:

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

using namespace std;
typedef long long LL;

const int N = 1e5 + 10;
struct Node{
    //当前节点的区间的左右端点,而不是左右孩子编号,左右孩子编号可以由当前节点编号求得!
    int l, r;
    //当前区间和,以及当前区间需要更新多少!
    LL sum, add;
}tr[N*4];

int n, m;   //n个点m个查询!
int w[N];

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)   //如果根节点的修改值不等于0的话!说明这里有改动!将其传递下去!
    {
        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);
    }
}

void modify(int u, int l, int r, int v) //修改的是区间元素!
{
    if (l <= tr[u].l && tr[u].r <= r)
    {
        tr[u].sum += (LL)(tr[u].r - tr[u].l + 1)*v;
        tr[u].add += v;
    }
    else
    {
        pushdown(u);
        int mid = tr[u].r + tr[u].l >> 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)   //查询的是区间和!
{
    //如果插叙的区间已经覆盖了当前的节点,例如查询的是 [2, 6],当前节点的区间端点: [3, 4];
    //所以说是满足条件的!则说明没必要再往下找了!
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].sum;
    
    pushdown(u);
    //LL sum将sum定义成了局部变量,也可以将其进行传参携带,但是不方便!
    //[清华大学考研机试题,整数拆分]详解了递归中的局部变量的作用:
    //记录的是从当前节点到达叶子节点的答案,本题答案求的是“区间和”,
    //则记录的是从当前节点区间的和值。只不过因为将区间和划分到子节点里面去了,所以需要递归查找!
    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') 
        {
            int d;
            scanf ("%d", &d);
            //根节点编号,更新的区间的做右端点,增加的权值!
            modify (1, l, r, d);
        }
        
        //区间查询:
        else
        {
            //输出查询的区间和!
            cout << query(1, l, r) << endl;
        }
    }
    
    return 0;
}

草稿:

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

using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
struct Node{
    //当前节点所在的区间的左右端点
    int l, r;
    //当前节点的区间和,更新标记
    LL sum, add;
}tr[4*N];
int n, m;
int w[N];

void pushup(int root)
{
    tr[root].sum = tr[root<<1].sum + tr[root<<1|1].sum;
}

//参数:当前节点的编号,当前节点所在区间的左右端点!
void build(int u, int l, int r)
{
    //当前节点为叶子节点:
    if (l == r)
    {
        tr[u] = {l, r, w[r], 0};
        return ;
    }
    //否则:先把当前节点的左右区间端点存进去
    tr[u] = {l,r}; 
    //划分出当前节点的中间节点,去递归建立左右子树!
    int mid = (l + r) >> 1;
    //递归建立左子树:
    build(u<<1, l, mid);
    //递归建立右子树:
    build(u<<1|1, mid+1, r);
    //递归到了叶子节点,然后会开始回溯,可是由于叶子节点的区间和赋值为w[u];
    //则需要我们去更新其父节点的区间和 = 左右节点的区间和相加!
    pushup(u);
}

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 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;
        tr[u].add += v;
    }
    else
    {
        //如果当前所在的节点是打过修改标记的,说明之前它的子节点并没有进行标记修改,
        //而现在又要处理其子节点了,子节点是因为之前节省时间没有去递归修改的,所以现在要
        //pushdown一下!临时修改!
        pushdown(u);
        int mid = (tr[u].r + tr[u].l) >> 1;
        if (l <= mid) modify (u<<1, l, r, v);
        if (r > mid) modify (u<<1|1, l, r, v);
        pushup(u);
    }
    
    return ;
}

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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值