• SPLAY,LCT学习笔记(四)


    前三篇好像变成了SPLAY专题...

    这一篇正式开始LCT!

    其实LCT就是基于SPLAY的伸展操作维护树(森林)连通性的一个数据结构

    核心操作有很多,我们以一道题为例:

    例:bzoj 2049 洞穴勘测

    要求:加边和删边,询问连通性

    其实如果没有删边,裸跑并查集似乎就可以搞定

    但由于存在删边,并查集思想受阻,我们要考虑更高级的数据结构

    于是LCT横空出世!

    LCT的核心思想:多棵SPLAY维护虚实树链

    首先介绍一下树链剖分问题:

    树链剖分问题是将一棵树划分成多条树链的思想,有很多种剖分方法,比如轻重树链剖分(最常见,最常用),长短树链剖分(在部分题目中可以替代树上的dsu算法,而且无论码量还是时间复杂度都是很优越的),以及LCT要使用的虚实树链剖分

    所谓虚实树链剖分,就是讲一棵树的边划分为实边和虚边,对于实边连起来的一条链用一个SPLAY维护

    举个例子:

    这是一棵树(废话) 

    我们对他进行一下虚实剖分,如下:

    如图,用实线表示的是实边,用虚线表示的是虚边,而我们用一些SPLAY维护这些链,就是:

     如图,用黑色实线连起来的点表示一个SPLAY,用黑色虚线框圈起来的是一个SPLAY,用蓝色虚线连的边表示父亲指针

    为什么要这样建立SPLAY?

    LCT维护树链的SPLAY有一个原则:每棵SPLAY的中序遍历会产生一个序列,而这个序列所对应的树链深度是单调递增的!

    这是很重要的一个性质

    接下来还有一个性质(当然在上面那张图上体现不太出来):就是每棵SPLAY的根节点的父亲一定要指向这棵SPLAY中中序遍历最靠前的那个点在原树中的父节点

    换言之,我们不一定能保证SPLAY的根是深度最浅的,但是他的父亲一定要指向SPLAY维护的节点中深度最浅者的父节点

    可是还要注意一点,如图,虽然存在9->4,10->4,8->4三个父亲指向,但是4的儿子只有一个,就是8,剩下的并不计入4的子节点中(即人们常说的“认父不认子”)

    接下来我们就可以进行一些操作了:

    首先,本身我们除了上述原则以外,如何进行虚实树链剖分是无所谓的,所以我们完全可以重构这些SPLAY,使得两个点被同一棵SPLAY维护

    怎么操作?

    我们使用一个函数叫access,使得一个点与整棵树的根所连接的

    现在假设我们要把8号节点和1号节点放到一棵SPLAY里,我们怎么办呢?

    首先,我们把8号节点转到本身SPLAY的根上

    这一点很容易,根据SPLAY的伸展操作,双旋即可

    不会旋转的详见前三篇SPLAY专题

    (这里顺便说一句:由于在LCT中SPLAY只有一种旋转方式,就是将某一点旋转到SPLAY的根,所以旋转函数会比原来好写一些)

    至于如何找到这个SPLAY的根,方法也很简单:一个SPLAY的根一定不会是任意一个SPLAY的儿子(废话),所以仅需找到这个点的父亲,看看这个点的父亲的儿子中有没有他就可以了,如果没有这个点就是根

    剩下的旋转操作就和普通SPLAY一样了。

    判断这个点是不是根:

    bool berot(int rt)
    {
        if(c[f[rt]][0]==rt||c[f[rt]][1]==rt)
        {
            return 0;
        }
        return 1;
    }

    将某一点旋转到根的位置上:

    void rotate(int rt)
    {
        int ltyp=0;
        int fa=f[rt];
        int ffa=f[fa];
        if(c[fa][1]==rt)
        {
            ltyp=1;
        }
        if(!berot(fa))
        {
            if(c[ffa][1]==fa)
            {
                c[ffa][1]=rt;
            }else
            {
                c[ffa][0]=rt;
            }
        }
        c[fa][ltyp]=c[rt][ltyp^1];
        c[rt][ltyp^1]=fa;
        f[c[fa][ltyp]]=fa;
        f[fa]=rt;
        f[rt]=ffa;
        update(fa);
    }
    void splay(int rt)
    {
        repush(rt);
        while(!berot(rt))
        {
            int fa=f[rt];
            int ffa=f[fa];
            if(!berot(fa))
            {
                if((c[fa][0]==rt&&c[ffa][0]!=fa)||(c[fa][1]==rt&&c[ffa][1]!=fa))
                {
                    rotate(rt);
                }else
                {
                    rotate(fa);
                }
            }
            rotate(rt);
        }
        update(rt);
    }

    这是access中所需要的操作之一

    接下来access操作就很简单了,因为现在我们已经让8号点到了根节点的位置上,如果用图来看,就是:

    (所以说了那么多,其实我们只干了这么点事而已...)

    接下来,我们就要把8扔进1所在的SPLAY里了

    这就是access函数的第二步

    首先,如果想把8扔进去,那么8就要在原来的SPLAY里面作为一个节点(废话)

    作为哪个节点呢?

    显然是右儿子!

    为什么?

    因为这个点的深度一定要比整个树链中深度最浅的点的父亲的深度深!(读十遍)

    所以自然扔到右儿子去

    什么?原来的右儿子怎么办?

    不管...

    反正LCT是“认父不认子”的...

    access代码:

    void access(int rt)
    {
        int y=0;
        while(rt)
        {
            splay(rt);
            c[rt][1]=y;
            update(rt);
            y=rt;
            rt=f[rt];
        }
    }

    所以,打通了以后,这个图会变成这样:

    (忘记编号了,不过位置都没变,所以应该没啥事...) 

    (原谅我越来越丑陋的画风...)

    这就完成了我们所需要的操作,接下来我们基本就可以“为所欲为”了

    首先,我们可以对这棵树进行换根!

    因为我们发现,在我们访问一条树链的时候,有很大概率这条树链本身并不能满足深度单调递增

    那这样的树链是不能用同一棵SPLAY来维护的

    可是我们就需要操作这个树链啊

    那我们把树根换掉不就好了吗

    所以假设我们需要操作树链(u,v),我们仅需将u转成树根,然后将v用access操作转进去就可以啦

    于是问题变成了怎么把u转成树根

    我们使用一个操作叫makeroot,表示把某一点变成树根

    怎么变?直接拽上去?

    你说对了...

    首先,我们把u点用access操作转到和根节点在同一个SPLAY里

    然后,我们把u点用SPLAY操作直接转到树根上

    最后,我们翻转整个SPLAY即可

    为什么?

    首先,当我们access一个节点后,这个点一定是没有右子树的(很显然啊,access过程中把它置0了...)

    那么这个点又被放在了原树的右节点上,那这不说明access后,这个节点在SPLAY中一定是深度最大的点吗?

    那我们把他转到根节点上,再翻转整个SPLAY,不就让这个点成为了深度最大的点吗?(别忘了,LCT要求中序遍历深度单调递增啊)

    这不就搞定了吗

    所以操作如下:

    void makeroot(int rt)
    {
        access(rt);
        splay(rt);
        reverse(rt);//翻转整个SPLAY
    }

    有了这个操作,剩下所有操作都是顺理成章的了

    连边:如果想从(u,v)连一条边,那么我们可以首先把u转到树根上,然后直接连边即可

    void link(int st,int ed)
    {
        makeroot(st);
        if(getroot(ed)==st)
        {
            return;
        }
        f[st]=ed;
    }

    删边:把u转到树根上,直接删边即可

    void cut(int st,int ed)
    {
        makeroot(ed);
        if(getroot(st)==ed&&f[ed]==st&&!c[ed][1])
        {
            c[st][0]=f[ed]=0;
            update(st);
        }
    }

    抽出一条从u到v的树链:

    void split(int st,int ed)
    {
        makeroot(st);
        access(ed);
        splay(ed);
    }

    剩下基本就随便搞了

    贴下bzoj 2049代码

    #include <cstdio>
    #include <cmath>
    #include <cstring>
    #include <cstdlib>
    #include <iostream>
    #include <algorithm>
    #include <queue>
    #include <stack>
    #define which(x) (c[f[x]][1]==x)
    #define ls tree[rt].lson
    #define rs tree[rt].rson
    using namespace std;
    int c[100005][2];
    int f[100005];
    bool ttag[100005];
    char s[10];
    bool berot[100005];
    int n,m;
    void reverse(int rt)
    {
    	ttag[rt]^=1;
    	swap(c[rt][0],c[rt][1]);
    }
    void pushdown(int rt)
    {
    	if(ttag[rt])
    	{
    		reverse(c[rt][0]);
    		reverse(c[rt][1]);
    		ttag[rt]=0;
    	}
    }
    void repush(int rt)
    {
    	if(!berot[rt])
    	{
    		repush(f[rt]);
    	}
    	pushdown(rt);
    }
    void rotate(int rt)
    {
        int ltyp=0;
        int fa=f[rt];
        int ffa=f[fa];
        if(c[fa][1]==rt)
        {
            ltyp=1;
        }
        if(berot[fa])
        {
            berot[fa]=0;
            berot[rt]=1;
        }else
        {
            if(c[ffa][1]==fa)
            {
                c[ffa][1]=rt;
            }else
            {
                c[ffa][0]=rt;
            }
        }
        c[fa][ltyp]=c[rt][ltyp^1];
        c[rt][ltyp^1]=fa;
        f[c[fa][ltyp]]=fa;
        f[fa]=rt;
        f[rt]=ffa;
    }
    void splay(int rt)
    {
    	repush(rt);
    	while(!berot[rt])
    	{
    		int fa=f[rt];
    		int ffa=f[fa];
    		if(!berot[fa])
    		{
    			if((c[fa][0]==rt&&c[ffa][0]!=fa)||(c[fa][1]==rt&&c[ffa][1]!=fa))
    			{
    				rotate(rt);
    			}else
    			{
    				rotate(fa);
    			}
    		}
    		rotate(rt);
    	}
    }
    void access(int rt)
    {
    	int y=0;
    	while(rt)
    	{
    		splay(rt);
    		berot[c[rt][1]]=1;
    		berot[y]=0;
    		c[rt][1]=y;
    		y=rt;
    		rt=f[rt];
    	}
    }
    void getroot(int rt)
    {
    	access(rt);
    	splay(rt);
    	reverse(rt);
    }
    void link(int st,int ed)
    {
    	getroot(st);
    	f[st]=ed;
    }
    void cut(int st,int ed)
    {
    	getroot(st);
    	access(ed);
    	splay(st);
    	c[st][1]=f[ed]=0;
    	berot[ed]=1;
    }
    bool check(int st,int ed)
    {
    	getroot(st);
    	access(ed);
    	splay(st);
    	while(!berot[ed])
    	{
    		ed=f[ed];
    	}
    	if(st==ed)
    	{
    		return 1;
    	}else
    	{
    		return 0;
    	}
    }
    int main()
    {
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++)
    	{
    		berot[i]=1;
    	}
    	for(int i=1;i<=m;i++)
    	{
    		scanf("%s",s);
    		int x,y;
    		scanf("%d%d",&x,&y);
    		if(s[0]=='C')
    		{
    			link(x,y);
    		}else if(s[0]=='Q')
    		{
    			if(check(x,y))
    			{
    				printf("Yes
    ");
    			}else
    			{
    				printf("No
    ");
    			}
    		}else
    		{
    			cut(x,y);
    		}
    	}
    }
  • 相关阅读:
    关于信号的一些知识
    压缩感知综合理解篇
    稀疏编码之字典学习
    Iterator和for...of
    ES6中的Symbol
    原生node写一个静态资源服务器
    css中的流,元素,基本尺寸
    Node-Blog整套前后端学习记录
    mongodb 和 mongoose 初探
    Express 初步使用
  • 原文地址:https://www.cnblogs.com/zhangleo/p/10764213.html
Copyright © 2020-2023  润新知