前三篇好像变成了SPLAY专题...

这一篇正式开始LCT!

其实LCT就是基于SPLAY的伸展操作维护树(森林)连通性的一个数据结构

核心操作有很多,我们以一道题为例:

例:bzoj 2049 洞穴勘测

要求:加边和删边,询问连通性

其实如果没有删边,裸跑并查集似乎就可以搞定

但由于存在删边,并查集思想受阻,我们要考虑更高级的数据结构

于是LCT横空出世!

LCT的核心思想:多棵SPLAY维护虚实树链

首先介绍一下树链剖分问题:

树链剖分问题是将一棵树划分成多条树链的思想,有很多种剖分方法,比如轻重树链剖分(最常见,最常用),长短树链剖分(在部分题目中可以替代树上的dsu算法,而且无论码量还是时间复杂度都是很优越的),以及LCT要使用的虚实树链剖分

所谓虚实树链剖分,就是讲一棵树的边划分为实边和虚边,对于实边连起来的一条链用一个SPLAY维护

举个例子:

这是一棵树(废话)

我们对他进行一下虚实剖分,如下:

如图,用实线表示的是实边,用虚线表示的是虚边,而我们用一些SPLAY维护这些链,就是:

 如图,用黑色实线连起来的点表示一个SPLAY,用黑色虚线框圈起来的是一个SPLAY,用蓝色虚线连的边表示父亲指针

为什么要这样建立SPLAY?

LCT维护树链的SPLAY有一个原则:每棵SPLAY的中序遍历会产生一个序列,而这个序列所对应的树链深度是单调递增的!

这是很重要的一个性质

接下来还有一个性质(当然在上面那张图上体现不太出来):就是每棵SPLAY的根节点的父亲一定要指向这棵SPLAY中中序遍历最靠前的那个点在原树中的父节点

换言之,我们不一定能保证SPLAY的根是深度最浅的,但是他的父亲一定要指向SPLAY维护的节点中深度最浅者的父节点

可是还要注意一点,如图,虽然存在9->4,10->4,8->4三个父亲指向,但是4的儿子只有一个,就是8,剩下的并不计入4的子节点中(即人们常说的“认父不认子”)

接下来我们就可以进行一些操作了:

首先,本身我们除了上述原则以外,如何进行虚实树链剖分是无所谓的,所以我们完全可以重构这些SPLAY,使得两个点被同一棵SPLAY维护

怎么操作?

我们使用一个函数叫access,使得一个点与整棵树的根所连接的

现在假设我们要把8号节点和1号节点放到一棵SPLAY里,我们怎么办呢?

首先,我们把8号节点转到本身SPLAY的根上

这一点很容易,根据SPLAY的伸展操作,双旋即可

不会旋转的详见前三篇SPLAY专题

(这里顺便说一句:由于在LCT中SPLAY只有一种旋转方式,就是将某一点旋转到SPLAY的根,所以旋转函数会比原来好写一些)

至于如何找到这个SPLAY的根,方法也很简单:一个SPLAY的根一定不会是任意一个SPLAY的儿子(废话),所以仅需找到这个点的父亲,看看这个点的父亲的儿子中有没有他就可以了,如果没有这个点就是根

剩下的旋转操作就和普通SPLAY一样了。

判断这个点是不是根:

bool berot(int rt)
{
if(c[f[rt]][0]==rt||c[f[rt]][1]==rt)
{
return 0;
}
return 1;
}

将某一点旋转到根的位置上:

void rotate(int rt)
{
int ltyp=0;
int fa=f[rt];
int ffa=f[fa];
if(c[fa][1]==rt)
{
ltyp=1;
}
if(!berot(fa))
{
if(c[ffa][1]==fa)
{
c[ffa][1]=rt;
}else
{
c[ffa][0]=rt;
}
}
c[fa][ltyp]=c[rt][ltyp^1];
c[rt][ltyp^1]=fa;
f[c[fa][ltyp]]=fa;
f[fa]=rt;
f[rt]=ffa;
update(fa);
}
void splay(int rt)
{
repush(rt);
while(!berot(rt))
{
int fa=f[rt];
int ffa=f[fa];
if(!berot(fa))
{
if((c[fa][0]==rt&&c[ffa][0]!=fa)||(c[fa][1]==rt&&c[ffa][1]!=fa))
{
rotate(rt);
}else
{
rotate(fa);
}
}
rotate(rt);
}
update(rt);
}

这是access中所需要的操作之一

接下来access操作就很简单了,因为现在我们已经让8号点到了根节点的位置上,如果用图来看,就是:

(所以说了那么多,其实我们只干了这么点事而已...)

接下来,我们就要把8扔进1所在的SPLAY里了

这就是access函数的第二步

首先,如果想把8扔进去,那么8就要在原来的SPLAY里面作为一个节点(废话)

作为哪个节点呢?

显然是右儿子!

为什么?

因为这个点的深度一定要比整个树链中深度最浅的点的父亲的深度深!(读十遍)

所以自然扔到右儿子去

什么?原来的右儿子怎么办?

不管...

反正LCT是“认父不认子”的...

access代码:

void access(int rt)
{
int y=0;
while(rt)
{
splay(rt);
c[rt][1]=y;
update(rt);
y=rt;
rt=f[rt];
}
}

所以,打通了以后,这个图会变成这样:

(忘记编号了,不过位置都没变,所以应该没啥事...)

(原谅我越来越丑陋的画风...)

这就完成了我们所需要的操作,接下来我们基本就可以“为所欲为”了

首先,我们可以对这棵树进行换根!

因为我们发现,在我们访问一条树链的时候,有很大概率这条树链本身并不能满足深度单调递增

那这样的树链是不能用同一棵SPLAY来维护的

可是我们就需要操作这个树链啊

那我们把树根换掉不就好了吗

所以假设我们需要操作树链(u,v),我们仅需将u转成树根,然后将v用access操作转进去就可以啦

于是问题变成了怎么把u转成树根

我们使用一个操作叫makeroot,表示把某一点变成树根

怎么变?直接拽上去?

你说对了...

首先,我们把u点用access操作转到和根节点在同一个SPLAY里

然后,我们把u点用SPLAY操作直接转到树根上

最后,我们翻转整个SPLAY即可

为什么?

首先,当我们access一个节点后,这个点一定是没有右子树的(很显然啊,access过程中把它置0了...)

那么这个点又被放在了原树的右节点上,那这不说明access后,这个节点在SPLAY中一定是深度最大的点吗?

那我们把他转到根节点上,再翻转整个SPLAY,不就让这个点成为了深度最大的点吗?(别忘了,LCT要求中序遍历深度单调递增啊)

这不就搞定了吗

所以操作如下:

void makeroot(int rt)
{
access(rt);
splay(rt);
reverse(rt);//翻转整个SPLAY
}

有了这个操作,剩下所有操作都是顺理成章的了

连边:如果想从(u,v)连一条边,那么我们可以首先把u转到树根上,然后直接连边即可

void link(int st,int ed)
{
makeroot(st);
if(getroot(ed)==st)
{
return;
}
f[st]=ed;
}

删边:把u转到树根上,直接删边即可

void cut(int st,int ed)
{
makeroot(ed);
if(getroot(st)==ed&&f[ed]==st&&!c[ed][1])
{
c[st][0]=f[ed]=0;
update(st);
}
}

抽出一条从u到v的树链:

void split(int st,int ed)
{
makeroot(st);
access(ed);
splay(ed);
}

剩下基本就随便搞了

贴下bzoj 2049代码

#include <cstdio>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <iostream>
#include <algorithm>
#include <queue>
#include <stack>
#define which(x) (c[f[x]][1]==x)
#define ls tree[rt].lson
#define rs tree[rt].rson
using namespace std;
int c[100005][2];
int f[100005];
bool ttag[100005];
char s[10];
bool berot[100005];
int n,m;
void reverse(int rt)
{
ttag[rt]^=1;
swap(c[rt][0],c[rt][1]);
}
void pushdown(int rt)
{
if(ttag[rt])
{
reverse(c[rt][0]);
reverse(c[rt][1]);
ttag[rt]=0;
}
}
void repush(int rt)
{
if(!berot[rt])
{
repush(f[rt]);
}
pushdown(rt);
}
void rotate(int rt)
{
int ltyp=0;
int fa=f[rt];
int ffa=f[fa];
if(c[fa][1]==rt)
{
ltyp=1;
}
if(berot[fa])
{
berot[fa]=0;
berot[rt]=1;
}else
{
if(c[ffa][1]==fa)
{
c[ffa][1]=rt;
}else
{
c[ffa][0]=rt;
}
}
c[fa][ltyp]=c[rt][ltyp^1];
c[rt][ltyp^1]=fa;
f[c[fa][ltyp]]=fa;
f[fa]=rt;
f[rt]=ffa;
}
void splay(int rt)
{
repush(rt);
while(!berot[rt])
{
int fa=f[rt];
int ffa=f[fa];
if(!berot[fa])
{
if((c[fa][0]==rt&&c[ffa][0]!=fa)||(c[fa][1]==rt&&c[ffa][1]!=fa))
{
rotate(rt);
}else
{
rotate(fa);
}
}
rotate(rt);
}
}
void access(int rt)
{
int y=0;
while(rt)
{
splay(rt);
berot[c[rt][1]]=1;
berot[y]=0;
c[rt][1]=y;
y=rt;
rt=f[rt];
}
}
void getroot(int rt)
{
access(rt);
splay(rt);
reverse(rt);
}
void link(int st,int ed)
{
getroot(st);
f[st]=ed;
}
void cut(int st,int ed)
{
getroot(st);
access(ed);
splay(st);
c[st][1]=f[ed]=0;
berot[ed]=1;
}
bool check(int st,int ed)
{
getroot(st);
access(ed);
splay(st);
while(!berot[ed])
{
ed=f[ed];
}
if(st==ed)
{
return 1;
}else
{
return 0;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
berot[i]=1;
}
for(int i=1;i<=m;i++)
{
scanf("%s",s);
int x,y;
scanf("%d%d",&x,&y);
if(s[0]=='C')
{
link(x,y);
}else if(s[0]=='Q')
{
if(check(x,y))
{
printf("Yes\n");
}else
{
printf("No\n");
}
}else
{
cut(x,y);
}
}
}

SPLAY,LCT学习笔记(四)的更多相关文章

  1. LCT 学习笔记

    LCT学习笔记 前言 自己定的学习计划看起来完不成了(两天没学东西,全在补题),决定赶快学点东西 于是就学LCT了 简介 Link/Cut Tree是一种数据结构,我们用它解决动态树问题 但是LCT不 ...

  2. C#可扩展编程之MEF学习笔记(四):见证奇迹的时刻

    前面三篇讲了MEF的基础和基本到导入导出方法,下面就是见证MEF真正魅力所在的时刻.如果没有看过前面的文章,请到我的博客首页查看. 前面我们都是在一个项目中写了一个类来测试的,但实际开发中,我们往往要 ...

  3. IOS学习笔记(四)之UITextField和UITextView控件学习

    IOS学习笔记(四)之UITextField和UITextView控件学习(博客地址:http://blog.csdn.net/developer_jiangqq) Author:hmjiangqq ...

  4. java之jvm学习笔记四(安全管理器)

    java之jvm学习笔记四(安全管理器) 前面已经简述了java的安全模型的两个组成部分(类装载器,class文件校验器),接下来学习的是java安全模型的另外一个重要组成部分安全管理器. 安全管理器 ...

  5. Learning ROS for Robotics Programming Second Edition学习笔记(四) indigo devices

    中文译著已经出版,详情请参考:http://blog.csdn.net/ZhangRelay/article/category/6506865 Learning ROS for Robotics Pr ...

  6. Typescript 学习笔记四:回忆ES5 中的类

    中文网:https://www.tslang.cn/ 官网:http://www.typescriptlang.org/ 目录: Typescript 学习笔记一:介绍.安装.编译 Typescrip ...

  7. ES6学习笔记<四> default、rest、Multi-line Strings

    default 参数默认值 在实际开发 有时需要给一些参数默认值. 在ES6之前一般都这么处理参数默认值 function add(val_1,val_2){ val_1 = val_1 || 10; ...

  8. muduo网络库学习笔记(四) 通过eventfd实现的事件通知机制

    目录 muduo网络库学习笔记(四) 通过eventfd实现的事件通知机制 eventfd的使用 eventfd系统函数 使用示例 EventLoop对eventfd的封装 工作时序 runInLoo ...

  9. python3.4学习笔记(四) 3.x和2.x的区别,持续更新

    python3.4学习笔记(四) 3.x和2.x的区别 在2.x中:print html,3.x中必须改成:print(html) import urllib2ImportError: No modu ...

  10. Go语言学习笔记四: 运算符

    Go语言学习笔记四: 运算符 这章知识好无聊呀,本来想跨过去,但没准有初学者要学,还是写写吧. 运算符种类 与你预期的一样,Go的特点就是啥都有,爱用哪个用哪个,所以市面上的运算符基本都有. 算术运算 ...

随机推荐

  1. 【CSS】元素样式

    1.使用CSS的三种方式: 方式一.通过元素的style属性来设置元素的样式 方式二.在HTML头部标签<head>中通过<link>标签引入一个外部的CSS资源,通常是一个C ...

  2. js动画最佳实现——requestAnimationFrame

    我们经常用setInterval来实现动画,其实这种做法不是太好,因为不同浏览器的刷新频率也不一样(一般认为设置16为最佳,按每秒60帧算,1000/60≍16.67) var dis = 0,tim ...

  3. Apache Solr 初级教程(介绍、安装部署、Java接口、中文分词)

    Python爬虫视频教程零基础小白到scrapy爬虫高手-轻松入门 https://item.taobao.com/item.htm?spm=a1z38n.10677092.0.0.482434a6E ...

  4. Linux命令之如何从普通用户切换至管理员用户

    普通用户,标志是一个$符号 管理员用户,标志是一个#符号 我要切换,敲打命令  sudo su - 然后输入你的管理员用户的密码(输入密码的时候是不可见的) 然后你就切换到#状态了.

  5. 实现一个string类

    实现要实现:构造,析构,拷贝构造,赋值的功能 1. 提供构造函数 string(), string(const string & str),string(const  char * str), ...

  6. 复选框QCheckBox

    复选框一共有三种状态:全选中.半选中和无选中.若一个父选项的子选项全部为选中状态,则该父选项为全选中:若子选项全部为无选中状态,则该父选项为无选中状态:若子选项既有全选中和无选中状态,则该父选项为半选 ...

  7. JavaScript之原型|typeof|instanceof

    //var 变量X --> 对象 --> 构造器 --> 原型对象 function Person(){}; var stu = new Person(); //var stu = ...

  8. SEO之robots.txt

    [关键词:robot.txt,sitemap,User-Agent,Disallow,Allow][声明:摘自Wikipedia] 1. 定义:robots.txt(统一小写)是一种存放于网站根目录下 ...

  9. 说几个python与c区别的地方以及静态变量,全局变量的区别

    一: python代码: a = 2 def b(): print a a = 4 print a b() 在b函数中,有a=4这样的代码,说明a是函数b内部的局部变量,而不是外部的那个值为2的全局变 ...

  10. python 错误--UnboundLocalError: local variable '**' referenced before assignment

    val = 9 def test(flag): if flag: val = 1 else: print("test") return val if __name__ == '__ ...