Link-Cut-Tree 动态树算法

Link-Cut-Tree 动态树算法总结

动态树是一类要求维护森林连通性的算法总称,其中最常用的就是lct (Link-Cut-Tree).

lct 支持一下操作

链上求和 
链上求最值
链上修改 (前三项均可用树链剖分+线段树实现)
断开树上的一条边
连接两个点,保证连接后仍然是一棵树
判断森林连通性

说到lct,自然需要引入一些概念:
Preferred Child:重儿子(为了便于理解这里沿用树链剖分中的命名),重儿子与父亲节点同在一棵Splay中,一个节点最多只能有一个重儿子
Preferred Edge:重(zhòng)边,连接父亲节点和重儿子的边
Preferred Path:重链,由重边及重边连接的节点构成的链
由一条重链上的所有节点所构成的Splay称作这条链的辅助树
每个点的关键值为这个点的深度,即这棵Splay的中序遍历是这条链从链顶到链底的所有节点构成的序列
辅助树的根节点的父亲指向链顶的父亲节点,然而链顶的父亲节点的儿子并不指向辅助树的根节点
Link-Cut-Tree 动态树算法_第1张图片

原树与辅助树的关系
原树中的重链 -> 辅助树中两个节点位于同一棵Splay中
原树中的轻链 -> 辅助树中子节点所在Splay的根节点的father指向父节点(参见上图)
注意原树与辅助树的结构并不相同
辅助树的根节点≠原树的根节点
辅助树中的father≠原树中的father
由于要维护的信息已经都在辅助树中维护了,所以LCT无需维护原树,只维护辅助树即可,也就是在实际程序中是不存在原树的,即使是原树的换根操作也只是通过辅助树来间接完成。
轻链和重链并不是固定的,随着算法的进行,轻链和重链随算法需要和题目要求而变化,然而无论怎么变化,由这棵辅助树一定能生成原树,并满足辅助树的所有性质

这里重点说一下换根操作,也是我刚开始卡住的地方
换根实际上是完成下图的操作:
Link-Cut-Tree 动态树算法_第2张图片
注意这转的是原树!!!!
那么假设1,2,3这三个点在一个splay树中,因为splay是以深度为关键字建树的,所有随着原树中根的改变,本身的深度就发生了变化,原先最深的是3号节点,而换根之后最深的就变成了1号节点,那么我们对splay进行翻转操作,将他的左右子树进行交换,就可以实现深度的更新,很神奇啊!


附代码: bzoj 2049 洞穴勘测
#include<iostream>  
#include<cstdio>  
#include<cstring>  
#define N 2000003  
using namespace std;  
int n,m;  
int ch[N][3],fa[N],next[N],size[N],st[N],rev[N],top;  
int isroot(int x)  //是否是辅助树的链顶,即当前splay 的根 
{  
  return ch[fa[x]][0]!=x&&ch[fa[x]][1]!=x;//他父亲的左右儿子都不是他,辅助树的根节点的父亲指向链顶的父亲节点,然而链顶的父亲节点的儿子并不指向辅助树的根节点  
}  
void update(int x)  
{  
 size[x]=size[ch[x][0]]+size[ch[x][1]]+1;  
}  
void pushdown(int x)  
{  
 if (!x) return;  
 if (rev[x])  
 {  
 swap(ch[x][0],ch[x][1]);  
 rev[ch[x][0]]^=1; rev[ch[x][1]]^=1; rev[x]=0;  
 }  
}  
int get(int x)  
{  
  return ch[fa[x]][1]==x;  
}  
void rotate(int x)  
{  
    int y=fa[x],z=fa[y],l,r;  
    l=get(x); r=l^1;  
    if(!isroot(y))  
      ch[z][ch[z][1]==y]=x;   
    ch[y][l]=ch[x][r]; fa[ch[y][l]]=y;//很神奇,这两行放到if前就会TLE   
    ch[x][r]=y; fa[y]=x; fa[x]=z;//因为更改了fa[y]的缘故,单纯的splay if 语句中判断的是z是否为根,所有不影响,但是lct 中splay与单纯的splay有细节上的差别  
}  
void splay(int x)  
{  
  top=0; st[++top]=x;  
  for (int i=x;!isroot(i);i=fa[i])  
   st[++top]=fa[i];  
  for (int i=top;i>=0;i--) pushdown(st[i]);//由于找节点并非自上至下,故操作之前需预先将节点到辅助树根的标记全下传一遍,注意翻转标记只会影响当前这颗树,不会改变整颗树中的顺序。   
  while(!isroot(x))    
   {    
    int y=fa[x];   
    if (!isroot(y)) //判断y 是否是辅助树中的根节点   
     rotate(get(x)==get(y)?y:x);  //splay 之字形旋转
     rotate(x);    
   }    
}  
void access(int x) //将一个点与原先的重儿子切断,并使这个点到根路径上的边全都变为重边,执行Access(x)函数后这个节点到根的路径上的所有节点形成了一棵Splay,便于操作或查询节点到根路径上的所有节点 
{  
  int t=0;  
  while (x)  
  {  
   splay(x);//将x 转到辅助树的根节点  
   ch[x][1]=t;  //将x 原来的重儿子斩断 ,但是x的重儿子并未斩断与x的关系,也就是重儿子只是当前存储了当前的路径,是不断改变的,下一次询问时还可以重新通过fa记录的关系得到一条新的重链,保证了原树的信息
   t=x; x=fa[x];  
  }  
}  
void rever(int x) //换根,换根换的是原树的根,是把x在原树中正常转动到根结点,在原树转动之后,那么原树中对应的深度也相应发生了变化,因为splay维护的是原树的信息,并且是以深度为关键字建树,所有树的形态发生翻转,以保证可以通过splay还原原树。有一点需要注意就是原树其实不需要维护,他是虚拟的不存在的 
{  
  access(x); splay(x); rev[x]^=1;  //注意Access(x)之后x不一定是Splay的根节点 所以Access之后通常还要Splay一下 
}  
void cut(int x,int y)  //先把x转到他所在链的根, 
{  
  rever(x); //这里之所以要把x转到他所在子树的根是因为lct可以维护多棵树,并支持合并,但是如果连接是x不是他所在树的根的话,那么他之前一定有一个父亲节点,连接时就会发生混乱。 
  access(y); splay(y); ch[y][0]=fa[x]=0;  //因为原树换根后,x,y的位置关系发生了改变,所有y 砍掉的是左儿子,而不是右儿子!!
}  
void link(int x,int y)  //连接,建立新的父子关系
{  
  rever(x); fa[x]=y; splay(x);  
}  
int find (int x)  //判断森林连通性,因为一颗辅助splay的父亲不一定是当前根的父亲,而是重链的的链顶的父亲,因为splay是以深度为关键字建树,所有我们要不停的向左子树方向寻找。
{  
  access(x); splay(x);  
  while(ch[x][0]) x=ch[x][0];  
  return x;  
}  
int main()  
{  
  scanf("%d%d",&n,&m);  
  for (int i=1;i<=m;i++)  
   {  
    char s[10]; int x,y; scanf("%s%d%d",s,&x,&y);  
    if (s[0]=='Q')  
     {  
       if (find(x)==find(y)) printf("Yes\n");  
       else printf("No\n");  
     }  
    else  
    if (s[0]=='C')  
     link(x,y);  
    else  
     cut(x,y);  
   }  
}   



你可能感兴趣的:(Link-Cut-Tree 动态树算法)