Link-Cut-Tree 的基本姿势

10 篇文章 1 订阅
7 篇文章 0 订阅

Link-Cut-Tree之前经常听大神说,然而发现它是动态树的时候,我才知道是我太弱了= =

那么LCT好处都有啥?

1、支持动态加边删边(维护森林)

2、可以维护区间信息

3、所有操作可在线完成且均摊效率log(n)

LCT的结构:将整个树的边分为轻边和重边(Preferred edge)  ,与树链剖分类似(没学过请自觉面壁)

其中重边是进行操作的边,每个点只能有一个重边连接的儿子(Preferred child),于是可以证明每个点在且仅在一个重链上(没有边的点本身算作重链)

我们要做的是将重链放到splay中维护(没学过请自觉面壁),同时让splay树的根记录原树重链顶部节点的(轻边所连的)父亲,这时原树已经成为了一个概念,实际保存的是用father指针维护的各种splay,值得指出的是,splay的根不一定是真正的重链的顶点,实际的顶点应当在splay的最左端,因为对于splay的每个节点,其左儿子为它原树上的祖先,右儿子为它原树重链上的后代

这样我们要维护的数组有:f[maxn] ch[maxn][2],其中f对于每棵splay的根节点表示这个重链顶端的父亲,其余用于维护splay结构 ;ch维护splay上的左右儿子,并不表示原树上的边

根据如上定义可以给出判断一个点是否为splay 的根的方法

bool isroot(int x)
{
    return ch[f[x]][0]!=x&&ch[f[x]][1]!=x;
}

通常来说,LCT只需要拿来判断图的联通,即森林中的两点是否在同一棵树上(例题:bzoj2049),有些题目在link-cut的同时也要求维护权值,只要加一个函数maintain一下就可以支持。需要maintain的权值(sum,max等)信息随题目而异,但大体需要从splay上的两个儿子更新自身的值。以下代码中含maintain如只需判断连通可删去。

下面是LCT的基本操作


1、splay(t) 及 rotate:使t到达splay的根,与正常splay不同的是要通过儿子找到父亲再旋转,代码如下

void rotate(int k)//lift k to a higher level
{
	int t=f[k];
	bool l;
	l=(ch[t][0]!=k);
	bool r=l^1;
	if(!isroot(t)) 
		ch[f[t]][ch[f[t]][1]==t]=k;
	f[k]=f[t];
	f[t]=k;
	f[ch[k][r]]=t;
	ch[t][l]=ch[k][r];
	ch[k][r]=t;
	maintain(t);
	maintain(k);
}
void splay(int t)
{
	stack<int> stk;
	int i;
	for(i=t;!isroot(i);i=f[i])
		stk.push(i);
	stk.push(i);
	while(!stk.empty())
	{
		pushdown(stk.top());
		stk.pop();
	}
	while(!isroot(t))
	{
		int fa=f[t];
		if(!isroot(fa))
		{
			int ffa=f[fa];
			bool d1=ch[ffa][1]==fa;
			bool d2=ch[fa][1]==t;
			if(d1^d2) rotate(t);
			else rotate(fa);
		}
		rotate(t);
	}
}

pushdown的作用在第四条体现

2、access(t):使根节点到t的路径变为一个重链,且保证t所在splay上只含root-t路径上的点

具体方法:首先将t splay到所在splay_tree的根,然后断开它与其splay上右儿子,即原树上t的后代,然后找到该链的父亲将其重儿子设置为t,重复操作直到到达原树的根所在splay的根,也就是没有父亲的点

void access(int t)
{
	int tmp=0;
	while(t)
	{
		splay(t);
		ch[t][1]=tmp;
		tmp=t;
		maintain(t);
		t=f[t];
	}
}

3、root(t) 找到t所在原树的根,用于判断两点之间是否连通

代码1:真正找到所在原树的根,这需要将t与root相连,然后确保t在splay根节点后向左走

int root(int t)
{
	access(t);
	splay(t);
	while(ch[t][0]) t=ch[t][0];
	return t;
}
代码2:找到 原树的根所在splay的根,同样可以判断连通,而且代码更加简洁,但不够严谨

int root(int t)
{
    while(f[t]) t=f[t];
    return t;
}

4、moveroot(t) :将t所在原树的树根设为t

void moveroot(int t)
{
	access(t);
	splay(t);
	rev[t]^=1;
}
rev是反转标记,这段代码的用意是将root-t路径分离出来,然后对条路径上的儿子-父亲关系进行反转,体现在splay上就是区间翻转的操作 注意 换根操作只对root-t路径产生影响

pushdown函数就是为了支持rev的翻转操作

void pushdown(int t)
{
	if(rev[t])
	{
		rev[ch[t][0]]^=1;
		rev[ch[t][1]]^=1;
		rev[t]=0;
		swap(ch[t][0],ch[t][1]);
	}
}
5、split(x,y)

将x到y的路径分离出来放到splay中且保证y为splay的根,若需要维护路径信息,y上的值即为答案

void split(int x,int y)
{
	moveroot(x);
	access(y);
	splay(y);
}
6、cut(x,y)

void try_cut(int x,int y)
{
	split(x,y);
	if(ch[y][0]==x&&!ch[x][0]&&!ch[x][1])//判断节点是否有边
	{
		ch[y][0]=f[x]=0;
		maintain(y);
	}
}
7、link(x,y)

void link(int x,int y)
{
	moveroot(x);//保证f[x]为空
	f[x]=y;
}
附:bzoj2049代码

/**************************************************************
    Problem: 2049
    User: Leo_h
    Language: C++
    Result: Accepted
    Time:4800 ms
    Memory:992 kb
****************************************************************/
 
#include<cstdio>
#include<algorithm>
#include<stack>
using namespace std;
#define maxn 10005
int n,m;
int f[maxn];
int ch[maxn][2];
int rev[maxn];
bool isroot(int x)
{
    return ch[f[x]][0]!=x&&ch[f[x]][1]!=x;
}
void rotate(int k)//lift k to a higher level
{
    int t=f[k];
    bool l;
    l=(ch[t][0]!=k);
    bool r=l^1;
    if(!isroot(t)) 
        ch[f[t]][ch[f[t]][1]==t]=k;
    f[k]=f[t];
    f[t]=k;
    f[ch[k][r]]=t;
    ch[t][l]=ch[k][r];
    ch[k][r]=t;
}
void pushdown(int t)
{
    if(rev[t])
    {
        rev[ch[t][0]]^=1;
        rev[ch[t][1]]^=1;
        rev[t]=0;
        swap(ch[t][0],ch[t][1]);
    }
}
void splay(int t)
{
    stack<int> stk;
    int i;
    for(i=t;!isroot(i);i=f[i])
        stk.push(i);
    stk.push(i);//you modified this
    while(!stk.empty())
    {
        pushdown(stk.top());
        stk.pop();
    }
    while(!isroot(t))
    {
        int fa=f[t];
        if(!isroot(fa))
        {
            int ffa=f[fa];
            bool d1=ch[ffa][1]==fa;
            bool d2=ch[fa][1]==t;
            if(d1^d2) rotate(t);
            else rotate(fa);
        }
        rotate(t);
    }
}
void access(int t)
{
    int tmp=0;
    while(t)
    {
        splay(t);
        ch[t][1]=tmp;
        tmp=t;
        t=f[t];
    }
}
void moveroot(int t)
{
    access(t);
    splay(t);
    rev[t]^=1;
}
int root(int t)
{
    while(f[t]) t=f[t];
    return t;
}
void link(int x,int y)
{
    moveroot(x);
    f[x]=y;
}
void split(int x,int y)
{
    moveroot(x);
    access(y);
    splay(y);
}
void cut(int x,int y)
{
    split(x,y);
    ch[y][0]=f[x]=0;
}
int main()
{
    scanf("%d%d",&n,&m);
    char str[10];
    int f1,f2;
    for(int i=1;i<=m;i++)
    {
        scanf("%s%d%d",str,&f1,&f2);
        switch(*str)
        {
            case 'C':
                link(f1,f2);
                break;
            case 'D':
                cut(f1,f2);
                break;
            case 'Q':
                if(root(f1)==root(f2)) printf("Yes\n");
                else printf("No\n");
                break;
        }
    }
}







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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值