上次xuefeng说我的专题总结(初探插头dp)太不适合入门了,所以这次丢一些题解包以外的东西。

关键是我自己也不会。。。急需梳理一下思路。。。

(让我口胡数据结构???顺便推广一下全世界最短的lct板子反正也没人要来看个热闹啊如果有什么继续压的方法记得告诉我啊

一段时间之前学过的数据结构,当时理解的不太深刻。

然后lct1专题也是挺久之前做的了,自己再口胡一遍加深一下印象。

lct这个东西吧,其实就是链剖。

树链剖分是按照子树大小划分重链,是静态的(虽说也可以麻烦一些动态重构),大多数时候用线段树维护链上信息。

而lct,又叫动态树,是按照询问和修改的需要划分实链,平时就是提出树上的一条链,这上面的点之间都是实边,与链外点的连边是虚边。

lct是动态的,它的实链和虚链是可以动态转换的。用于维护信息的数据结构是splay。据说FHQ Treap也是可以的,但是复杂度不是很对。

反正主流的打法就是splay,而且还是多个splay,每个splay维护的是一条实链上的所有点,排序key值是每个点在树上的深度。

所谓的实链虚链,其实就在于,你认识你爸爸,但是你爸爸认不认识你。

因为在树里每个点可以有很多儿子,而在lct中只有左右儿子(毕竟是个splay)。而其实,因为维护的是一条实链,所以其实你只认了一个儿子。。。

我们能够对实链进行快速的操作,因为它已经在一个splay里了,对splay操作和询问都不难。

实边就是你爸爸认识你(在一个splay里,虽说可能不直接相邻),虚边就是你爸爸不认识你。实链就是由实边构成的原树上的一条链。

lct能维护的操作有:换根,链查询/修改,判联通性,连边,断边。但是很难维护子树信息(除非对于每个节点开一个平衡树。。。)

lct可以维护的其实不是树,而是森林,也就是不用保证它是联通的。但是不能出环必须保证是树型结构,所以遇到维护一个图往往都要特殊处理。

数组:f[]表示父亲,c[][2]表示左右儿子,lz[]表示区间翻转懒标记。

常用函数:

not_root(int)用来判这个点是不是树根。只要判一下爸爸的儿子是不是你就行。。。

bool not_root(int p){return c[f[p]][0]==p||c[f[p]][1]==p;}

rev(int)用来翻转区间。为什么用到它下面再说。(我的lc与rc宏定义成了c[p][0]与c[p][1])。翻转就是交换儿子然后放个标记呗。

注意这里p节点有标记代表的是p已经翻转(就是lc和rc已经交换)而lc和rc还没有翻转

void rev(int p){swap(lc,rc);lz[p]^=1;}

down(int)下传懒标记,这个不用多说吧。

void down(int p){if(lz[p])rev(lc),rev(rc),lz[p]=0;}

up(int)上传子树信息。因题而异。不说了。

rotate(int)和splay里的一样,是把p节点往上翻一下让它走到它父亲的位置,并且保持平衡树性质。

牵扯到的所有点就是p,父亲fa,爷爷gr,以及p的一个儿子br。

p往上走了,那么在操作前fa是gr哪个方向的儿子,那么p也就占上了fa的位置成为gr的对应儿子。

fa被p挤下去了,因为大小关系的限制,原来p是fa的哪个儿子,那么fa现在就是p的另一个方向的儿子。

本来p可能有两个儿子,但是现在有一个儿子是fa了,那么原有的一个儿子就需要过继给fa。

因为大小关系,所以原来p在fa的哪个方向,那么br也一定就在fa的哪个方向。(想想二叉搜索树的样子)

注意信息上传。可以只传fa。因为在后续操作中p还会被更新所以不要担心。

void rotate(int p){
    int fa=f[p],gr=f[fa],dir=c[fa][1]==p,br=c[p][!dir];
    c[p][!dir]=fa;c[fa][dir]=br;if(not_root(fa))c[gr][c[gr][1]==fa]=p;
    f[br]=fa;f[fa]=p;f[p]=gr;up(fa);
}

splay(int)就是把一个节点p不断向上翻到所在splay的根。

但是在那之前,你需要从上到下把所有的翻转标记释放,不然树的结构就不对了。

void splay(int p){
    int top=0;q[++top]=p;
    for(int r=p;not_root(r);q[++top]=r=f[r]);for(;top;down(q[top--]));
    for(;not_root(p);rotate(p));up(p);
}//这是伪的

然而你如果理解splay的话你会知道这个东西是单旋。出于某些特殊的原因和某些特殊的数据,它有可能会被卡掉。(虽说很少有数据这么毒瘤)

但是出于不会证明的原因带着不会证明的复杂度,我们有一个不会证明的优化:

如果翻转之前,爷爷,父亲,和节点p三点一线(就是都是右儿子或都是左儿子)那么就先rotate父亲再rotate儿子。否则一直rotate儿子。

这样操作时候在n较大时期望的树高会降低。画个图发现的确是对的,但是并不会证明。

至少你会在luogu的模板题上第一个测试点T飞。

void splay(int p){
    int top=0,fa,gr;q[++top]=p;
    for(int r=p;not_root(r);q[++top]=r=f[r]);for(;top;down(q[top--]));
    while(not_root(p)){fa=f[p];gr=f[fa];
        if(not_root(fa))rotate(c[fa][1]==p^c[gr][0]==fa?fa:p);rotate(p);
    }up(p);
}

这些都不是lct的东西。到这里要真正开始lct了。

因为每个splay维护的都是一条你剖分出的实链,但是现在这条实链不一定是你想要的那一个。

所以我们弄一个函数access(int)来把一个点到根上所有的路径都变成实边,这样我们就搞出了一条链。

既然虚链是认父不认子,而实链是双向认,所以问题就在于爸爸认不认识你。

所以我们只要让到根路径上的每一个点都把路径上的儿子认出来就好了吧。

但是暴跳父亲复杂度肯定会挂啊。。。但是别忘了你还有splay呢,splay就是一条已有实链啊。

所以我们把p这个点splay到根再跳父亲,复杂度就是$O(log)$的了。

跳到splay的顶端之后如果还有爸爸,那么就证明你这个点上面是个虚边,你爸爸不认识你。

于是开个变量把你记住,等跳你爸爸的时候让它顺便把你认了就好了。

因为树的结构有变化,splay也变了,所以要记得上传信息。

说了这么多,代码就一行。。。但是它挺对的。。。真的就很简短的解决了上面的问题

void access(int p){for(int r=0;p;p=f[r=p])splay(p),rc=r,up(p);}

有些题里直接会让你换根,有些题的某些操作在换根之后会比较方便(看下去你就知道了)

考虑怎么换根。用上面的这些函数。首先用access打通p节点到根的实链路径,这样p和现在的根就在同一个splay里了,只不过深度不对。

如果p节点的深度变成1,那么它现在就是根了对吧。所以现在我们需要的是。。深度大的变成深度小的。

我说过,splay的key值是深度对吧。所以你只要「欺骗」它一下就好了。我们把整个splay翻转一下,不就实现了大小颠倒吗?

也就是你需要把当前这棵splay翻转一下。于是你把p节点splay到平衡树的根,再给它翻转就好了。

void make(int p){access(p);splay(p);rev(p);}

对于判联通性操作,我们只需要判一下两个点它们是不是在一棵树里(废话)。

而在同一棵树里的点有什么共性:它们的树根相同(又废话)。

所以我们只要实现一个找根的函数不就得了?

我们只需要把p到根的路径打通,然后把p点splay到根,这样就得到了p节点和根同时在的平衡树了。

然后因为平衡树是按照深度做key值的,而根是深度最小的,也就是查splay里的最小值呗,那就一直找左儿子就好了。

最后顺手把找到的根splay一下让树平衡(也许不必要?),保证复杂度。

int find(int p){access(p);splay(p);for(;lc;p=lc)down(p);splay(p);return p;}

然后,说了这么半天,怎么做链操作和链查询啊?不能操作不能查询那这个数据结构还有什么用啊!

假如我们要把x到y的路径做查询或操作。

我们的access函数是针对与树根的,所以我们首先需要树根变成路径的端点,于是先换根为x。

然后我们的操作都是对于splay的,需要xy在同一个splay(也就是实链)上,所以需要打通xy的路径,access(y)。

现在你的确让它们在同一个splay里了,而且链上的所有点都在,链外的都不在。

但是因为你access函数调用了splay,所以现在splay的根是谁不好说。于是你还要把y或x节点splay到根。

那么你要修改就只需要在根x或y上打标记,查询就在up之后直接查询x或y就能得到答案了。

void split(int x,int y){make(x);access(y);splay(y);}

然而说好的lct是Link-Cut-Tree啊。这没Link也没Cut的是个什么东西。(愈发有单口相声的感觉?)

先说连边(因为它简单)。在前面我说过了,你必须保证连边之前两点不联通,这个可以通过find函数判掉。

所以我们默认Link函数传入的惨一定合法。判断联通部分就写在主函数里了。

那么如果现在我们的确可以连边,那就好说了吧。

我们让x作为它所在的联通块的根,然后直接接在y的底下就好了。连一个虚边,认父不认子就行。

void link(int x,int y){make(x);f[x]=y;}

然后还剩下一个Cut。和Link一样这个可能也需要判断是否合法。

大多数良心出题人都会保证操作合法,但是还是有些出题人热爱毒瘤。

判断条件好说:不联通肯定不行。

然后我们把x设定为根,调用find找y的根,如果不同于x,就完戏了。

而find函数的过程中,你先splay了y,后又splay了x。

分情况讨论:

如果splay节点x时,最后一次rotate满足三点一线而进行了双旋,那么y-p-x就是三点一线了,x和y之间夹了一个p深度差一定不是1,所以也完戏了。

而这样splay的结果就是由右偏的y-p-x变成了左偏的x-p-y,只有通过双旋才会出现x-y没有直接父子关系的情况,所以我们直接判定if(f[y]!=x)return false;

剩余的情况,也就是并没有存在三点一线,这时候最后一次操作一定是rotate(x)替代y的位置,这样的话在splay中x-y一定是直接父子。

因为splay是一条实链上的所有点,所以接下来我们可以直接依据深度关系来判定了。如果存在深度介于x-y之间的那么就return false;

而现在y已经是x的右儿子了,想要有深度介于它们之间的,一定是y的左儿子,所以只要y有左儿子就return false;

手玩所有情况也足以证明,以上条件已经充分且必要。所以就是:

make(x);if(find(y)!=x||f[y]!=x||c[y][0])return false;//原树并不存在这条边

好,到现在我们能保证断边合法了。那就简单了。

调用split函数就可以直接把这两个点提出来,直接双向断绝父子关系就干净利落的断开了这条边。

然而这样的话其实还没完,y少了个儿子它不开心啊,所以你要把它up一下它就开心了。

void cut(int x,int y){split(x,y);f[x]=c[y][0]=0;up(y);}

然后lct的板子应该就没了。这是一个扩展性挺强的数据结构,需要比较深刻的理解吧。。。

惆怅的看着这么多个只有一行的为了调用其它函数而存在的函数,你大概也知道这玩意常数有多大。

所以在不少时候,明明作为一个$O(n\ logn)$的数据结构,也还是会被$O(n\ log^2 \ n)$的树剖线段树爆踩。

复杂度证明的话还是需要势能分析,和splay一样,用期望算的话,能大概理解复杂度还是$O(n\ log\ n)$的。

尤其是access那里调用了那么多次splay,但是复杂度还是$O(n \ log \ n)$的,不是很会证。

没事,带着它的常数,你把它当成$O(n\ log^2 \ n)$的就好了。所以数据范围一般不敢开的特别大。

当时刚学的时候第一个板子写了十几个小时然后还被教练叫出去说压行的事什么来着。

然后现在做完了插头dp/无限之环之类的题之后发现他还是没有那么难写的。

希望这次口胡完之后能够理解,以后写lct不要总是对着板子打下来了。。。

 1 bool not_root(int p){return c[f[p]][0]==p||c[f[p]][1]==p;}
 2 void rev(int p){swap(lc,rc);lz[p]^=1;}
 3 void down(int p){if(lz[p])rev(lc),rev(rc),lz[p]=0;}
 4 void up(int p){...}
 5 void rotate(int p){
 6     int fa=f[p],gr=f[fa],dir=c[fa][1]==p,br=c[p][!dir];
 7     c[p][!dir]=fa;c[fa][dir]=br;if(not_root(fa))c[gr][c[gr][1]==fa]=p;
 8     f[br]=fa;f[fa]=p;f[p]=gr;up(fa);
 9 }
10 void splay(int p){
11     int top=0,fa,gr;q[++top]=p;
12     for(int r=p;not_root(r);q[++top]=r=f[r]);for(;top;down(q[top--]));
13     while(not_root(p)){fa=f[p];gr=f[fa];
14         if(not_root(fa))rotate(p);rotate(p);
15     }up(p);
16 }
17 void access(int p){for(int r=0;p;p=f[r=p])splay(p),rc=r,up(p);}
18 void make(int p){access(p);splay(p);rev(p);}
19 int find(int p){access(p);splay(p);for(;lc;p=lc);splay(p);return p;}
20 void split(int x,int y){make(x);access(y);splay(y);}
21 void link(int x,int y){make(x);f[x]=y;}
22 void cut(int x,int y){split(x,y);f[x]=c[y][0]=0;up(y);}
比不少人短了将近一半的板子。。。

相关文章:

  • 2021-08-15
  • 2021-10-03
  • 2022-02-19
  • 2021-12-19
  • 2022-12-23
  • 2021-03-16
  • 2021-08-14
  • 2021-11-02
猜你喜欢
  • 2022-12-23
  • 2021-10-11
  • 2021-08-08
  • 2021-06-01
  • 2021-10-08
  • 2021-06-09
相关资源
相似解决方案