zoukankan      html  css  js  c++  java
  • LCT学习笔记

    LCT学习笔记

    前言

    老吕又讲了LCT,据他说特别简单,于是就强行灌输(雾。
    打字两分钟,画图两小时。。。

    引入

    • 维护一棵树,维护以下的操作:

      • 链上求和
      • 链上求最值
      • 链上修改
      • 子树修改
      • 子树求和

    可能你第一眼想的是树链剖分,的确,这都是树链剖分的基本操作。

    但是如果再增加一些操作呢

      • 换根
      • 断开树上的一条边
      • 连接两个点,保证连接后仍是一棵树

    线段树就不好做了,于是我们的 LCT 就出场了。

    注:树剖也是可以换根的,可能我上面说的不清楚,具体怎么换可以参考这篇博客。我只是说不太好做。(可能我太菜了)。

    简介

    LCT,全称 Link-Cut Tree,一种动态树,用来解决动态的树上问题。说它是树也不大准确,它维护的其实是一个森林。据我不可信的猜测,这个名字可能是由于这个数据结构特有的特色来命名的,也就是 Link,Cut,支持树上的删边,加边。这一点是普通线段树没法做到的,LCT的 access 也是他的一大特色,也是常用的一个函数。(个人感觉)

    构造

    我们在学习树链剖分的时候,就知道,将链进行剖分,主要有三种形式:

    1.重链剖分。
    只要是按照子树大小进行剖分,就是把儿子数最多的儿子当做重儿子,重儿子连成的链叫做重链。

    2.长链剖分。
    并不是很常见,我也不到了解。

    3.实链剖分。
    将树上的链分成虚实两种,一个点最多只有一个孩子作为实孩子。连接实孩子的称为实边,实边组成的链称为实链。

    我们在 LCT 中就是采用的是实链剖分,其中,实孩子是不固定的,它可以通过我们的修改而发生改变,我想,这也是 LCT 的一个动态,当然,其主要的动态还是动态删边和加边。因此,我们需要选用更灵活的数据结构。

    维护一条链,理论上 FHQ-Treap 和 Splay 都是可以的,但是 FHQ-Treap 要比 Splay 多一个 (log) ,而且网络上的题解大部分都写的是 Splay,因此,这里推荐 Splay 的写法,不会 Splay 的可以去学习一下,因为这是非常重要的一部分。因为我也没写过FHQ-Teap的

    我们在之前说过,一个点顶多只有一个实孩子,也就是说一条实链上,每个节点的深度在原树中都是不同的,因此,我们把深度作为关键字用 Splay 维护,对于一个节点,它的左儿子的深度要比它小,右儿子的深度要比它大。

    这里补充一下两个概念:

    原树:也就是我们对其进行剖分的树。在我们实现的时候,原树是 不存储 的,只是为了方便我们理解。

    辅助树:也就是一棵splay,或者说一些 Splay。

    • 它维护的是原树中的一条实链,在程序中真正操作的都是辅助树。中序遍历这些点的时候,其对应的就是原树中的一条链。

    • 在 LCT 中每棵 Splay 的根节点的指向 原树这条链 的链顶的父亲节点(即链最顶端的点的父亲节点)。主要的特点在于儿子认父亲,而父亲不认儿子,对应原树的一条 虚边

    基础操作

    我们先造一颗树。这是一棵原树。

    我们选择一些边作为虚边,选择一些边作为实边。

    然后,让我们画出辅助树。

    我们找出其中的 Splay,大概就是这个亚子。

    了解完这些之后,我们开始今天的重点。

    变量声明

    我习惯将变量放到结构体里。

    • tree.ch[0/1] 左右儿子

    • f[N] 父亲

    • tree.sum 路径权值和

    • tree.val 点权

    • tree.laz[N] 翻转标记

    主要的函数:

    • link(x,y)连接两个点

    • cut(x,y):断开两个点间的边

    • access(x):把 (x) 点下面的实边断开,并把 (x) 点一路向上边到树的根

    • makeroot(x):把 (x) 点变为树的根

    • find(x):查找 (x) 所在树的根

    • isroot(x):判断 (x) 是否是辅助树的根

    • split(x,y) : 提取出 (x,y) 间的路径

    • update(x,y) : 修改 (x) 的点权为 (y)

      当然还有 rotatesplaypushuppushdown ,不过这些都是线段树或 Splay 的基本操作,就不详细展开了。

    accsee

    作用:断开当前点连的实链,到根节点连一条实链。

    方法:把 (x) 点伸展到splay的根,再把它的右子树连到 (t)(t) 的初值为 0,也就了与下一层的实链断开了,然后 (t) 更新为 (x),而 (x) 更新为 (x) 的父亲,继续向上连接。因为我们现在的连接,父亲认儿子,儿子认父亲,一直到根,也就到根连接了一条实链。

    假设我们 (access(9)) ,我们的图就变成了这样。原谅我不会制作动图,没有详细的变化过程。

    void access(int p) 
    {
    	int t=0;//因为当前点是这条链的最后一个点,旋转到根之后右边的点就是当前点之后的点,也就是要断开的点
    	while(p)
    	{
    		splay(p);//把 p 伸展到根节点, 
    		rson(p)=t;//不断让父亲向它连边,也就是连上了实边 
    		t=p;
    		p=f[p];
    		push_up(p);
    	}
    }
    

    makeroot

    作用:把x点变为所在原树的根。

    方法:首先的把 (x)(access) 到根,把 (x) 点到根就变成了一个 Splay,然后把 (x) 伸展到根。由于 (x) 点是辅助树在原树中最下面的点,所以这时其它的点都在 (x) 的左子树上,只要把左子树变成右子树,(x) 也就变成了根。

    我们上面 (accsee(9)) ,不妨就继续让 (9) 变成根。先 Splay 一下。

    void makeroot(int p)//是当前点变成原树里的根节点 
    {
    	access(p);//到根节点连实链,也就是一颗 splay
    	splay(p);//将当前点转到根节点
    	tree[p].laz^=1//由于 x 点是最后一个,当前为根节点时所有的点都在他的左边,^一下让所有的点都在他右边,就变成了根了
    }
    

    findtoot

    作用:查找原树的根

    我们想一下,在辅助树中,怎么才能找到原树的根呢?

    我们发现,位于最顶部的 Splay,它的最左边的孩子为原树的根,因为我们要保证 Splay 的形态,先要保证它的中序遍历和原树一致。

    方法:首先把 (x)(access) 到原树的根,并把它 Splay 到辅助树的根,这时原树的根就是 (x) 左子树中最左侧的点。

    再借用上面的 (access(9))(Spaly(9))

    int find(int x)//找原树的根 
    {
    	access(x);//x到根建一颗splay
    	splay(x);//将 x 伸展到根节点
    	while(lson(x)) push_down(x),x=lson(x);//因为原树根节点肯定就是中序遍历的第一个点,也就是最顶上的
    	return x;// splay的最左边的儿子,一直找左儿子就行了 
    }
    

    split

    作用:提取出 (x,y) 间的路径

    我们再 (makeroot(9)) ,图在前面,就不放了,我们 (access(10))(Splay(10))

    void split(int x, int y) {
        makeroot(x);//首先把x置为根节点 
        access(y);//生成一颗 Splay
        splay(y);
        //y维护的就是x - y 路径上的信息 
    }
    
    

    作用:把 (x) 点和 (y) 点之间连一条边
    方法:把 (x) 点变成所在原树的根,然后把 (x) 点的父亲变成 (y) 就可以了。

    比如说加一条连向 (9) 的边。

    void link(int x,int y)//连边
    {
    	makeroot(x);//使p变成根节点
    	f[x]=y;//x变成y的父亲,也就是连了边
    }
    

    cut

    作用:把 (x) 点和 (y) 点之间的边删掉
    方法:把 (x) 点变成所在原树的根,然后把 (y)(access) 到根,Splay (y) 到辅助树的根,然后断开y与它左孩子间的边。由于 (x) 是原树的根,(y) 是树中的一点,所以就 (y) 点通过 (access)(x) 点连到一个辅助树中时,(x) 点一定是它们所在实链的链顶。而 (y) splay到辅助树的根时,如果 (x),(y) 间有一条边,则 (x)一定是 (y) 的左孩子。

    比如说删去 (8 o 9) 这条边。

    void cut(int x,int y)//删边
    {
    	makeroot(x);//x变成根节点
    	access(y);//y通向 x 减了一个实链,也就是一颗 splay,因为 x,y之间有边,所以这颗splay 里面只有两个点
    	splay(y);//将 y 转到顶部 
    	if(lson(y)!=x ||rson(x)) return;//两者之间本来就没有边
    	f[x]=0;//删去原来连边的信息 
    	lson(y)=0;
    	push_up(x);
    }
    

    isroot

    作用:判断是否是splay的根
    方法:splay的根结点的父亲并不认这个孩子。
    注意:原树的根的父亲点是 (0)

    bool isroot(int x)//判断当前点是否是实链的根节点
    {//当前点是根节点因为这它认父亲,父亲不认儿子 
    	return lson(f[x])!=x && rson(f[x])!=x; 
    }
    

    下面的部分都是基础操作,Splay 有个地方有点不一样,可以看见。

    pushup

    void push_up(int p)
    {
    	tree[p].sum=tree[lson(p)].sum^tree[rson(p)].sum^a[p];
        or
        tree[p].sum=tree[lson(p)].sum+tree[rson(p)].sum+a[p];
    }
    

    pushdown

    void ff(int p) 
    {
    	swap(lson(p),rson(p));
    	tree[p].laz^=1;
    }
    void push_down(int p)
    {
    	if(!tree[p].laz) return;
    	if(lson(p)) ff(lson(p));
    	if(rson(p)) ff(rson(p));
    	tree[p].laz=0;
    }
    

    rotate

    void rotate(int x,int op)
    {
    	int y=f[x];
    	if(!isroot(y))
    		tree[f[y]].ch[rson(f[y])==y]=x;//原先父亲节点与其父亲节点的边断开,连上现在的这个点 
    	f[x]=f[y];//儿子节点的爸爸换成爷爷 
    	if(tree[x].ch[op])//儿子节点op儿子有的话,改变他的父亲为父亲 
    		f[tree[x].ch[op]]=y;
    	tree[y].ch[!op]=tree[x].ch[op];//父亲的儿子变成儿子的儿子 
    	f[y]=x;//父亲的父亲变成儿子 
    	tree[x].ch[op]=y;//儿子的对应儿子变成父亲
    	push_up(y); 
    	//注:注释里的父亲,儿子,爷爷,都表示没变化之前的称谓 
    }
    
    

    spaly

    这里讲一下和普通 Splay 的一点区别,就是我们先用栈将我们接下来要旋转的点存储下来,然后一起 pushdown 。这样就不用边旋转边 pushdown。

    int sta[M],top;//为了将懒惰标记一气儿下传 
    void splay(int x)
    {
    	sta[++top]=x;
    	for(int i=x;!isroot(i);i=f[i]) sta[++top]=f[i];
    	while(top) push_down(sta[top--]);//splay之前先将要旋转的链上的懒惰标记全部下穿,免去了边旋转边下传的麻烦
    	while(!isroot(x))//当前点不是根 
    	{
    		if(!isroot(f[x]))//父亲也不是根 
    		{	
    			if((rson(f[x])==x)^(rson(f[f[x]])==f[x]))//不在一边 
    				rotate(x,lson(f[x])==x);//旋转当前节点 
    			else
    				rotate(f[x],lson(f[f[x]])==f[x]);//链的情况,旋转父亲节点才能改变形态,旋转父亲节点 
    		}
    		rotate(x,lson(f[x])==x);
    	}
    	push_up(x);
    }
    

    习题

    后面的没做,做了有时间再补代码。

    参考资料

    oi_wiki

    flashhu大佬的博客

    亲学长的博客

    老师的课件

    本欲起身离红尘,奈何影子落人间。
  • 相关阅读:
    丰富eclipse注解的内容
    ThreadLocal总结
    算法+结构?设计模式?
    面试需要准备内容
    HIbernate-0.8版本源码翻看
    2019第一篇博文
    随笔
    Expert one on one 读书笔记之容器
    Expert one on one development without ejb读书笔记
    Mysql索引部分学习关于多列索引的部分
  • 原文地址:https://www.cnblogs.com/jcgf/p/15242044.html
Copyright © 2011-2022 走看看