[线段树系列] LCT打延迟标记的正确姿势

这一篇博客将教你什么?

如何用LCT打延迟标记,LCT和线段树延迟标记间的关系,为什么延迟标记要这样打。

——正片开始——

学习这一篇博客前,确保你会以下知识:

Link-Cut-Tree,普通线段树

当然,不会也没有关系,你可以先收藏这篇博客,等你学了以后再来看。

最好通过了这一道题:【模板】线段树Ⅱ

没有通过也没关系,对于本篇的知识只是一个启发作用。

我们平时使用的Link-Cut-Tree一般只需要打一个翻转标记rev[x]。

然后我们用pushR(x)函数来下发翻转标记。

那么我们现在来看这样一道题:TreeⅡ

练习LCT打标记的绝世好题,可以说就是一道模板题了。

我们来看它需要维护什么:树的权值。

如果能把这个操作2去掉,树剖将绝杀,可惜换不得。

单走一个“树”,nice,直接LCT。

询问权值和,可以,给LCT来一个下传标记,开始你的操作秀。

我们直接来看pushdown部分:

操作有乘和加两种,根据运算法则,乘法优先,所以首先判断

if(lazM[x]!=1){...}

然后是加法

if(lazA[x]){...}

最后回到我们的翻转标记。

然后我们来看pushM和pushA部分

#define mul(x,y) x*=y;x%=MOD;
inline void pushM(unsigned int x,unsigned int d){
    mul(sum[x],d);mul(val[x],d);//节点信息直接更新
    mul(lazM[x],d);mul(lazA[x],d);//按照运算法则先把乘标记乘了,再把加标记乘了
}

有没有回想起什么?没错,就是线段树的懒标记。

我们看看线段树2的懒标记下传部分

void pushdown(int p){
        mul(p<<1)=(mul(p<<1)*mul(p))%P;
        mul(p<<1|1)=(mul(p<<1|1)*mul(p))%P;
        add(p<<1)=(add(p<<1)*mul(p))%P;
        add(p<<1|1)=(add(p<<1|1)*mul(p))%P;
        sum(p<<1)=(sum(p<<1)*mul(p))%P;
        sum(p<<1|1)=(sum(p<<1|1)*mul(p))%P;//按照运算法则更新标记和节点信息
        mul(p)=1;
        add(p<<1)=(add(p<<1)+add(p))%P;
        add(p<<1|1)=(add(p<<1|1)+add(p))%P;
        sum(p<<1)=(sum(p<<1)+add(p)*(r(p<<1)-l(p<<1)+1))%P;
        sum(p<<1|1)=(sum(p<<1|1)+add(p)*(r(p<<1|1)-l(p<<1|1)+1))%P;//按照运算法则更新标记和节点信息
        add(p)=0;
}

惊人的一致。

猜到pushA的写法了吧?那我不讲了,后面有代码。

透过现象看本质。

两种数据结构都是在维护一个区间,只不过LCT维护的树上一段路径的区间。

如果这道题把操作2去掉,我们用树链剖分写,线段树维护,一样是这样打标记。

为什么?

如果你当初学线段树的时候理解了线段树2打标记里面先成后加的原理,你可能思考一下就明白了。

这里的原因和线段树非常相似:精度。

我们的标记是打在父节点上的,告诉它它的孩子加了多少,乘了多少。

如果我们先加了那么多,再乘那么多,结果是不一样的,如果非要等价,需要对式子变形。

我们看这样一个式子:(a+b)*c,它并不等价于a*c+b,运算的顺序是会影响结果的。

然而我们打标记的时候并不能确定顺序。这时我们为何不用上很早就明白的运算法则呢?

看下面两种顺序:

先+后*:(a+b)*c = a*c+b*c,先*后+:(a+b)*c = a*c+b

我们发现,先+后*的式子并不等于先*后+的式子,要让它相等必须在加的那一项也*上c。

但是要让先*后+的式子转化成先+后*的式子,我们就必须用到除法,就会变成实数运算,还有可能得到无限小数影响精度。所以我们只需要使用先*后+的优先顺序,并且在打乘法标记时把加法标记也乘上这个值就可以了。

分析完后,相信各位应该能理解数据结构“懒标记”的概念以及为何选择这种优先顺序了。

那么剩下的就是很正常的LCT操作了,给出此题的代码。

注意这道题有一个坑点:模数是51061,看上去很小,然而暗藏出题人的心机。

我们来看51061的平方 ——> 2516125921,再看int的数据范围 —— > 2147483647

于是我们需要开long long,但是我写的时候为了卡常用了unsigned int。

#include<bits/stdc++.h>
using namespace std;
int n,q;
const int N=1000010;
#define MOD 51061
unsigned int fa[N],val[N],ch[N][2],rev[N],lazM[N],lazA[N],siz[N],sum[N],stk[N];
#define add(x,y) x+=y;x%=MOD;
#define mul(x,y) x*=y;x%=MOD;
inline unsigned int read(){
    unsigned int data=0,w=1;char ch=0;
    while(ch!=‘-‘ && (ch<‘0‘||ch>‘9‘))ch=getchar();
    if(ch==‘-‘)w=-1,ch=getchar();
    while(ch>=‘0‘ && ch<=‘9‘)data=data*10+ch-‘0‘,ch=getchar();
    return data*w;
}
inline bool chk(unsigned int x){
    return ch[fa[x]][1]==x;
}
inline bool nroot(unsigned int x){
    return ch[fa[x]][0]==x||ch[fa[x]][1]==x;
}
inline void pushup(unsigned int x){
    sum[x]=(sum[ch[x][0]]+sum[ch[x][1]]+val[x])%MOD;
    siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
}
template<class T>inline void fswap(T&x,T&y){
    T t=x;x=y;y=t;
}
inline void pushR(unsigned int x){
    fswap(ch[x][0],ch[x][1]);
    rev[x]^=1;
}
inline void pushM(unsigned int x,unsigned int d){
    mul(sum[x],d);mul(val[x],d);
    mul(lazM[x],d);mul(lazA[x],d);
}
inline void pushA(unsigned int x,unsigned int d){
    add(sum[x],d*siz[x]);add(val[x],d);
    add(lazA[x],d);
}
inline void pushdown(unsigned int x){
    if(lazM[x]!=1){
        pushM(ch[x][0],lazM[x]);
        pushM(ch[x][1],lazM[x]);
        lazM[x]=1;
    }
    if(lazA[x]){
        pushA(ch[x][0],lazA[x]);
        pushA(ch[x][1],lazA[x]);
        lazA[x]=0;
    }
    if(rev[x]){
        if(ch[x][0])pushR(ch[x][0]);
        if(ch[x][1])pushR(ch[x][1]);
        rev[x]=0;
    }
}
inline void rotate(unsigned int x){
    int y=fa[x],z=fa[y],k=chk(x),w=ch[x][k^1];
    ch[y][k]=w;if(w)fa[w]=y;
    if(nroot(y))ch[z][chk(y)]=x;fa[x]=z;
    ch[x][k^1]=y;fa[y]=x;
    pushup(y);pushup(x);
}
inline void splay(unsigned int x){
    int y=x,z=0;
    stk[++z]=y;
    while(nroot(y))stk[++z]=y=fa[y];
    while(z)pushdown(stk[z--]);
    while(nroot(x)){
        y=fa[x];z=fa[y];
        if(nroot(y)){
            if(chk(x)==chk(y))rotate(y);
            else rotate(x);
        }rotate(x);
    }
    pushup(x);
}
inline void access(unsigned int x){
    for(int y=0;x;x=fa[y=x])
        splay(x),ch[x][1]=y,pushup(x);
}
inline void makeroot(unsigned int x){
    access(x);splay(x);
    pushR(x);
}
inline int findroot(unsigned int x){
    access(x);splay(x);
    while(ch[x][0])pushdown(x),x=ch[x][0];
    splay(x);
    return x;
}
inline void split(unsigned int x,unsigned int y){
    makeroot(x);
    access(y);splay(y);
}
inline void link(unsigned int x,unsigned int y){
    makeroot(x);
    if(findroot(y)!=x)fa[x]=y;
}
inline void cut(unsigned int x,unsigned int y){
    makeroot(x);
    if(findroot(y)==x && fa[y]==x && !ch[y][0]){
        fa[y]=ch[x][1]=0;
        pushup(x);
    }
}
int main(){
    n=read();q=read();
    for(int i=1;i<=n;i++)val[i]=siz[i]=lazM[i]=1;
    for(int i=1;i<n;i++){
        int a=read();int b=read();
        link(a,b);
    }
    char opt[10];
    int u,v,d;
    while(q--){
        scanf("%s",opt);
        if(opt[0]==‘+‘){
            u=read();v=read();d=read();
            split(u,v);pushA(v,d);
        }else if(opt[0]==‘-‘){
            u=read();v=read();
            cut(u,v);
            u=read();v=read();
            link(u,v);
        }else if(opt[0]==‘*‘){
            u=read();v=read();d=read();
            split(u,v);pushM(v,d);
        }else{
            u=read();v=read();
            split(u,v);
            printf("%d\n",sum[v]);
        }
    }
    return 0;
}

其实这也揭示了数据结构间的联系:形式不同,作用相似。

透过现象看本质,通过结果推原因,都是学习数据结构的重要方式。

数据结构不只是背背代码,用来加速这么简单的,如果明白了数据结构的运行方式和原理,

你也一定会感慨里面蕴含着的发明者的智慧和它给你带来的知识上的进步。

我是灯塔...一个喜欢数据结构的OIer博主,关注我,我将给你带来更多精彩的文章。

原文地址:https://www.cnblogs.com/light-house/p/11772614.html

时间: 2024-10-09 15:14:03

[线段树系列] LCT打延迟标记的正确姿势的相关文章

【POJ】3468 A Simple Problem with Integers ——线段树 成段更新 懒惰标记

A Simple Problem with Integers Time Limit:5000MS   Memory Limit:131072K Case Time Limit:2000MS Description You have N integers, A1, A2, ... , AN. You need to deal with two kinds of operations. One type of operation is to add some given number to each

SPOJ GSS3 线段树系列1

SPOJ GSS系列真是有毒啊! 立志刷完,把线段树搞完! 来自lydrainbowcat线段树上的一道例题.(所以解法参考了lyd老师) 题意翻译 n 个数, q 次操作 操作0 x y把 Ax 修改为 y 操作1 l r询问区间 [l,r] 的最大子段和 数据规模在50000,有负数. 冷静分析 因为要维护最大子段和,那么我们可以在线段树struct中维护这么几个信息: sum(区间和).lmax(从左顶点出发的最大子段和).rmax(从右顶点出发的最大子段和).maxx(这段的最大子段和)

【线段树】线段树系列 0.2单点修改区间求和线段树

1080 线段树练习 题目描述 Description 一行N个方格,开始每个格子里都有一个整数.现在动态地提出一些问题和修改:提问的形式是求某一个特定的子区间[a,b]中所有元素的和:修改的规则是指定某一个格子x,加上或者减去一个特定的值A.现在要求你能对每个提问作出正确的回答.1≤N<100000,,提问和修改的总数m<10000条. 输入描述 Input Description 输入文件第一行为一个整数N,接下来是n行n个整数,表示格子中原来的整数.接下一个正整数m,再接下来有m行,表示

【线段树】线段树系列 0.1单点修改单点求和线段树

终于搞定了单点修改线段树...3个月..操蛋..根本没有模板题..觉得太弱了...艹蛋...必须进一步更新我的版本啊 #include<iostream> #include<cstdlib> #include<cstdio> #include<cmath> using namespace std; struct node { int left,right,value; }; node point[100]; int father[100]; int g; v

线段树,不带延迟的模板

#include<iostream> #include<stdio.h> #include<math.h> using namespace std; const int maxx = 200010; int tree[maxx<<2]; int a[maxx]; int n; void build(int root,int l,int r) { //cout<<"root:"<<root<<"

codevs 1081 线段树练习 2 线段树

题目描述 Description 给你N个数,有两种操作 1:给区间[a,b]的所有数都增加X 2:询问第i个数是什么? 输入描述 Input Description 第一行一个正整数n,接下来n行n个整数,再接下来一个正整数Q,表示操作的个数. 接下来Q行每行若干个整数.如果第一个数是1,后接3个正整数a,b,X,表示在区间[a,b]内每个数增加X,如果是2,后面跟1个整数i, 表示询问第i个位置的数是多少. 输出描述 Output Description 对于每个询问输出一行一个答案 样例输

java,线段树

线段树: 你可以理解成:线段组成的树,很多人问我,线段树到底有何用处,其实这个问题,你可以自己去刷题,然后总结出检验. 线段的具体理解,我看到一篇很好的博客,我就不展开了.博客地址:https://blog.csdn.net/iwts_24/article/details/81484561 基础题目: hdu1166敌兵布阵: 如果我们没有学过线段树,我们肯定是用模拟+暴力的方法. 模拟+暴力的方法代码: package Combat.com; import java.math.BigInteg

HDU 4509 湫湫系列故事——减肥记II(线段树-区间覆盖 或者 暴力技巧)

http://acm.hdu.edu.cn/showproblem.php?pid=4509 题目大意: 中文意义,应该能懂. 解题思路: 因为题目给的时间是一天24小时,而且还有分钟.为了解题方便,我们将小时换成分钟,那么一天24小时,总共有1440分钟.顾我就可以把一天里的任意HH:MM时间换成分钟.就这样一天的时间就变成[0,1440]区间了. 因为所给的活动最多是5*10^5,如果把活动的时间在线段[0,1440]都修改,那么时间的复杂度最坏是O(5*10^5*1440). (1)方法一

[P2894][USACO08FEB] 酒店Hotel (线段树+懒标记下传)

题意:有 n个房间,题目给出两个操作,若 op==1,就输出最靠左的连续空房数量为 x的房间序列的最左边的序号,然后将这些房间改为入住:若 op==2,就将从 x~y的的序列全部改为空房: 解法:线段树+懒标记下传: 1.线段树:题目让在一个很长的序列操作很多次,暴力显然过不了,要用线段树优化: 2.懒标记下传:这是本题的精髓.   此题要维护的不是序列上的数值总和,而是该序列的最长连续空房的长度 sum,从该节点从左向右数最长的连续空房长度 lm,以及从该节点从右向左数最长的连续空房长度 rm