「专题总结」LCT入门

上次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]][]==p||c[f[p]][]==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]^=;}

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

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

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][]==p,br=c[p][!dir];
c[p][!dir]=fa;c[fa][dir]=br;if(not_root(fa))c[gr][c[gr][]==fa]=p;
f[br]=fa;f[fa]=p;f[p]=gr;up(fa);
}

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

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

void splay(int p){
int top=;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=,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][]==p^c[gr][]==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=;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][])return false;//原树并不存在这条边

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

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

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

void cut(int x,int y){split(x,y);f[x]=c[y][]=;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不要总是对着板子打下来了。。。

 bool not_root(int p){return c[f[p]][]==p||c[f[p]][]==p;}
void rev(int p){swap(lc,rc);lz[p]^=;}
void down(int p){if(lz[p])rev(lc),rev(rc),lz[p]=;}
void up(int p){...}
void rotate(int p){
int fa=f[p],gr=f[fa],dir=c[fa][]==p,br=c[p][!dir];
c[p][!dir]=fa;c[fa][dir]=br;if(not_root(fa))c[gr][c[gr][]==fa]=p;
f[br]=fa;f[fa]=p;f[p]=gr;up(fa);
}
void splay(int p){
int top=,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(p);rotate(p);
}up(p);
}
void access(int p){for(int r=;p;p=f[r=p])splay(p),rc=r,up(p);}
void make(int p){access(p);splay(p);rev(p);}
int find(int p){access(p);splay(p);for(;lc;p=lc);splay(p);return p;}
void split(int x,int y){make(x);access(y);splay(y);}
void link(int x,int y){make(x);f[x]=y;}
void cut(int x,int y){split(x,y);f[x]=c[y][]=;up(y);}

比不少人短了将近一半的板子。。。

稍后上例题。

(当时的板子还很麻烦而丑陋不要在意)

Cave洞穴勘测

辉辉热衷于洞穴勘测。某天,他按照地图来到了一片被标记为JSZX的洞穴群地区。

经过初步勘测,辉辉发现这片区域由n个洞穴(分别编号为1到n)以及若干通道组成,并且每条通道连接了恰好两个洞穴。

假如两个洞穴可以通过一条或者多条通道按一定顺序连接起来,那么这两个洞穴就是连通的,按顺序连接在一起的这些通道则被称之为这两个洞穴之间的一条路径。

洞穴都十分坚固无法破坏,然而通道不太稳定,时常因为外界影响而发生改变.

比如,根据有关仪器的监测结果,123号洞穴和127号洞穴之间有时会出现一条通道,有时这条通道又会因为某种稀奇古怪的原因被毁。

辉辉有一台监测仪器可以实时将通道的每一次改变状况在辉辉手边的终端机上显示:

如果监测到洞穴u和洞穴v之间出现了一条通道,终端机上会显示一条指令 Connect u v

如果监测到洞穴u和洞穴v之间的通道被毁,终端机上会显示一条指令 Destroy u v

经过长期的艰苦卓绝的手工推算,辉辉发现一个奇怪的现象:无论通道怎么改变,任意时刻任意两个洞穴之间至多只有一条路径。

因而,辉辉坚信这是由于某种本质规律的支配导致的。因而,辉辉更加夜以继日地坚守在终端机之前,试图通过通道的改变情况来研究这条本质规律。

然而,终于有一天,辉辉在堆积成山的演算纸中崩溃了……他把终端机往地面一砸(终端机也足够坚固无法破坏),转而求助于你,说道:“你老兄把这程序写写吧”。

辉辉希望能随时通过终端机发出指令 Query u v,向监测仪询问此时洞穴u和洞穴v是否连通。

现在你要为他编写程序回答每一次询问。

已知在第一条指令显示之前,JSZX洞穴群中没有任何通道存在。$n \leq 10000,m \le 200000$

保证连边断边都合法,询问是否联通。很多函数都用不到。是一个合格的模板题。

 #include<cstdio>
#define V inline void
#define I register int
int f[],n,m,c[][],lz[],st[];char o[];
bool nroot(I p){return c[][f[p]]==p||c[][f[p]]==p;}
V rev(I p){if(p)c[][p]^=c[][p]^=c[][p]^=c[][p],lz[p]^=;}
V down(I p){if(lz[p])lz[p]=,rev(c[][p]),rev(c[][p]);}
V rotate(I p){
I fa=f[p],gr=f[fa],k=c[][fa]==p,s=c[!k][p];
if(nroot(fa))c[c[][gr]==fa][gr]=p;c[!k][p]=fa;c[k][fa]=s;
if(s)f[s]=fa;f[fa]=p;f[p]=gr;
}
V splay(I p){
I y=p,tp=;st[++tp]=p;
while(nroot(y))st[++tp]=y=f[y];
while(tp)down(st[tp--]);
while(nroot(p)){
I fa=f[p],gr=f[fa];
if(nroot(fa))rotate((c[][fa]==p)^(c[][gr]==fa)?p:fa);
rotate(p);
}
}
V access(I p){for(I x=;p;p=f[x=p])splay(p),c[][p]=x;}
V make(I p){access(p);splay(p);rev(p);}
int find(I p){access(p);splay(p);while(c[][p])down(p),p=c[][p];splay(p);return p;}
V link(I x,I y){make(x);find(y);f[x]=y;}
V cut(I x,I y){make(x);find(y);f[y]=c[][x]=;}
main(){
scanf("%d%d",&n,&m);I x,y;
while(m--){
scanf("%s%d%d",o,&x,&y);if(x<y)x^=y^=x^=y;
if(o[]=='C')link(x,y);
else if(o[]=='Q')make(x),puts(find(y)==x?"Yes":"No");
else cut(x,y);
}
}

树的维护

由N个结点组成的树。树的节点被编号为1到N,边被编号为1到N-1。每一条边有一个权值。然后你要在树上执行一系列指令。指令可以是如下三种之一:

CHANGE i v:将第i条边的权值改成v。

NEGATE a b:将点a到点b路径上所有边的权值变成其相反数。

QUERY a b:找出点a到点b路径上各边的最大权值。

$n \le 10000 ,m \le 100000$

没Link没Cut不卡nlog2n,真是一道树剖好题。

首先这道题它的权值在边上,不是很好维护,所以用到一些技巧,就是边化点。

对于每条边都开一个点,将边权放在这个点上,然后再把这个代表边的点与原边的两个端点连边。

然后就变成了单点权值修改,路径取相反数,查极值。

相反数之后怎么还能求最大值啊?其实,相反数之后,最大值就变成了最小值,最小值就变成了最大值。

所以只要顺便再维护一下最小值,每次取相反数时最大小值交换再取负就好了。

其实思路还是挺神仙的,但是当时执着于板子水过去了。。。

 #include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
#define lc c[p][0]
#define rc c[p][1]
int n,f[],c[][],mx[],mn[],lz[],lzn[],v[];
int sta[],A,B;char o[];
bool not_root(int p){ return c[f[p]][]==p||c[f[p]][]==p; }
void rev(int p){ if(p)lc^=rc^=lc^=rc,lz[p]^=; }
void neg(int p){
if(!p)return;
int res=mx[p]; mx[p]=-mn[p]; mn[p]=-res; v[p]=-v[p];
lzn[p]^=;
}
void up(int p){
if(lzn[p])neg(lc),neg(rc),lzn[p]=;
mn[p]=min(mn[lc],mn[rc]); mx[p]=max(mx[lc],mx[rc]);
if(p>n)mn[p]=min(v[p],mn[p]),mx[p]=max(v[p],mx[p]);
}
void down(int p){
if(lz[p]) rev(lc),rev(rc),lz[p]=;
if(lzn[p]) neg(lc),neg(rc),lzn[p]=;
}
void rotate(int p){
int fa=f[p],gr=f[fa],k=c[fa][]==p,s=c[p][!k];
if(not_root(fa)) c[gr][c[gr][]==fa]=p;
c[p][!k]=fa; c[fa][k]=s;
if(s) f[s]=fa;
f[p]=gr; f[fa]=p;
up(fa); up(p); up(gr);
}
void splay(int p){
int res=p,top=;
sta[++top]=p;
while(not_root(res)) sta[++top]=res=f[res];
while(top) down(sta[top--]);
while(not_root(p)){
int fa=f[p],gr=f[fa];
if(not_root(fa)) rotate((c[gr][]==fa)^(c[fa][]==p)?p:fa);
rotate(p);
}
up(p);
}
void access(int p){ for(int y=;p;p=f[y=p])splay(p),rc=y,up(p); }
void make_root(int p){ access(p); splay(p); rev(p); }
void split(int x,int y){ make_root(x); access(y); splay(y); }
void link(int x,int y){ make_root(x); f[x]=y; }
int main(){
scanf("%d",&n);
mn[]=; mx[]=-;
for(int i=,a,b,vv;i<n;++i)
scanf("%d%d%d",&a,&b,&vv),link(a,n+i),link(n+i,b),v[n+i]=vv;
while(){
scanf("%s",o);
if(o[]=='D')return ;
scanf("%d%d",&A,&B);
if(o[]=='C') make_root(n+A),v[n+A]=B,up(n+A);
else if(o[]=='N') split(A,B),neg(B);
else split(A,B),printf("%d\n",mx[B]);
/* for(int i=1;i<2*n;++i)printf("%d ",c[i][0]);puts("");
for(int i=1;i<2*n;++i)printf("%d ",c[i][1]);puts("");
for(int i=1;i<2*n;++i)printf("%d ",f[i]);puts("");
for(int i=1;i<2*n;++i)printf("%d ",lz[i]);puts("");
for(int i=1;i<2*n;++i)printf("%d ",lzn[i]);puts("");
for(int i=1;i<2*n;++i)printf("%d ",mx[i]);puts("");
for(int i=1;i<2*n;++i)printf("%d ",mn[i]);puts("");puts("---");*/
}
}

看这调试语句就知道我经历了什么

tree

一棵n个点的树,每个点的初始权值为1。对于这棵树有q个操作,每个操作为以下四种操作之一:

+ u v c:将u到v的路径上的点的权值都加上自然数c;

- u1 v1 u2 v2:将树中原有的边(u1,v1)删除,加入一条新边(u2,v2),保证操作完之后仍然是一棵树;

* u v c:将u到v的路径上的点的权值都乘上自然数c;

/ u v:询问u到v的路径上的点的权值和,求出答案对于51061的余数。

本来大多数操作还是可以用树剖的,但是有Link/Cut就只能LCT了。

但是其实还是挺板子的。只不过下传标记麻烦了一点,就像打线段树一样打就可以。

 #include<cstdio>
#define lc c[p][0]
#define rc c[p][1]
#define mod 51061
#define int long long
int M[],A[],v[];int lz[],f[],c[][];
int n,m,sum[],sta[],a,b,C,d,siz[];char s[];
bool not_root(int p){return c[f[p]][]==p||c[f[p]][]==p;}
void rev(int p){if(p)lc^=rc^=lc^=rc,lz[p]^=;}
void down(int p){
if(M[p]!=)(v[lc]*=M[p])%=mod,(v[rc]*=M[p])%=mod,(M[lc]*=M[p])%=mod,(M[rc]*=M[p])%=mod,
(A[lc]*=M[p])%=mod,(A[rc]*=M[p])%=mod,(sum[lc]*=M[p])%=mod,(sum[rc]*=M[p])%=mod,M[p]=;
if(A[p])(v[lc]+=A[p])%=mod,(v[rc]+=A[p])%=mod,(A[lc]+=A[p])%=mod,(A[rc]+=A[p])%=mod,
(sum[lc]+=siz[lc]*A[p])%=mod,(sum[rc]+=siz[rc]*A[p])%=mod,A[p]=;
if(lz[p])rev(lc),rev(rc),lz[p]=;
}
void up(int p){down(p);sum[p]=((lc?sum[lc]:)+(rc?sum[rc]:)+v[p])%mod;siz[p]=(siz[lc]+siz[rc]+)%mod;}
void rotate(int p){
int fa=f[p],gr=f[fa],k=c[fa][]==p,s=c[p][!k];
if(not_root(fa))c[gr][c[gr][]==fa]=p; c[p][!k]=fa; c[fa][k]=s;
if(s)f[s]=fa; f[fa]=p; f[p]=gr; up(fa);
}
void splay(int p){
int res=p,top=;sta[++top]=p;
while(not_root(res))sta[++top]=res=f[res];
while(top)down(sta[top--]);
while(not_root(p)){
int fa=f[p],gr=f[fa];
if(not_root(fa))rotate((c[fa][]==p)^(c[gr][]==fa)?p:fa);
rotate(p);
}
up(p);
}
void access(int p){for(int y=;p;p=f[y=p])splay(p),rc=y,up(p);}
void make_root(int p){access(p);splay(p);rev(p);}
void split(int x,int y){make_root(x);access(y);splay(y);}
void link(int x,int y){make_root(x);f[x]=y;up(y);}
void cut(int x,int y){split(x,y);f[x]=c[y][]=;up(y);}
main(){
scanf("%lld%lld",&n,&m);
for(int i=;i<=n;++i)v[i]=M[i]=siz[i]=;
for(int i=,sa,sb;i<n;++i)scanf("%lld%lld",&sa,&sb),link(sa,sb);
while(m--){
scanf("%s",s);
if(s[]=='+')scanf("%lld%lld%lld",&a,&b,&C),split(a,b),(A[b]+=C)%=mod,(v[b]+=C)%=mod,(sum[b]+=siz[b]*C)%=mod;
if(s[]=='-')scanf("%lld%lld%lld%lld",&a,&b,&C,&d),cut(a,b),link(C,d);
if(s[]=='*')scanf("%lld%lld%lld",&a,&b,&C),split(a,b),(M[b]*=C)%=mod,(A[b]*=C)%=mod,(v[b]*=C)%=mod,(sum[b]*=C)%=mod;
if(s[]=='/')scanf("%lld%lld",&a,&b),split(a,b),printf("%lld\n",sum[b]);
}
}

水管局长数据加强版

SC省MY市有着庞大的地下水管网络,嘟嘟是MY市的水管局长(就是管水管的啦),嘟嘟作为水管局长的工作就是:每天供水公司可能要将一定量的水从x处送往y处,嘟嘟需要为供水公司找到一条从A至B的水管的路径,接着通过信息化的控制中心通知路径上的水管进入准备送水状态,等到路径上每一条水管都准备好了,供水公司就可以开始送水了。嘟嘟一次只能处理一项送水任务,等到当前的送水任务完成了,才能处理下一项。
在处理每项送水任务之前,路径上的水管都要进行一系列的准备操作,如清洗、消毒等等。嘟嘟在控制中心一声令下,这些水管的准备操作同时开始,但由于各条管道的长度、内径不同,进行准备操作需要的时间可能不同。供水公司总是希望嘟嘟能找到这样一条送水路径,路径上的所有管道全都准备就绪所需要的时间尽量短。嘟嘟希望你能帮助他完成这样的一个选择路径的系统,以满足供水公司的要求。另外,由于MY市的水管年代久远,一些水管会不时出现故障导致不能使用,你的程序必须考虑到这一点。
不妨将MY市的水管网络看作一幅简单无向图(即没有自环或重边):水管是图中的边,水管的连接处为图中的结点。

N ≤ 100000,M ≤ 1000000,Q ≤ 100000
任何时候我们考虑的水管网络都是连通的,即从任一结点A必有至少一条水管路径通往任一结点B。

这就是需要维护图的题了。直接弄肯定是不行的。

还记得一道叫做货车运输的题吗?当时题意也差不多,就是查询路径最大值尽量小。

可以用最小生成树的对吧。

但是这题有Link/Cut怎么办?而且还是在图上。。。

但是你已经知道是最小生成树了,那就可以用lct来维护了啊。

但是还没完,你断掉一个水管之后,你怎么知道哪条边才是最小生成树上的替补啊?

发现这道题并没有强制在线。。。

所以,你不会断边,但是加边你还是会的吧。

加边就是在当前的最小生成树下,找到要新连的边的两个端点在最小生成树上路径的最大边权。

如果这个最大边权小于等于新边,那么新边就没用了。啥也不用干。

否则,就把生成树里最大的那条边断掉(就是Cut掉它对应的点和两个端点),再同理把新边连上。

看样子你好像会连边了。可是。。。可是你还是不会删边啊!

时光倒流。。。过程中所有报废的水管都直接不建,然后从后往前处理每一个操作,这样删边就变成了加边。

套路,不会也没办法,记住且会用就好了。

我好像是写过题解的。。。

 #include<cstdio>
#include<queue>
using namespace std;
#define lc c[p][0]
#define rc c[p][1]
int f[],c[][],n,m,q,lz[],mx[],mxp[],v[];
int sta[],A[],B[],V[],opt[],x[],y[];
int lck[],ans[];
struct hashmap{
#define mod 2000003
int fir[],l[],x[],y[],v[],cnt;
int &operator[](pair<int,int>p){
int xx=p.first,yy=p.second;
if(xx<yy)xx^=yy^=xx^=yy;
long long hsh=(xx*19733737ll+yy)%mod;
for(int i=fir[hsh];i;i=l[i])if(x[i]==xx&&y[i]==yy)return v[i];
l[++cnt]=fir[hsh];fir[hsh]=cnt;x[cnt]=xx;y[cnt]=yy;return v[cnt];
}
}mm;
void read(int &p,register char ch=getchar()){
while(ch>''||ch<'')ch=getchar();
while(ch<=''&&ch>='')p=(p<<)+(p<<)+ch-,ch=getchar();
}
bool not_root(int p){return c[f[p]][]==p||c[f[p]][]==p;}
void rev(int p){lc^=rc^=lc^=rc,lz[p]^=;}
void up(int p){
if(mx[lc]>mx[rc])mx[p]=mx[lc],mxp[p]=mxp[lc];
else mx[p]=mx[rc],mxp[p]=mxp[rc];
if(v[p]>mx[p])mx[p]=v[p],mxp[p]=p;
}
void down(int p){if(lz[p])rev(lc),rev(rc),lz[p]=;}
void rotate(int p){
int fa=f[p],gr=f[fa],k=c[fa][]==p,br=c[p][!k];
if(not_root(fa))c[gr][c[gr][]==fa]=p; c[p][!k]=fa; c[fa][k]=br;
f[p]=gr; f[fa]=p; f[br]=fa; up(fa);
}
void splay(int p){
int res=p,top=;sta[++top]=p;
while(not_root(res))sta[++top]=res=f[res];
while(top)down(sta[top--]);
while(not_root(p)){
int fa=f[p],gr=f[fa];
if(not_root(fa))rotate((c[fa][]==p)^(c[gr][]==fa)?fa:p);
rotate(p);
}
up(p);
}
void access(int p){for(int y=;p;p=f[y=p])splay(p),rc=y,up(p);}
void make_root(int p){access(p);splay(p);rev(p);}
void split(int x,int y){make_root(x);access(y);splay(y);}
void link(int x,int y){make_root(x);f[x]=y;up(y);}
void cut(int x,int y){split(x,y);f[x]=c[y][]=;up(y);}
struct edge{
int a,b,l,num;
friend bool operator<(edge a,edge b){return a.l>b.l;}
}e[];
priority_queue<edge>qu;
int fa[];
int find(int k){return fa[k]==k?k:fa[k]=find(fa[k]);}
void Kruscal(){
for(int i=;i<=n;++i)fa[i]=i;
for(int i=;i<=m;++i)if(!lck[n+i])qu.push((edge){A[i],B[i],V[i],n+i});
while(!qu.empty()){
int a=qu.top().a,b=qu.top().b,v=qu.top().l,num=qu.top().num;qu.pop();
if(find(a)!=find(b))fa[fa[a]]=fa[b],link(a,num),link(num,b);
}
}
int main(){
scanf("%d%d%d",&n,&m,&q);
for(int i=;i<=m;++i)read(A[i]),read(B[i]),read(V[i]),mm[make_pair(A[i],B[i])]=n+i,v[n+i]=V[i];
for(int i=;i<=q;++i)read(opt[i]),read(x[i]),read(y[i]),opt[i]--;
for(int i=;i<=q;++i)if(opt[i])lck[mm[make_pair(x[i],y[i])]]=;
Kruscal();
for(int i=q;i;--i)
if(opt[i]){
split(x[i],y[i]);int nmx=mx[y[i]],tbc=mxp[y[i]],obj=mm[make_pair(x[i],y[i])];
if(nmx<=V[obj-n])continue;
cut(tbc,A[tbc-n]);cut(tbc,B[tbc-n]);
link(obj,x[i]);link(obj,y[i]);
}else split(x[i],y[i]),ans[i]=mx[y[i]];
for(int i=;i<=q;++i)if(!opt[i])printf("%d\n",ans[i]);
}

GERALD07加强版

N个点M条边的无向图,询问保留图中编号在[l,r]的边的时候图中的联通块个数。1≤N、M、K≤200,000。强制在线。

现在看来,线段树分治+并查集不乏是一个不错的做法。但是强制在线的话就算了。

正解的做法还是特别神仙。也大概用到了上一题的东西。

从前往后扫每一条边,构造生成树,最小生成树,以边的编号为边权。

然后动态构造最小生成树的时候,我们能记录下每条边取代了什么时候的一条边。

既然是统计联通块数,那么对于新加入的边,它肯定能在后续的询问里存活时间更长,要比之前的边更优,所以要取代。

然后考虑最后,每条边对询问的贡献。

对于询问[l,r],如果这区间里的一条边所取代边在l之后,那么它其实并不会贡献答案,他并没有使两个不联通的块联通。

而如果在l之前,那么就取代了一条在[l,r]之间实际并不存在的边,这条边就存在于生成树中。

所以现在的问题就是,问区间[l,r]中有多少个位置所取代的边编号小于l,主席树就可以维护。

当时的题解写的是个啥啊。。。

 #include<cstdio>
#include<iostream>
using namespace std;
int c[][],f[],w[],n,m,k,opt,fid[],lst[],q[];
int x[],y[],ans,rt[],v[],t[][],lz[],cnt;
int find(int p){return fid[p]==p?p:fid[p]=find(fid[p]);}
#define lc c[p][0]
#define rc c[p][1]
bool not_root(int p){return c[f[p]][]==p||c[f[p]][]==p;}
void rev(int p){lc^=rc^=lc^=rc;lz[p]^=;}
void down(int p){if(lz[p])rev(lc),rev(rc),lz[p]=;}
void up(int p){w[p]=min(p>n?p:,min(w[lc],w[rc]));}
void rotate(int p){
int fa=f[p],gr=f[fa],dir=c[fa][]==p,br=c[p][!dir];
if(not_root(fa))c[gr][c[gr][]==fa]=p; c[p][!dir]=fa; c[fa][dir]=br;
f[p]=gr; f[fa]=p; f[br]=fa; up(fa);
}
void splay(int p){
int res=p,top=;q[++top]=p;
while(not_root(res))q[++top]=res=f[res];
while(top)down(q[top--]);
while(not_root(p)){
int fa=f[p],gr=f[fa];
if(not_root(fa))rotate(c[fa][]==p^c[gr][]==fa?fa:p);
rotate(p);
}
up(p);
}
void access(int p){for(int y=;p;p=f[y=p])splay(p),rc=y,up(p);}
void make_root(int p){access(p);splay(p);rev(p);}
void split(int x,int y){make_root(x);access(y);splay(y);}
void cut(int x,int y){split(x,y);f[x]=c[y][]=;up(y);}
void link(int x,int y){make_root(x);f[x]=y;up(y);}
void build(int &p,int cpy,int adx,int l=,int r=m){
if(!p)p=++cnt;
if(l==r){v[p]=v[cpy]+;return;}
if(adx<=l+r>>)build(t[p][],t[cpy][],adx,l,l+r>>),t[p][]=t[cpy][];
else build(t[p][],t[cpy][],adx,(l+r>>)+,r),t[p][]=t[cpy][];
v[p]=v[t[p][]]+v[t[p][]];//printf("%d %d %d\n",l,r,v[p]);
}
int ask(int p1,int p2,int l,int r,int cl=,int cr=m){//printf("%d %d %d %d\n",cl,cr,v[p2],v[p1]);
if(!(v[p2]-v[p1]))return ;
if(l<=cl&&cr<=r)return v[p2]-v[p1];
return (l<=cl+cr>>?ask(t[p1][],t[p2][],l,r,cl,cl+cr>>):)+(r>cl+cr>>?ask(t[p1][],t[p2][],l,r,(cl+cr>>)+,cr):);
}
int main(){w[]=;
scanf("%d%d%d%d",&n,&m,&k,&opt);
for(int i=;i<=n;++i)fid[i]=i;
for(int i=;i<=m;++i){
scanf("%d%d",&x[i],&y[i]);
if(x[i]==y[i])lst[i]=i;
else if(find(x[i])!=find(y[i]))fid[fid[x[i]]]=fid[y[i]],link(x[i],n+i),link(n+i,y[i]);
else split(x[i],y[i]),lst[i]=w[y[i]]-n,cut(lst[i]+n,x[lst[i]]),cut(lst[i]+n,y[lst[i]]),
link(x[i],n+i),link(y[i],n+i);
build(rt[i],rt[i-],lst[i]);//printf("%d\n",lst[i]);
}
for(int i=,l,r;i<=k;++i){
scanf("%d%d",&l,&r);
if(opt)l^=ans,r^=ans;
ans=n-ask(rt[l-],rt[r],,l-);
printf("%d\n",ans);
}
}

Sone1是肯定不会写的。

难存的情缘

一天机房的夜晚,无数人在MC里奋斗着。。。

大家都知道矿产对于MC来说是多么的重要,但由于矿越挖越少,勇士们不得不跑到更远的地方挖矿,但这样路途上就会花费相当大的时间,导致挖矿效率低下。

cjj提议修一条铁路,大家一致同意。

大家都被CH分配了一些任务:

zjmfrank2012负责绘制出一个矿道地图,这个地图包括家(当然这也是一个矿,毕竟不把家掏空我们是不会走的),和无数个矿,所以大家应该可以想出这是一个无向无环图,也就是一棵树。

Digital_T和cstdio负责铺铁路。。所以这里没他们什么事,两位可以劳作去了。

这个时候song526210932和RMB突然发现有的矿道会刷怪,并且怪的数量会发生变化。作为采矿主力,他们想知道从一个矿到另一个矿的路上哪一段会最困难。。。(困难值用zjm的死亡次数表示)

1<=N<=10000,1<=c<=1000000,1<=操作次数<=100000

树剖专题里的玩意又过来了,可见树剖的优越性。

不Link不Cut单点修改链上最值,板子不说了。

 #include<cstdio>
#include<iostream>
using namespace std;
#define lc c[p][0]
#define rc c[p][1]
int f[],c[][],n,v[],mx[],lz[];
int sta[],o1,o2;char o[];
bool not_root(int p){return c[f[p]][]==p||c[f[p]][]==p;}
void rev(int p){if(p)lc^=rc^=lc^=rc,lz[p]^=;}
void down(int p){if(lz[p])rev(lc),rev(rc),lz[p]=;}
void up(int p){mx[p]=max(mx[lc],mx[rc]);if(p>n)mx[p]=max(mx[p],v[p]);}
void rotate(int p){
int fa=f[p],gr=f[fa],k=c[fa][]==p,s=c[p][!k];
if(not_root(fa))c[gr][c[gr][]==fa]=p; c[p][!k]=fa; c[fa][k]=s;
if(s)f[s]=fa; f[fa]=p; f[p]=gr;
up(fa);
}
void splay(int p){
int res=p,top=;sta[++top]=p;
while(not_root(res))sta[++top]=res=f[res];
while(top)down(sta[top--]);
while(not_root(p)){
int fa=f[p],gr=f[fa];
if(not_root(fa))rotate((c[gr][]==fa)^(c[fa][]==p)?p:fa);
rotate(p);
}
up(p);
}
void access(int p){for(int y=;p;p=f[y=p])splay(p),rc=y,up(p);}
void make_root(int p){access(p);splay(p);rev(p);}
void split(int x,int y){make_root(x);access(y);splay(y);}
void link(int x,int y){make_root(x);f[x]=y;}
int main(){
scanf("%d",&n);
for(int i=,a,b,vv;i<n;++i)scanf("%d%d%d",&a,&b,&v[n+i]),link(a,n+i),link(n+i,b);
while(){//puts("$");
scanf("%s",o);
if(o[]=='D')return ;
scanf("%d%d",&o1,&o2);
if(o[]=='C')make_root(n+o1),v[n+o1]=o2,up(n+o1);
else split(o1,o2),printf("%d\n",mx[o2]);
}
}
上一篇:JS.match方法 正则表达式


下一篇:LCT(Link Cut Tree)总结