【原创】有关线段树的懒标记的讨论

lazy

Preface

数据结构学复习Part 2。
Part 1是树状数组,但因为我语文不好写一半不写了。

线段树方面只再解决四个问题,其一便是懒标记;其二是多维线段树;其三动态开点;最后是可持久化。
(其五是链剖?)

为什么需要懒标记

(原标题: 震惊!线段树过了大样例却TLE爆零是怎么回事,背后真相曝光,居然是这样!)

这是一道需要单点修改、区间修改、单点查询、区间查询的数据结构题,每次都询问你最值或者和或者极差之类的。

反正用线段树很容易做。
然后出题人每次操作都区间修改[1,n],区间查询[1,n]。
结果就是你每次都必须要把整棵线段树都修改一遍。

再比如说区间翻转,把一个区间给reverse或者都乘以-1,这些操作有一个非常鲜明的特点,你翻转两次就会翻转回来,所以你翻转一次就好了。那如果你乖乖的,出题人给你什么你就做什么,把整棵树跑了 m m m次,下来一看,诶,你们怎么这么快?什么?你们只做了一次?

懒标记是什么 懒标记怎么做

(原标题:最新!懒标记具体什么情况,懒标记有什么亮点)

懒标记,顾名思义是一种标记。

嗯。然后呢?

打了这个标记人就会变懒。

嗯。懒在什么地方?

你的线段树就会懒得修改。

嗯。什么叫懒得修改?

就是咕咕咕的意思。
我让你这个学生做数学作业,你放在一边,暂时不做,等到我老师要来收作业了,你才做。
我让你这个区间区间加上数,你打个标记,暂时不加,等到我上层要来拜访我了,你才加。

嗯。看起来懒标记不是好学生的亚子。咕咕咕有什么用呢?

这样就会节省下很多时间。比方说你就可以把很多次加法变成一次加法,或者把奇数次翻转变成一次翻转,把偶数次忽略。

嗯。为什么可以这么做呢?

如果我们想要修改某个点或者区间,我们一定会访问到其上层的某个较大的区间,假设修改在那个大点的区间被咕咕咕了,被搁置了;这个小点的区间就没有被修改到。
那么我们对这个小区间的查询和再次修改会不会有影响呢?答案是不会的。因为我们想要访问到这个小点的区间,一定会途径那个咕咕咕的大区间,那个大区间就一定会把它累积的修改实施。

嗯。怎么个实施法呢?

接上文,我们在那个大区间里进行了修改操作,必然会影响到大区间的左右两个儿子。但我们之后往下走到这个小区间,这只会走其中的一个儿子;继续往下走的时候,发现总有一个儿子不被访问。(其实不一定,那你就假设“这个小区间”是一个点吧)
所以就相当于每个区间把懒标记传给了两个儿子,只是区别在于我访问完这个区间之后,我会走这两个儿子中的哪些个罢了。


嗯。嗯……你到底在说什么?你不是一度的语文年级前几班级第一吗?

那是你那不是我。

嗯……那你去找物管啊,不是,去找语文老师补习语文啊。你是不是数论写多了就不会说人话了啊?

不管了,反正我的博客只要我自己一个人看懂就好了。

嗯,那你根本不要写这个过于水的内容啊。已经理解了就直接skip啊。浪费时间,解心疑。

……


不同类型的懒标记与多个懒标记的优先度问题

嗯……好花里古哨,不是,花里胡哨的标题啊。

前文也有说过,什么加减乘除翻转都可以懒标记。
但是比方说洛谷的线段树模板2,就有了加法和乘法两种修改。

我们假设这个区间 p p p已经被打上了 p × a + b p\times a+b p×a+b的懒标记。
就是说 p \bold{p} p先乘a再加b。
现在假设我们要让这个区间加上数 x x x,很简单,把加法懒标记 + b +b +b改成 + ( b + x ) +(b+x) +(b+x)
现在假设我们要让这个区间乘上数 y y y,也很简单, ( p ∗ a + b + x ) ∗ y = p ∗ a ∗ y + ( b + x ) ∗ y (p*a+b+x)*y=p*a*y+(b+x)*y (pa+b+x)y=pay+(b+x)y,即把乘法懒标记和加法懒标记都乘上 y y y
(两个一起来?没关系,这种情况只会发生在懒标记下传的时候,而传过来的懒标记也一定是先乘再加的,所以就先乘再加咯)

那么先加再乘呢,即 ( p + a ) × b (p+a)\times b (p+a)×b的情况?
乘法,乘上 y y y,那么显然乘法懒标记乘上 y y y即可。
加法,加上 x x x ( p + a ) × b + x (p+a)\times b+x (p+a)×b+x,该怎么把 x x x塞到 + a +a +a × b \times b ×b里面?

方案一: 不放,直接pushdown掉。

这样肯定不优,我只要一加一乘一加一乘你就没有懒标记了。

方案二:这不是伤疤问题吗? ( p + a ) × b + x = ( p + a ) × b + ( x b ) × b = ( p + a + x b ) × b (p+a)\times b+x=(p+a)\times b+(\frac{x}{b})\times b=(p+a+\frac{x}{b})\times b (p+a)×b+x=(p+a)×b+(bx)×b=(p+a+bx)×b,所以我们把 + a +a +a加上 x b \frac{x}{b} bx就好了。

浮点数您敢用吗?

这就解决了所谓的优先度问题,先乘后加。


落实到具体的代码实现时才发现我一直在避开一个重要的问题——

懒标记的意义到底是什么,下传的时候,到底是更新我还是我的子区间?
我存的懒标记,是我没经历过的还是我经历过的?

这个问题似乎应该在线段树1里解决,谁叫我用的树状数组呢?
这个跟你pushup和pushdown的逻辑与时机有关。

网上多是后者,就是我手里篡着的懒标记是我已经用了的,所谓的停滞是更新到我这里就不再往下更新了。
然后线段树的时候,如果访问到某个点,下传懒标记,递归左右子区间,更新自己的值。
因为左右儿子已经被更新过了,所以我这个区间的值也能够正确算出来了。


代码就可以敲了。

#include
#include
#include
#include
using namespace std;

typedef long long ll;
inline void Read(ll &p)
{
	p=0;
	char c=getchar();
	while(c<'0' || c>'9') c=getchar();
	while(c>='0' && c<='9')
		p=p*10+c-'0',c=getchar();
}

inline void Read(int &p)
{
	p=0;
	char c=getchar();
	while(c<'0' || c>'9') c=getchar();
	while(c>='0' && c<='9')
		p=p*10+c-'0',c=getchar();
}

const int MAXN=102030;
int n,q,opt,u,v,arr[MAXN];
ll p,w,sum[MAXN<<2],laa[MAXN<<2],lat[MAXN<<2];//lazy_add lazy_times
#define lson pos<<1
#define rson pos<<1|1
#define mid ((lef+rig)>>1)

inline void pushdown(int pos,int lef,int rig)
{
	(((sum[lson]*=lat[pos])%=p)+=laa[pos]*(mid-lef+1)%p)%=p,((lat[lson]*=lat[pos])%=p,((laa[lson]*=lat[pos])%=p)+=laa[pos])%=p;
	(((sum[rson]*=lat[pos])%=p)+=laa[pos]*(rig-mid)%p)%=p,((lat[rson]*=lat[pos])%=p,((laa[rson]*=lat[pos])%=p)+=laa[pos])%=p;
	lat[pos]=1,laa[pos]=0;
}

inline void pushup(int pos){sum[pos]=(sum[lson]+sum[rson])%p;}

void build(int pos,int lef,int rig)
{
	if(lef==rig) {sum[pos]=arr[mid]%p,lat[pos]=1; return ;}
	build(lson,lef,mid),build(rson,mid+1,rig),lat[pos]=1,pushup(pos);
}
//Gauche Droit
void add(int pos,int lef,int rig,int gau,int dro,ll del)
{
	if(rig<gau || dro<lef) return ;
	if(gau<=lef && rig<=dro) {(sum[pos]+=del*(rig-lef+1)%p)%=p,(laa[pos]+=del)%=p; return;}
	pushdown(pos,lef,rig),add(lson,lef,mid,gau,dro,del),add(rson,mid+1,rig,gau,dro,del),pushup(pos);
}

void times(int pos,int lef,int rig,int gau,int dro,ll del)
{
	if(rig<gau || dro<lef) return ;
	if(gau<=lef && rig<=dro) {(sum[pos]*=del)%=p,(lat[pos]*=del)%=p,(laa[pos]*=del)%=p; return;}
	pushdown(pos,lef,rig),times(lson,lef,mid,gau,dro,del),times(rson,mid+1,rig,gau,dro,del),pushup(pos);
}

ll query(int pos,int lef,int rig,int gau,int dro)
{
	if(rig<gau || dro<lef) return 0;
	if(gau<=lef && rig<=dro) return sum[pos];
	pushdown(pos,lef,rig);
	ll al=query(lson,lef,mid,gau,dro),ar=query(rson,mid+1,rig,gau,dro);
	pushup(pos);
	return (al+ar)%p;
}

int main() 
{
	Read(n),Read(q),Read(p);
	for(int i=1;i<=n;i++) Read(arr[i]);
	build(1,1,n); 
	while(q--) 
	{
		Read(opt);
		if(opt==1) Read(u),Read(v),Read(w),w%=p,times(1,1,n,u,v,w);
		else if(opt==2) Read(u),Read(v),Read(w),w%=p,add(1,1,n,u,v,w);
		else Read(u),Read(v),printf("%lld\n",query(1,1,n,u,v));  
	}
}

双倍经验【AHOI2009】维护数列,看上去好像只需要改一下q的读入位置就可以原样交了,但是不知道为什么交不起,双倍经验失败。

说在后面

我学信息的转折点在于秒掉了读入优化。
以及后续的若干事情。

后续的若干事情说出来不太好,就不说了。

至少这些后续的很多事情(以及OF-EX-4)让我明白了让事情变成难题的唯一原因只有你以为它是难题。
(不是,有些东西你要意识到是真的不可做是真的难,但至少这些你得干掉的东西你就不要怕,微笑着面对它,消除恐惧……)

与线段树无关。

我只是像决斗前的伽罗瓦一样疯狂留下自己的笔墨罢了。
而且好多笔墨我已经决定尘封了,就算不尘封也写不出写不完了。

嗯……

一嗯胜千言。

那就讲个笑话吧。

经过了树剖的洗礼我的线段树码风和思路都大改,可是我的整体码风却没有收到冲击仍然差不多,这是为什么?
是因为我融会贯通了?
不是!是因为在此之前我基本上没用过线段树!第一次真正的用是在NOIP2018DAY1T1!
在此之后,我就成了自诩线段树大尸了。

说在说在后面

我寻思也才过两天吧,我咋就不知道“我学信息的转折点是在于秒掉了读入优化以及后续的一些事情”,后续的什么事情啊?
中考吗?
忘了忘了,一觉起来全忘了。

你可能感兴趣的:(#,心得,#,题目,#,☠☠☠☠☠哼本人已死亡)