需要支持多种操作的线段树该如何确定运算顺序?

先来看一道最简单的加乘标记:

\(\huge\text{点我看题}\)

本题需要我们进行加法,乘法的在线修改以及查询取模后的结果。因为加法和乘法对于取模运算来说是不受限制的,即可以随时在操作过程中进行取模操作。

对于在线修改,我第一个想到的是lazytag(延迟标记)。求出某区间内的值保存在懒标内,几乎可以达到
\(O(N\log N)\)的时间复杂度。

因为本题需要支持加法和乘法操作,因此我们使用两个懒标,分别存储加法和乘法后的数值,pushdown时按照某种先后顺序下放即可。

经过分析,我们有以下两种选择:

  1. 加法优先
segtree[root*2].value=((segtree[root*2].value+segtree[root].add)*segtree[root].mul)%p

但是这样的话,更新操作并不方便,并且计算过程中会出现小数而出现精度误差,因此这种操作是不优的。

  1. 乘法优先
segtree[root*2].value=(segtree[root*2].value*segtree[root].mul+segtree[root].add*len_qujian)%p

这样操作的话,不会出现精度误差,故我们选择乘法优先。


总代码如下

#include<bits/stdc++.h>
#define MAXN 100005
#define mid ((l+r)>>1)
using namespace std;

int n,m,mod,flag,x,y,z;
long long a[MAXN];

struct tree
{
    long long v,mul,add;
    //数据,乘法懒标,加法懒标
}t[4*MAXN];

void build(int root,int l,int r)
{
    t[root].add=0;
    t[root].mul=1;//初始化懒标
    if (l==r) t[root].v=a[l];
    else
    {
        build(root<<1,l,mid);
        build(root<<1|1,mid+1,r);
        t[root].v=t[root<<1].v+t[root<<1|1].v;
    }
    t[root].v%=mod;
    return;
}//初始化建树

void pushdown(int root,int l,int r)//标记下放
{
    t[root<<1].v=(t[root<<1].v*t[root].mul+t[root].add*(mid-l+1))%mod;
    t[root<<1|1].v=(t[root<<1|1].v*t[root].mul+t[root].add*(r-mid))%mod;//更新值
    t[root<<1].add=(t[root<<1].add*t[root].mul+t[root].add)%mod;//左儿子的加法标记
    t[root<<1|1].add=(t[root<<1|1].add*t[root].mul+t[root].add)%mod;//右儿子的加法标记
    t[root<<1].mul=(t[root<<1].mul*t[root].mul)%mod;//左儿子的乘法标记
    t[root<<1|1].mul=(t[root<<1|1].mul*t[root].mul)%mod;//右儿子的乘法标记
    t[root].add=0;t[root].mul=1;//清空标记
}

void addition(int root,int now_l,int now_r,int l,int r,long long k)
{
    if(l>now_r||r<now_l) return;//无重叠部分
    if(l<=now_l&&r>=now_r)//部分重叠
    {
        t[root].add=(t[root].add+k)%mod;//修改加法标记
        t[root].v=(t[root].v+k*(now_r-now_l+1))%mod;//修改当前点
        return;
    }
    pushdown(root,now_l,now_r);
    int Mid=(now_l+now_r)>>1;
    addition(root<<1,now_l,Mid,l,r,k);
    addition(root<<1|1,Mid+1,now_r,l,r,k);
    //二分进行加法操作
    t[root].v=(t[root<<1].v+t[root<<1|1].v)%mod;
    return;
}

void multiplication(int root,int now_l,int now_r,int l,int r,long long k)
{
    if(l>now_r||r<now_l) return;//无重叠部分
    if(l<=now_l&&r>=now_r)//部分重叠
    {
        t[root].v=(t[root].v*k)%mod;//修改当前点
        t[root].add=(t[root].add*k)%mod;//修改加法标记
        t[root].mul=(t[root].mul*k)%mod;//修改乘法标记
        return;
    }
    pushdown(root,now_l,now_r);
    int Mid=(now_l+now_r)>>1;
    multiplication(root<<1,now_l,Mid,l,r,k);
    multiplication(root<<1|1,Mid+1,now_r,l,r,k);
    //二分进行乘法操作
    t[root].v=(t[root<<1].v+t[root<<1|1].v)%mod;
    return;
}

long long query(int root,int now_l,int now_r,int l,int r)
{
    if(l>now_r||r<now_l) return 0;//无重叠部分
    if(l<=now_l&&r>=now_r) return t[root].v;//部分重叠
    pushdown(root,now_l,now_r);
    int Mid=(now_l+now_r)>>1;
    return (query(root<<1,now_l,Mid,l,r)+query(root<<1|1,Mid+1,now_r,l,r))%mod;
}

template<class T> inline void read(T &re)
{
    re=0;T sign=1;char tmp;
    while((tmp=getchar())&&(tmp<'0'||tmp>'9')) if(tmp=='-') sign=-1;re=tmp-'0';
    while((tmp=getchar())&&(tmp>='0'&&tmp<='9')) re=re*10+(tmp-'0');re*=sign;
}

int main()
{
    read(n);read(m);read(mod);
    for(register int i=1;i<=n;i++) read(a[i]);
    build(1,1,n);
    for(register int i=1;i<=m;i++)
    {
        read(flag);
        if(flag==1) {read(x);read(y);read(z);multiplication(1,1,n,x,y,z);}
        else if(flag==2){read(x);read(y);read(z);addition(1,1,n,x,y,z);}
        else if(flag==3){read(x);read(y);printf("%lld\n",query(1,1,n,x,y));}
    }
    return 0;
}

根据 @初学C++的本间芽衣子 的建议,本文有了下面的扩展内容:

\[\text{hdu4578}\]
There are n integers, a1,a2,…, an. The initial values of them are 0. There are four kinds of operations.
Operation 1: Add c to each number between a x and a y inclusive. In other words, do transformation a k<---a k+c, k=x,x+1,…,y.
Operation 2: Multiply c to each number between a x and a y inclusive. In other words, do transformation a k<---a k×c, k = x,x+1,…,y.
Operation 3: Change the numbers between a x and a y to c, inclusive. In other words, do transformation a k<---c, k = x,x+1,…,y.
Operation 4: Get the sum of p power among the numbers between a x and a y inclusive. In other words, get the result of a xp+a x+1p+…+a yp.

大意:

对于一个区间有4个操作:

  1. 将a~b都加上c
  2. 将a~b都乘上c
  3. 将a~b都变成c
  4. 查询a~b的每个数的p次方的和(p=1,2,3)

与上题类似,本题的本质是线段树的区间更新和求和。但是求和时要返回的是区间各元素的和,平方和或立方和

显然,我们肯定不能遍历子节点求和,会T到飞起

考虑到我们只用维护到最多立方和,因此想到储存三个标记——加法(lazy1),乘法(lazy2),赋值(lazy3)

然后,我们需要想出一种方法完成上述的几个操作,如下:

  1. 加法:
  • 一次方:区间每个数都加\(c\) --→ 加\(len*c\)

  • 平方:\((a+c)^2\) = \(a^2\)+\(2ac\)+\(c^2\)。所以区间每个数都加\(c\)之后的平方和=\(p2\)+\(2*p1*c\)+\(len*c^2\)
  • 立方:\((a+c)^3\)=\(a^3\)+\(3a^2c\)+\(3ac^2\)+\(c^3\)。所以区间每个数都加c之后的立方和 =\(p3\)+\(3*p2*c\)+\(3*p1*c^2\)+\(len*c^3\)

  1. 乘法:

\((ac)^n\)=\(a^n*c^n\);

  • 一次方:\(p1*c\)
  • 平方:\(p2*c^2\)
  • 立方:\(p3*c^3\)
  1. 赋值:
  • 一次方:\(len*c\)
  • 平方:\(len*c^2\)
  • 立方:\(len*c^3\)

本题到这里都很好想,然而最重要的部分是——

多个lazy同时存在该如何处理?

首先是赋值。如果先进行加法或乘法操作再进行赋值,那么之前的加法乘法操作没有任何意义。

于是我们考虑给lazy3赋值的同时清空lazy1和lazy2,这样的话如果lazy1!=0 或 lazy2>1所代表的加法/乘法运算一定在赋值操作之后。这样我们就可以放心的让lazy3第一个PushDown

同样的,先加后乘还是先乘后加?

\((a+b)*c=ac+bc\)

\(a*c+b=ac+b\)

差距在最后的部分,也就是将lazy1向子区间更新时该加\(b*c\)还是\(b\)的问题。

当我们进行乘操作时判断一下lazy1是否不为0,如果true则代表之前已经有进行加法运算,那么应该将lazy1乘以c

到这里,本题就完成了

总结:线段树的标记下放永远都是最恶心的东西,这玩意儿没有一个定论,只能靠自己推。多做题背背顺序也行……

以后如果还有题会在这儿更新的

https://home.cnblogs.com/u/tqr06/

https://www.cnblogs.com/tqr06/p/10400144.html

转载于:https://www.cnblogs.com/tqr06/p/10486283.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值