Live2D

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

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

\(\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

posted @ 2019-03-06 21:36  tqr06  阅读(213)  评论(5编辑  收藏  举报