需要支持多种操作的线段树该如何确定运算顺序?
先来看一道最简单的加乘标记:
本题需要我们进行加法,乘法的在线修改以及查询取模后的结果。因为加法和乘法对于取模运算来说是不受限制的,即可以随时在操作过程中进行取模操作。
对于在线修改,我第一个想到的是lazytag(延迟标记)。求出某区间内的值保存在懒标内,几乎可以达到
\(O(N\log N)\)的时间复杂度。
因为本题需要支持加法和乘法操作,因此我们使用两个懒标,分别存储加法和乘法后的数值,pushdown时按照某种先后顺序下放即可。
经过分析,我们有以下两种选择:
- 加法优先
segtree[root*2].value=((segtree[root*2].value+segtree[root].add)*segtree[root].mul)%p
但是这样的话,更新操作并不方便,并且计算过程中会出现小数而出现精度误差,因此这种操作是不优的。
- 乘法优先
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个操作:
- 将a~b都加上c
- 将a~b都乘上c
- 将a~b都变成c
- 查询a~b的每个数的p次方的和(p=1,2,3)
与上题类似,本题的本质是线段树的区间更新和求和。但是求和时要返回的是区间各元素的和,平方和或立方和
显然,我们肯定不能遍历子节点求和,会T到飞起
考虑到我们只用维护到最多立方和,因此想到储存三个标记——加法(lazy1),乘法(lazy2),赋值(lazy3)
然后,我们需要想出一种方法完成上述的几个操作,如下:
- 加法:
一次方:区间每个数都加\(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\)。
- 乘法:
\((ac)^n\)=\(a^n*c^n\);
- 一次方:\(p1*c\)。
- 平方:\(p2*c^2\)
- 立方:\(p3*c^3\)
- 赋值:
- 一次方:\(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
到这里,本题就完成了