Link Cut Tree 初探(模板)

        之前说了Splay Tree 和 树链剖分,其实都是为今天Link Cut Tree 做铺垫,在这里首先要膜拜鼻祖杨哲大牛orz…… 

        杨哲——《QTREE解法的一些研究》:点击打开链接  这个是官方论文的介绍。

        我在学习的时候在网上看到,有人说Link Cut Tree其实是树链剖分+Splay,但是学完之后发现这个说法其实并不妥,首先还是一样介绍一下几个概念:

                1、preferred path:在访问(access)一个点后,从根节点到该点的路径(链)称为preferred path

                2、preferred chrild:对于一个节点,若它的一个儿子在preferred path上,则这个儿子就是preferred chrild

                3、Auxiliary Tree:对于每一条preferred path,我们都用一棵树来维护它的性质,这样的树称为辅助树(Auxiliary Tree)

        介绍完这几个概念后,你可能发现了Link Cut Tree与树链剖分的相似之处,就是每一条preferred path相当于一条树链,然后经过一次次的access,也就把一棵树剖分成了很多条preferred path(链)。但是我想说,看起来想,但是Link Cut Tree的剖分方式并不是按照轻重儿子去剖,而是单纯的看access的方式,而且如果是access(i)那么,这条preferred path并不会包含i的儿子,这是与树链剖分不同的地方。

        既然如此,可能你会说,既然有了树链剖分,那么还要Link Cut Tree干嘛?告诉你,Link Cut Tree顾名思义,是一个可以Link也可以Cut的Tree。具体点就是,树链剖分相当于只是一个静态的问题,它的树的结构(父子关系)不会变,也不会要求添加或者删除某一条边,而Link Cut Tree可支持动态的改变树的结构,可以任意对树进行切断和重(chong)连。这个怎么做到的呢?别急,慢慢来……

        然后,我们再说说这个Auxiliary Tree。一般来说我们选择Splay Tree来维护一条链。一开始,每个点独立地属于一棵Splay Tree(我当时就想,这样不会MLE吗?其实这可以很巧妙的解决,后面会说)。接下来access后便把这些Auxiliary Tree合并了。

        首先,说说最重要的access操作,Link Cut Tree精髓所在。access(i),即访问i这个节点,同时把从根一直到改点的路径标记为preferred path,并把这路径上的点交给一棵Auxiliary Tree来维护。我初学时就纳闷,一个节点即属于原树,又属于Auxiliary Tree,它的父亲到底是那棵树里的父亲,若为Auxiliary Tree的父亲,是否会丢掉原来的关系?其实,不必纠结。在原树中,我们只保存某个节点的父亲而不保存儿子,当该点进入Auxiliary Tree后才保存左右儿子的信息。然后设置root[]数组,若root[i]==1则代表点i是一棵Auxiliary Tree的根。对于每一个Splay Tree,我们约定,深度:左儿子<父亲<右儿子。access(i)时,先splay(i),把i转到它所在的Auxiliary Tree的根(当然一开始它已经是根)。然后把根变成i的右儿子,因为preferred path不包括i的儿子。接着,更新右儿子,然后继续往上走,一直走到原树的根。

                                                       

        粗线表示preferred path,左边为开始状态,右边为access(N)之后。可以看见,从根A到N所有的边都被标记为粗,在代码中就表现为A的右儿子是C,C的右儿子是G,G的右儿子是H……然后原来不是Auxiliary Tree的根的K、J、O都变成了根,具体见代码,这个还是不容易理解的,只能说到这了:

inline void access(int x)			//仅仅只是标记preferred path,并没有把x变为根,若要把x变为根,加一个splay(x) 
{
	int y=0;
	while (x)				//没到原树的根就一直做
	{
		splay(x);			//把x变为Auxiliary Tree的根
		root[tree[x].r]=1;		//把右儿子标记为根
		tree[x].r=y;			//更新儿子
		root[y]=0;			//path的其它点不为根
		y=x;				//往上继续走
		x=tree[x].fa;
	}
}

        然后Link Cut Tree中rotate操作与splay操作也有所不同:

inline void rotate(int x)
{
	int fa=tree[x].fa,grand=tree[fa].fa;
	if (get(x))
	{
		tree[fa].r=tree[x].l;
		tree[tree[fa].r].fa=fa;
		tree[x].l=fa;
	} else
	{
		tree[fa].l=tree[x].r;
		tree[tree[fa].l].fa=fa;
		tree[x].r=fa;
	}
	tree[x].fa=grand;
	tree[fa].fa=x;
	if (grand) 
	{
		if (tree[grand].r==fa) tree[grand].r=x;
		else if (tree[grand].l==fa) tree[grand].l=x;         //用else if不能直接用else,因为原树不一定是二叉树  
	}
	root[x]=root[x] xor root[fa];             	  //用xor快速交换root的值 
	root[fa]=root[x] xor root[fa];			  //根的传递 
	update(fa); 
	update(x);
}

inline void splay(int x)
{
	while (!root[x]) rotate(x);			  //当x为Auxiliary Tree的根的时候停止
}

        接下来是最感兴趣的Link和Cut。Link:

inline void link(int x,int y)
{
  access(x);
  splay(x);
  reverse(tree[x].l);
  access(y);
  splay(y);
  tree[y].r=x;
  tree[x].fa=y;
  root[x]=0;} 

 
Cut:
inline void cut(int x)  
{  
  access(x);  
  splay(x);//旋转x点到根节点  
  tree[tree[x].l].fa=0;  
  root[tree[x].l]=true;//设置左子树根节点  
  tree[x].l=-1;  
}

       Link Cut Tree 差不多就是这样了,接下来又是时间复杂度,可以证明(我就不证明了,去看杨哲大牛的论文把)时间复杂度为均摊的O(NlogN)。然后模板题的话是BZOJ 2002 弹飞绵羊:http://www.lydsy.com/JudgeOnline/problem.php?id=2002

      这题对于一个弹簧,若他在位置i,弹力为k,则连边(i,i+k),若i+k>n+1,则连边(i,n+1),最后就会形成一棵以n+1为根的树。然后询问在某个位置经过几次弹射能到n+1之外,那么问题就转化为了求该点在这棵树的深度。然后,对于修改,则正好可用Link和Cut来完成。

代码如下:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<iomanip>
#define LL long long
#define INF 2147483647
#define ENT putchar('\n')
#define up(_i,_a,_b) for (int _i=_a;_i<=_b;_i++)
#define down(_i,_a,_b) for (int _i=_a;_i>=_b;_i--)
#define efo(_i,_a) for (int _i=ls[_a];_i!=0;_i=g[_i].next)
#define file(x) freopen(#x".in","r",stdin),freopen(#x".out","w",stdout);
using namespace std;
inline int read()
{
         LL f=1,d=0;char s=getchar();
         while (s<'0'||s>'9'){if (s=='-') f=-1;s=getchar();}
         while (s>='0'&&s<='9'){d=d*10+s-'0';s=getchar();}
         return f*d;
}
inline LL readln()
{
       LL f=1,d=0;char s=getchar();
       while (s<'0'||s>'9'){if (s=='-') f=-1;s=getchar();}
       while (s>='0'&&s<='9'){d=d*10+s-'0';s=getchar();}
       while (s!='\n') s=getchar();
       return f*d;
}
inline void write(LL x)
{
    if(x==0){putchar('0');return;}if(x<0)putchar('-'),x=-x;
    int len=0,buf[20];while(x)buf[len++]=x%10,x/=10;
    int i=len-1; while (i>=0) {putchar(buf[i--]+'0');}return;
}
inline void writeln(LL x) {write(x);ENT;}
 
struct node
{
    int l,r,fa,num,cnt,size;   //cntΪ¸Ã¹Ø¼ü×Ö³öÏÖ´ÎÊý£¬sizeΪ×ÓÊ÷´óС 
} tree[200010];
 
bool root[200010];
int sz;                  //szΪÕû¿ÃÊ÷µÄ´óС 
 
inline int get(int x)        //¿´xÊÇ×ó¶ù×Ó»¹ÊÇÓÒ¶ù×Ó 
{
    return tree[tree[x].fa].r==x;
}
 
inline void update(int x)   //¸üÐÂ×ÓÊ÷µÄsize    
{
    if (x)
    {
        tree[x].size=tree[x].cnt;
        if (tree[x].l) tree[x].size+=tree[tree[x].l].size;
        if (tree[x].r) tree[x].size+=tree[tree[x].r].size;
    }
}
 
inline void rotate(int x)
{
    int fa=tree[x].fa,grand=tree[fa].fa;
    if (get(x))
    {
        tree[fa].r=tree[x].l;
        tree[tree[fa].r].fa=fa;
        tree[x].l=fa;
    } else
    {
        tree[fa].l=tree[x].r;
        tree[tree[fa].l].fa=fa;
        tree[x].r=fa;
    }
    tree[x].fa=grand;
    tree[fa].fa=x;
    if (grand) 
    {
        if (tree[grand].r==fa) tree[grand].r=x;
        else if (tree[grand].l==fa) tree[grand].l=x;       //ÓÃelse if²»ÄÜÖ±½ÓÓÃelse£¬ÒòΪԭÊ÷²»Ò»¶¨ÊǶþ²æÊ÷ 
    }
    root[x]=root[x] xor root[fa];             //ÓÃxor¿ìËÙ½»»»rootµÄÖµ 
    root[fa]=root[x] xor root[fa];
    update(fa); 
    update(x);
}
 
inline void splay(int x)
{
    while (!root[x]) rotate(x);
}
 
inline void access(int x)
{
    int y=0;
    while (x)
    {
        splay(x);
        root[tree[x].r]=1;
        tree[x].r=y;
        root[y]=0;
        y=x;
        x=tree[x].fa;
    }
}
 
int main()
{
    int n,m;
    n=read();
    up(i,1,n)
    {
        tree[i].fa=min(read()+i,n+1);
        tree[i].size=tree[i].cnt=1;
    }
    tree[n+1].size=tree[n+1].cnt=1;
    memset(root,1,sizeof(root));
    m=read();
    up(i,1,m)
    {
        int q,x,k;
        q=read();
        x=read()+1;
        if (q==1)
        {
            access(x);
            splay(x);
            writeln(tree[tree[x].l].size);
        } else
        {
            k=read();
            splay(x);
            tree[tree[x].l].fa=tree[x].fa;
            root[tree[x].l]=1;                     //reverseʱ±ðÍüÁ˰Ѷù×Ó±ê¼ÇΪ¸ù 
            tree[x].l=0;
            update(x);
            tree[x].fa=min(k+x,n+1);
        }
    }
}


                
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值