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


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

    (huge ext{点我看题})

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

    对于在线修改,我第一个想到的是lazytag(延迟标记)。求出某区间内的值保存在懒标内,几乎可以达到
    (O(Nlog 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
    ",query(1,1,n,x,y));}
    	}
    	return 0;
    }
    

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

    [ ext{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

  • 相关阅读:
    通信中的相干与非相干
    OFDM为什么要在频域内插后做fftshift
    OFDM为什么把高频子载波作为保护频带
    辛几何
    奇文共欣赏
    6G新技术
    高扇出导致的系统异常解决方法

    线程与守护线程
    进程之间的通行
  • 原文地址:https://www.cnblogs.com/tqr06/p/10486283.html
Copyright © 2020-2023  润新知