P2147 [SDOI2008] 洞穴勘测 Splay+LCT详解

动态树算法——LCT

求点赞!!!(如果觉得我讲的还好!!!谢谢!!!)


用途

在线加边或删边、查询连通性、换根等

主要函数

access()、makeroot()、findroot()

link()、split()、cut()、splay()、rotate()、pushup()、pushdown()、isroot()、get()等

前置知识:Splay

splay 是建立在平衡树基础上的, 所谓平衡树,就是对于任意结点x来说,它的左儿子为根的子树上任意一个数都小于x,右子树为根的子树上任意一个数都大于x。

比如上面这张图:

当然,也不乏一些毒瘤的情况,例如右上的链,效率真的很低, 于是各大佬们日思夜想,tarjan大佬搞出了一个叫splay的东西。

splay就是通过旋转来改变链状结构,使查询效率变高。

例如下面这张图:(我画的好丑哈哈哈哈哈

 如果我现在进行旋转操作,把X节点放到P节点的位置上, 那么根据平衡树的规则,P>X,变为X的右子树,C>P, 仍为P的右子树,但是B就得挪位置了,因为X的右儿子改为P了。可以发现,X<B<P, 所以挂在P的左节点上, 就变成了这样:

我们可以发现,如果原树中,X是P的左儿子,那么因为X<P, 旋转后,P就一定会是X的右儿子,而原来X的右儿子(子树)B一定会是P的左儿子。

相似地,如果原树中,X是P的右儿子,那么因为X>P, 旋转后,P就一定会是X的左儿子,而原来X的左儿子(子树)B一定会是P的右儿子。

那么我们的rotate函数就可以这样写:

void rotate(int x)//X是要旋转的节点
{
	int y=fa[x];//父亲
	int z=fa[y];//祖父
	bool k1=(ch[y][1]==x),k2=(ch[z][1]==y);//分别判断儿子方向 
    if(!isroot(y))//FOR LCT: 判断是否是根 
    	ch[z][k2]=x;
    fa[x]=z;//孙子变成爷爷的儿子 
    ch[y][k1]=ch[x][!k1];
    fa[ch[x][!k1]]=y; //孙子的儿子变成爸爸的儿子 
    ch[x][!k1]=y;
    fa[y]=x;//爸爸孙子关系调换 
}

接下来就是多次旋转使节点X变为根节点。采取的是每一次取三个节点,则分成以下四种情况:

 yay 终于画完了

void splay(int x, int goal)//将x旋转为goal的儿子,如果goal是0则旋转到根
{
	while(t[x].ff!=goal)//一直旋转到x成为goal的儿子
	{
		int y=t[x].ff,z=t[y].ff;//父节点祖父节点
		if(z!=goal)//如果Y不是根节点,则分为上面两类来旋转
			(t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
			//这就是之前对于x和y是哪个儿子的讨论
		rotate(x);//无论怎么样最后的一个操作都是旋转x
	}
	if(goal==0)root=x;//如果goal是0,则将根节点更新为x
}
//普通splay写法 代码来源 https://www.cnblogs.com/cjyyb/p/7499020.html

//LCT中的splay写法不同就是在于出现虚边的情况需要判断是否是节点所位于的splay的根
int isroot(int rt){//是否是根
    return ch[fa[rt]][0]!=rt&&ch[fa[rt]][1]!=rt;
}
void push_up(int x)
{
    sz[x]=1;
    if(ch[x][0])
        sz[x]+=sz[ch[x][0]];
    if(ch[x][1])
        sz[x]+=sz[ch[x][1]];
    //recalculate size of subtrees
}
void splay(int rt)
{
    int top=0,stack[MAXN];//手写栈
    stack[++top]=rt;//第一个一定是根
    for(int i=rt;!isroot(i);i=fa[i])
		stack[++top]=fa[i];
    while(top)pushdown(stack[top--]);//暴力reverse,下传lazy_tag
    while(!isroot(rt)){//rt非根 
        int x=fa[rt],y=fa[x];//父亲和祖父节点
        if(!isroot(x)){//仍然非根
            if((ch[x][0]==rt)^(ch[y][0]==x))turn(rt);
            else turn(x);
        }
        turn(rt);
    }
    push_up(rt);
}

呼~splay到这就结束啦~ 

正式LCT开始

好吧第一次写这么长的题解hhhh(当然我一共也就写过两篇xswl

Access(x)

目的:打通一条从原树的根到X节点的实链。

步骤:

1. 

以下access转载自LCT总结——概念篇+洛谷P3690[模板]Link Cut Tree(动态树)(LCT,Splay) - Flash_Hu - 博客园为了优化体验(其实是强迫症),蒟蒻把总结拆成了两篇,方便不同学习阶段的Dalao们切换。 LCT总结——应用篇戳这里 概念、性质简述 首先介绍一下链剖分的概念(感谢laofu的讲课) 链剖分,是指一类https://www.cnblogs.com/flashhu/p/8324551.html

有一棵树,假设一开始实边和虚边是这样划分的(虚线为虚边)

那么所构成的LCT可能会长这样(绿框中为一个Splay,可能不会长这样,但只要满足中序遍历按深度递增(性质1)就对结果无影响)


现在我们要access(N),把A−N的路径拉起来变成一条Splay。
该路径上其它链都要给这条链让路,也就是把每个点到该路径以外的实边变虚。
所以我们希望虚实边重新划分成这样。
然后怎么实现呢?
我们要一步步往上拉。
首先把splay(N),使之成为当前Splay中的根。
为了满足性质2,原来N−O的重边要变轻。(我认为说实边变虚边更为准确,仅个人观点)
因为按深度O在N的下面,在Splay中O在N的右子树中,所以直接单方面将N的右儿子置为0(认父不认子)(最后一个是“0”零,不是字母“O”哈)
然后就变成了这样——

我们接着把N所属Splay的虚边指向的I(在原树上是L的父亲)也转到它所属Splay的根,splay(I)。
原来在I下方的重(实)边I−K要变轻(同样是将右儿子去掉)。
这时候I−L就可以变重了。因为L肯定是在I下方的(刚才L所属Splay指向了I),所以I的右儿子置为N,满足性质1。
然后就变成了这样——

I指向H,接着splay(H),H的右儿子置为I。

H指向AA,接着)splay(A),A的右儿子置为H。

差不多就和大佬说的一样,个人觉得实边虚边表述更好,否则容易和树链剖分混起来。

步骤总结一下:

(循环完成)

1. 将X节点转到根

2.换儿子

3. 更新信息(push_up)

4. X节点变为自己的父亲,回到1

void access(int x)
{//将 x 与 x所在树的根 连一条链
    for(int y=0;x;y=x,x=fa[x])
	{
        splay(x);
        ch[x][1]=y;
    }
}

 Makeroot(x)

目的:让X节点成为原树的根

步骤:

1. access(x), 让x成为当前splay中深度最大的节点。

2. splay(x), 让x成为splay树的根

3. 反转splay, 使x成为中序遍历下的最小节点(即原树中的根节点)

void makeroot(int x)
{
	access(x);
	splay(x);
	flag[x]^=1;
}//将 x 变为树根

Findroot(x)

目的:找到x节点在原树的根

步骤:

1. access(x), 让x节点与根节点在同一splay中

2. splay(x), 让根节点成为以x为根的splay中最左边的节点(因为根节点最小嘛)

3. 寻找最左边的节点,记得之前翻转过吗?别忘了这时候执行一下~

int findroot(int x)
{//找树根
    access(x);
	splay(x);
    while(ch[x][0])
    {
    	pushdown(x);
    	x=ch[x][0];//一直往左走
	}
	splay(x);//随机化保证复杂度 
    return x;
}

 

下面附上完整代码 of P2147 [SDOI2008] 洞穴勘测 

#include<iostream>
#include<algorithm>
#include<cstdio>
#define MAXN 10010//数组大小
#define MAX 999999999//极值
using namespace std;
int n,m;

int ch[MAXN][2];
int fa[MAXN], flag[MAXN];

bool get(int x) { return ch[fa[x]][1]==x; }

void pushdown(int rt){//标记下传
    if(!rt||!flag[rt]) 
		return;
	flag[ch[rt][0]]^=1;
	flag[ch[rt][1]]^=1;
	flag[rt]^=1;
    swap(ch[rt][0],ch[rt][1]);
}
inline int isroot(int rt){
    return ch[fa[rt]][0]!=rt&&ch[fa[rt]][1]!=rt;
}

void turn(int x)
{
    int y=fa[x],z=fa[y],k=get(x);
    if(!isroot(y)){
        if(ch[z][0]==y)ch[z][0]=x;
        else ch[z][1]=x;
    }
    ch[y][k]=ch[x][!k]; fa[ch[x][!k]]=y;
    ch[x][!k]=y; fa[y]=x;
    fa[x]=z;

}

void splay(int rt)
{
    int top=0,stack[MAXN];
    stack[++top]=rt;
    for(int i=rt;!isroot(i);i=fa[i])
		stack[++top]=fa[i];
    while(top)pushdown(stack[top--]);
    while(!isroot(rt)){
        int x=fa[rt],y=fa[x];
        if(!isroot(x)){
            if((ch[x][0]==rt)^(ch[y][0]==x))turn(rt);
            else turn(x);
        }
        turn(rt);
    }
}


void access(int x)
{//将 x 与 x所在树的根 连一条链
    for(int y=0;x;y=x,x=fa[x])
	{
        splay(x);
        ch[x][1]=y;
    }
}
void makeroot(int x)
{
	access(x);
	splay(x);
	flag[x]^=1;
}//将 x 变为树根
int findroot(int x)
{//找树根
    access(x);
	splay(x);
    while(ch[x][0])
    {
    	pushdown(x);
    	x=ch[x][0];
	}
	splay(x);//随机化保证复杂度 
    return x;
}
inline void split(int x,int y)
{
	makeroot(x);
	access(y);
	splay(y);
}
void cut(int x,int y)
{
    split(x,y);
    ch[y][0]=0;
    fa[x]=0;
}
void link(int x,int y)
{
	makeroot(x);
	fa[x]=y;
}
int main()
{
    char c[10];
    int x,y;
    scanf("%d %d",&n,&m); 
    while(m--)
	{
        scanf("%s",c);
		scanf("%d %d",&x,&y);
        if(c[0]=='C')link(x,y);
        if(c[0]=='D')cut(x,y);
        if(c[0]=='Q')
		{
            int tmp2=findroot(y);
			if(findroot(x)==tmp2)
				printf("Yes\n");
            else 
				printf("No\n");
        }
    }
    return 0;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值