【10】LCT学习笔记
前言
老早就想写了,但是一直抽不出时间。借助集训的契机把这篇学习笔记写出来。
时间跨度比较长,可能有一些代码不是现在的码风,我会标注出来的。
LCT 挺简单的,内容应该不多吧qwq。
长文警告:本文一共 \(1038\) 行,请合理安排阅读时间。
前置知识:【8】平衡树学习笔记 中的 Splay 部分。
LCT
LCT 是一种动态树,能在 \(O(\log n)\) 的复杂度内支持加边删边,换根以及大部分路径信息。LCT 是一种强路径弱子树的数据结构,遇到子树类问题还是需要考虑 DFS 序拍成序列或树剖。时间复杂度我不会证,所以就不证了,反正用起来实际效果和 \(O(n\log^2 n)\) 没啥区别。
LCT 可以维护森林,它将每一棵树分成若干条实链,同一实链节点之间通过实边相连,不同实链之间通过虚边相连。以下根均指节点对应的树的根。
每个点连向儿子的边中有且仅有一条实边,某条虚边变成实边时这条实边会变成虚边。
同一实链的节点通过一棵 Splay 维护,Splay 满足二叉搜索树的性质的关键字为节点的相对深度。即同一实链中深度最浅的点在最左子树,深度最深的点在最右子树。
Splay 的根节点的父节点为空,我们考虑利用这个空位。在 LCT 中,一条实链的 Splay 的根节点的父节点记录的是这条实链中最浅的节点在原树中的父节点。这固定了实链的位置。
上面这种特殊的记录方式对应了一条虚边。因此,实边的特点说节点父子相认,而虚边的特点是儿子认父亲,但父亲不认儿子。
判断左右儿子
由于旋转时需要确定方向,所以我们需要知道一个节点是其父亲的左儿子还是右儿子。这个过程可以单独写一个函数。
int wh(int x)
{
return ch[fa[x]][1]==x;
}
判断是否为根
利用 LCT 中 Splay 的根节点儿子认父亲,但父亲不认儿子的特性,如果一个节点不是它父亲的任何一个儿子,那么这个节点就是某个 Splay 的根。
bool isroot(int x)
{
return (x!=ch[fa[x]][0])&&(x!=ch[fa[x]][1]);
}
旋转
LCT 中的旋转与 Splay 的旋转本质相同,但需要特判如果 \(x\) 的爷爷 \(z\) 不是这条链上的点,即 \(y\) 是这个 Splay 的根节点,此时不能更新 \(z\) 的儿子,但是 \(x\) 的父亲照常更新,因为儿子认父亲,但父亲不认儿子。
void rotate(int x)
{
int y=fa[x],z=fa[y],k=wh(x);
if(!isroot(y))ch[z][wh(y)]=x;
fa[x]=z,fa[y]=x,fa[ch[x][k^1]]=y;
ch[y][k]=ch[x][k^1],ch[x][k^1]=y;
pushup(y),pushup(x);
}
Splay
和正常 Splay 的操作一样,把某个节点旋转到 Splay 的根。注意特判 \(y\) 为根的情况以及 pushdown 下放标记。
void splay(int x)
{
top=0,st[++top]=x;
for(int i=x;!isroot(i);i=fa[i])st[++top]=fa[i];
while(top)pushdown(st[top]),top--;
while(!isroot(x))
{
int y=fa[x];
if(!isroot(y))
{
if(wh(x)==wh(y))rotate(y);
else rotate(x);
}
rotate(x);
}
}
打通到根
把一个点 \(x\) 到根的路径打通为一条实链。我们一条实链一条实链地跳,设 \(x\) 是从上一条实链的根节点 \(t\) (初始状态为 \(0\) 表示空)跳一次 \(fa\) 过来的,先把 \(x\) 转到所在 Splay 的根,然后令 \(ch[x][1]=t\)。这个赋值操作有两个意义,一个是断开原来的实边,连上虚边。因为原本的 \(ch[x][1]\) 的父亲依旧是 \(x\),但是 \(x\) 已经不认它了,满足虚边的特点。另一个是连接新的实边,\(x\) 是 \(t\) 跳 \(fa\) 得到的,父亲是 \(x\),现在 \(x\) 也认它了,满足实边的特点。
并且这个 \(ch[x][1]\) 也满足了深度的二叉搜索树性质。\(x\) 是 \(t\) 跳 \(fa\) 得到的,合并到同一个链中 \(t\) 的相对深度更深,所以是 \(x\) 的右儿子。并且原本的 \(ch[x][1]\) 以及其子树是原本实链中比 \(x\) 深度更深的点,因为 \(t\) 连了实边,那么比 \(x\) 深度更深的点都会因为 \(x\) 连向 \(ch[x][1]\) 子树中最浅的边,即 \(x\) 的原实链中的直接儿子的连边变为虚边断开,在代码中表现为 \(ch[x][1]\) 被 \(t\) 覆盖。
记得 pushup。
void access(int x)
{
for(int t=0;x;t=x,x=fa[x])splay(x),ch[x][1]=t,pushup(x);
}
换根
把 \(x\) 换成树根。考虑直接把 \(x\) 打通到根,然后把 \(x\) 转到对应 Splay 的根节点,把整个 Splay 打上翻转标记。和【8】平衡树学习笔记 中的例题 \(2\) 一样。
因为把 \(x\) 换成树根之后,对其他实链节点的相对深度以及实链之间连接的节点没有影响,唯一有影响是 \(x\) 所处的实链。由于 access 操作打通到根是 \(x\) 是最深的节点,原本的根是最浅的节点,所以把整条链翻转一下就把根换成了 \(x\)。
void makeroot(int x)
{
access(x),splay(x),re[x]^=1;
}
由于时间久远,这个翻转标记并不是一般翻转标记的打法,正常的翻转标记应该是例题 \(2\) 中的写法。
提取路径
先把 \(x\) 换成根,再把 \(y\) 打通到根,那 \(y\) 这条实链不就是 \(x\) 和 \(y\) 之间的路径吗?操作或查询时记得把 \(y\) 转到根节点,不然不是对整个 Splay 进行操作。
int query(int x)
{
makeroot(x),access(y),splay(y);
return v[y];
}
找根
找节点 \(x\) 所在的子树的最浅的节点,如果 \(x\) 是整个 Splay 的根就是找这条实链的根节点。把 \(x\) 转到 Splay 的根,一直走左子树就是深度最浅的点。记得最后 Splay 保证复杂度。
int findroot(int x)
{
splay(x);
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
连边
连接节点 \(x,y\)。先把根换为 \(x\),再把 \(y\) 打通到根,然后把 \(y\) 转到 Splay 的根。然后把 \(x\) 的父节点连向 \(y\),相当于向 \(y\) 连了一条虚边,整个树的根变为 \(y\)。
此时如果 \(x,y\) 已经连通,那 \(x,y\) 肯定在同一条实链上,查 \(y\) 所在的实链的根一定为 \(x\),就不能修改 \(x\) 的父节点。而 \(y\) 在 Splay 的根节点,所以直接 findroot 就能判断。
void link(int x,int y)
{
makeroot(x),access(y),splay(y);
if(findroot(y)!=x)fa[x]=y;
}
删边
删除节点 \(x,y\) 之间的连边。先把根换为 \(x\),再把 \(y\) 打通到根,然后把 \(y\) 转到 Splay 的根。\(x\) 为根,在链中深度最浅,\(y\) 和 \(x\) 如果有直连边,那么 \(y\) 是深度次浅的节点。现在 \(y\) 是 Splay 的根,所以 \(ch[y][0]\) 只能是 \(x\)。把 \(x\) 的父节点和 \(ch[y][0]\) 指向空,就删掉了这条边。记得 pushup。
特别的,如果 \(ch[ch[y][0]][1]\) 不为空,证明 \(x\) 和 \(y\) 只是连通,并没有直连边,所以还需要再判一下。
void cut(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(ch[y][0]==x&&ch[ch[x][0]][1]==0)fa[x]=0,ch[y][0]=0,pushup(y);
}
例题
例题 \(1\) :
LCT 模板题,Splay 每个节点维护子树内所有点的权值的异或和,查询时提取路径后取 Splay 的根的信息即可,不多赘述。
再强调一遍,这里的懒标记定义有问题,最好按照例题 \(2\) 的写法,打上标记时立即更新当前节点的记录的状态,不然很容易出锅。
#include <bits/stdc++.h>
using namespace std;
long long n,m,x,y,c,a[400000],siz[400000],v[400000],ad[400000],mu[400000],ch[400000][2],fa[400000],re[400000],st[400000],top=0,cnt=0;
const long long mod=51061;
char op;
void pushup(long long x)
{
siz[x]=(siz[ch[x][0]]+siz[ch[x][1]]+1)%mod;
v[x]=(v[ch[x][0]]+v[ch[x][1]]+a[x])%mod;
}
void pushdown(long long x)
{
if(re[x])re[ch[x][0]]^=1,re[ch[x][1]]^=1,swap(ch[x][0],ch[x][1]),re[x]=0;
for(int i=0;i<=1;i++)
{
v[ch[x][i]]=v[ch[x][i]]*mu[x]%mod,mu[ch[x][i]]=mu[ch[x][i]]*mu[x]%mod,ad[ch[x][i]]=ad[ch[x][i]]*mu[x]%mod;
a[ch[x][i]]=a[ch[x][i]]*mu[x]%mod;
v[ch[x][i]]=(v[ch[x][i]]+ad[x]*siz[ch[x][i]])%mod,ad[ch[x][i]]=(ad[ch[x][i]]+ad[x])%mod;
a[ch[x][i]]=(a[ch[x][i]]+ad[x])%mod;
}
mu[x]=1,ad[x]=0;
}
long long wh(long long x)
{
return x==ch[fa[x]][1];
}
bool isroot(long long x)
{
return (x!=ch[fa[x]][0])&&(x!=ch[fa[x]][1]);
}
void rotate(long long x)
{
long long y=fa[x],z=fa[y],k=wh(x),l=wh(y);
if(!isroot(y))ch[z][l]=x;
fa[x]=z,fa[y]=x,fa[ch[x][k^1]]=y;
ch[y][k]=ch[x][k^1],ch[x][k^1]=y;
pushup(y),pushup(x);
}
void splay(long long x)
{
top=0,st[++top]=x;
for(int i=x;!isroot(i);i=fa[i])st[++top]=fa[i];
while(top)pushdown(st[top]),top--;
while(!isroot(x))
{
long long y=fa[x];
if(!isroot(y))
{
if(wh(x)==wh(y))rotate(y);
else rotate(x);
}
rotate(x);
}
}
void access(long long x)
{
for(long long t=0;x;t=x,x=fa[x])splay(x),ch[x][1]=t,pushup(x);
}
void makeroot(long long x)
{
access(x),splay(x),re[x]^=1;
}
long long findroot(long long x)
{
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
void link(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(findroot(y)!=x)fa[x]=y;
}
void cut(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(ch[y][0]==x&&ch[ch[x][0]][1]==0)fa[x]=0,ch[y][0]=0,pushup(y);
}
void add(long long x,long long y,long long c)
{
makeroot(x),access(y),splay(y);
v[y]=(v[y]+c*siz[y])%mod,ad[y]=(ad[y]+c)%mod,a[y]=(a[y]+c)%mod;
}
void mul(long long x,long long y,long long c)
{
makeroot(x),access(y),splay(y);
v[y]=v[y]*c%mod,mu[y]=mu[y]*c%mod,ad[y]=ad[y]*c%mod,a[y]=a[y]*c%mod;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)a[i]=1,mu[i]=1,ad[i]=0;
for(int i=1;i<=n-1;i++)
{
cin>>x>>y;
link(x,y);
}
for(int i=1;i<=m;i++)
{
cin>>op;
if(op=='+')cin>>x>>y>>c,add(x,y,c);
else if(op=='-')cin>>x>>y,cut(x,y),cin>>x>>y,link(x,y);
else if(op=='*')cin>>x>>y>>c,mul(x,y,c);
else if(op=='/')cin>>x>>y,makeroot(x),access(y),splay(y),printf("%lld\n",v[y]%mod);
}
return 0;
}
例题 \(2\) :
老生常谈的标记下传顺序问题。
Splay 每个节点维护子树内点权和,查询依旧是提取路径后查询 Splay 根的信息。
然后是加法懒标记和乘法懒标记,我们钦定先下传乘法标记,下传乘法标记是同时更新加法标记,即让加法标记也乘以乘法标记的数值。先下传加法标记无法处理标记之间的贡献。
#include <bits/stdc++.h>
using namespace std;
long long n,m,x,y,c,a[400000],siz[400000],v[400000],ad[400000],mu[400000],ch[400000][2],fa[400000],re[400000],st[400000],top=0,cnt=0;
const long long mod=51061;
char op;
void pushup(long long x)
{
siz[x]=(siz[ch[x][0]]+siz[ch[x][1]]+1)%mod;
v[x]=(v[ch[x][0]]+v[ch[x][1]]+a[x])%mod;
}
void pushdown(long long x)
{
if(re[x])re[ch[x][0]]^=1,re[ch[x][1]]^=1,swap(ch[x][0],ch[x][1]),re[x]=0;
for(int i=0;i<=1;i++)
{
v[ch[x][i]]=v[ch[x][i]]*mu[x]%mod,mu[ch[x][i]]=mu[ch[x][i]]*mu[x]%mod,ad[ch[x][i]]=ad[ch[x][i]]*mu[x]%mod;
a[ch[x][i]]=a[ch[x][i]]*mu[x]%mod;
v[ch[x][i]]=(v[ch[x][i]]+ad[x]*siz[ch[x][i]])%mod,ad[ch[x][i]]=(ad[ch[x][i]]+ad[x])%mod;
a[ch[x][i]]=(a[ch[x][i]]+ad[x])%mod;
}
mu[x]=1,ad[x]=0;
}
long long wh(long long x)
{
return x==ch[fa[x]][1];
}
bool isroot(long long x)
{
return (x!=ch[fa[x]][0])&&(x!=ch[fa[x]][1]);
}
void rotate(long long x)
{
long long y=fa[x],z=fa[y],k=wh(x),l=wh(y);
if(!isroot(y))ch[z][l]=x;
fa[x]=z,fa[y]=x,fa[ch[x][k^1]]=y;
ch[y][k]=ch[x][k^1],ch[x][k^1]=y;
pushup(y),pushup(x);
}
void splay(long long x)
{
top=0,st[++top]=x;
for(int i=x;!isroot(i);i=fa[i])st[++top]=fa[i];
while(top)pushdown(st[top]),top--;
while(!isroot(x))
{
long long y=fa[x];
if(!isroot(y))
{
if(wh(x)==wh(y))rotate(y);
else rotate(x);
}
rotate(x);
}
}
void access(long long x)
{
for(long long t=0;x;t=x,x=fa[x])splay(x),ch[x][1]=t,pushup(x);
}
void makeroot(long long x)
{
access(x),splay(x),re[x]^=1;
}
int findroot(int x)
{
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
void link(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(findroot(y)!=x)fa[x]=y;
}
void cut(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(ch[y][0]==x&&ch[ch[x][0]][1]==0)fa[x]=0,ch[y][0]=0,pushup(y);
}
void add(long long x,long long y,long long c)
{
makeroot(x),access(y),splay(y);
v[y]=(v[y]+c*siz[y])%mod,ad[y]=(ad[y]+c)%mod,a[y]=(a[y]+c)%mod;
}
void mul(long long x,long long y,long long c)
{
makeroot(x),access(y),splay(y);
v[y]=v[y]*c%mod,mu[y]=mu[y]*c%mod,ad[y]=ad[y]*c%mod,a[y]=a[y]*c%mod;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)a[i]=1,mu[i]=1,ad[i]=0;
for(int i=1;i<=n-1;i++)
{
cin>>x>>y;
link(x,y);
}
for(int i=1;i<=m;i++)
{
cin>>op;
if(op=='+')cin>>x>>y>>c,add(x,y,c);
else if(op=='-')cin>>x>>y,cut(x,y),cin>>x>>y,link(x,y);
else if(op=='*')cin>>x>>y>>c,mul(x,y,c);
else if(op=='/')cin>>x>>y,makeroot(x),access(y),splay(y),printf("%lld\n",v[y]%mod);
}
return 0;
}
例题 \(3\) :
LCT 做法非常无脑啊。
首先弹力系数是一个正整数,所以如果我们把每一个装置看作一个节点,每一次弹射看作一条边,从 \(i\) 连向 \(k_i\),并将被弹飞时的边连向一个虚拟节点,以这个虚拟节点为根,操作 \(1\) 就是把 \(x\) 打通到根,求整个链的大小就行了。对每个结点维护子树大小,直接查提取区间后 Splay 树根子树大小的信息就行了。
操作 \(2\) 就是删边和连边。先把本来的边删掉,更新弹力系数,再把边加回来。
这里的懒标记定义也有问题,最好按照例题 \(2\) 的写法。
#include <bits/stdc++.h>
using namespace std;
long long n,m,op,x,y,a[300000],siz[300000],ch[300000][2],fa[300000],re[300000],st[300000],top=0;
void pushup(long long x)
{
siz[x]=siz[ch[x][0]]+siz[ch[x][1]]+1;
}
void pushdown(long long x)
{
if(re[x])re[ch[x][0]]^=1,re[ch[x][1]]^=1,swap(ch[x][0],ch[x][1]),re[x]=0;
}
long long wh(long long x)
{
return x==ch[fa[x]][1];
}
bool isroot(long long x)
{
return (x!=ch[fa[x]][0])&&(x!=ch[fa[x]][1]);
}
void rotate(long long x)
{
long long y=fa[x],z=fa[y],k=wh(x),l=wh(y);
if(!isroot(y))ch[z][l]=x;
fa[x]=z,fa[y]=x,fa[ch[x][k^1]]=y;
ch[y][k]=ch[x][k^1],ch[x][k^1]=y;
pushup(y),pushup(x);
}
void splay(long long x)
{
top=0,st[++top]=x;
for(int i=x;!isroot(i);i=fa[i])st[++top]=fa[i];
while(top)pushdown(st[top]),top--;
while(!isroot(x))
{
long long y=fa[x];
if(!isroot(y))
{
if(wh(x)==wh(y))rotate(y);
else rotate(x);
}
rotate(x);
}
}
void access(long long x)
{
for(long long t=0;x;t=x,x=fa[x])splay(x),ch[x][1]=t,pushup(x);
}
void makeroot(long long x)
{
access(x),splay(x),re[x]^=1;
}
long long findroot(long long x)
{
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
void link(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(findroot(y)!=x)fa[x]=y;
}
void cut(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(ch[y][0]==x&&ch[ch[x][0]][1]==0)fa[x]=0,ch[y][0]=0,pushup(y);
}
int main()
{
scanf("%lld",&n);
for(int i=1;i<=n+1;i++)siz[i]=1;
for(int i=1;i<=n;i++)
{
scanf("%lld",&a[i]);
if(i+a[i]<=n)link(i,i+a[i]);
else link(i,n+1);
}
scanf("%lld",&m);
for(int i=1;i<=m;i++)
{
scanf("%lld",&op);
if(op==1)scanf("%lld",&x),x++,makeroot(n+1),access(x),splay(x),printf("%lld\n",siz[ch[x][0]]);
else if(op==2)
{
scanf("%lld%lld",&x,&y);
x++;
if(x+a[x]<=n)cut(x,x+a[x]);
else cut(x,n+1);
a[x]=y;
if(x+a[x]<=n)link(x,x+a[x]);
else link(x,n+1);
}
}
return 0;
}
例题 \(4\) :
比较复杂的 pushup。
对于每个节点,我们维护其 Splay 子树内对应链的区间的颜色数,最浅的点颜色数,最深的点颜色数。pushup 的时候,由于左儿子是深度较浅的点,所以如果左儿子深度最深的点的颜色与当前节点颜色相同,那么说明合并后这种颜色被重复计算了一次,应该减掉。右儿子深度最浅的点的颜色与当前节点颜色相同也是同理。对于和最浅的点颜色数,最深的点颜色数,直接继承左、右儿子的。
覆盖操作也比较简单。使用覆盖懒标记,打标记时更新子树内的颜色数为 \(1\),最浅的点颜色数和最深的点颜色数更新为这个颜色。
这里的懒标记定义也有问题,最好按照例题 \(2\) 的写法。
#include <bits/stdc++.h>
using namespace std;
long long n,m,x,y,z,a[200000],ls[200000],rs[200000],cnt[200000],ch[200000][2],fa[200000],re[200000],tg[200000],st[200000],top=0;
char op;
void pushdown(long long x)
{
if(re[x])
{
re[ch[x][0]]^=1,re[ch[x][1]]^=1;
swap(ls[x],rs[x]),swap(ch[x][0],ch[x][1]),re[x]=0;
}
if(tg[x])
{
tg[ch[x][0]]=tg[x],tg[ch[x][1]]=tg[x];
a[x]=tg[x],ls[x]=tg[x],rs[x]=tg[x],cnt[x]=1,tg[x]=0;
}
}
void pushup(long long x)
{
if(ch[x][0])pushdown(ch[x][0]);
if(ch[x][1])pushdown(ch[x][1]);
ls[x]=a[x],rs[x]=a[x],cnt[x]=1;
if(ch[x][0])
{
ls[x]=ls[ch[x][0]],cnt[x]+=cnt[ch[x][0]];
if(rs[ch[x][0]]==a[x])cnt[x]--;
}
if(ch[x][1])
{
rs[x]=rs[ch[x][1]],cnt[x]+=cnt[ch[x][1]];
if(ls[ch[x][1]]==a[x])cnt[x]--;
}
}
long long wh(long long x)
{
return x==ch[fa[x]][1];
}
bool isroot(long long x)
{
return (x!=ch[fa[x]][0])&&(x!=ch[fa[x]][1]);
}
void rotate(long long x)
{
long long y=fa[x],z=fa[y],k=wh(x),l=wh(y);
if(!isroot(y))ch[z][l]=x;
fa[x]=z,fa[y]=x,fa[ch[x][k^1]]=y;
ch[y][k]=ch[x][k^1],ch[x][k^1]=y;
pushup(y),pushup(x);
}
void splay(long long x)
{
top=0,st[++top]=x;
for(int i=x;!isroot(i);i=fa[i])st[++top]=fa[i];
while(top)pushdown(st[top]),top--;
if(ch[x][0])pushdown(ch[x][0]);
if(ch[x][1])pushdown(ch[x][1]);
while(!isroot(x))
{
long long y=fa[x];
if(!isroot(y))
{
if(wh(x)==wh(y))rotate(y);
else rotate(x);
}
rotate(x);
}
}
void access(long long x)
{
for(long long t=0;x;t=x,x=fa[x])splay(x),ch[x][1]=t,pushup(x);
}
void makeroot(long long x)
{
access(x),splay(x),re[x]^=1;
}
long long findroot(long long x)
{
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
void link(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(findroot(y)!=x)fa[x]=y;
}
void cut(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(ch[y][0]==x&&ch[ch[x][0]][1]==0)fa[x]=0,ch[y][0]=0,pushup(y);
}
int main()
{
ios::sync_with_stdio(false);
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n-1;i++)cin>>x>>y,link(x,y);
for(int i=1;i<=m;i++)
{
cin>>op;
if(op=='C')cin>>x>>y>>z,makeroot(x),access(y),splay(y),tg[y]=z;
else if(op=='Q')cin>>x>>y,makeroot(x),access(y),splay(y),printf("%lld\n",cnt[y]);
}
return 0;
}
例题 \(5\) :
本题是 LCT 维护动态最小生成树和边权 LCT 的经典应用。
首先,我们把边按照需要 A 型守护精灵的数量从小到大排序,然后从小到大遍历这些边依次加入图中。这样,我们就不需要考虑 A 型守护精灵的限制。考虑反证,假设存在最优路径在当前 A 型守护精灵的限制下,如果我们为了让 B 型守护精灵最大值最小导致 A 型守护精灵的最大值并非新加入的边,那这条路径会在之前的 A 型守护精灵的限制下被计算,所以不会有问题。而如果 A 型守护精灵的最大值是新加入的边,那相当于我们枚举 A 型守护精灵的最大值计算,覆盖了所有方案,也不会有问题。
根据最小生成树 Kruskal 的过程,最小生成树上的边一定是使某两个点连通的最小边。因此,我们在加边的时候维护最小生成树即可。假设加入的边连接了 \(u,v\),如果 \(u,v\) 不连通,直接连边。否则,我们用 LCT 查询出 \(u,v\) 之间路径上 B 型守护精灵的最大值,如果新加入的路径权值更小,那就删掉这条边,换成新的边。
然后就是如何维护边权 LCT 了。我们把每条边拆成一个点,分别连接它连接的两个端点。在 pushup 时如果是边拆成的点就加入边的信息并合并信息,否则值合并左右儿子的信息。
细节有点多。为了方便删边,我们需要维护 B 型守护精灵的最大值对应的边是哪条。同时,\(1\to n\) 的路径上不一定会经过 A 型守护精灵数量最大的那条边,所以我们还需要维护一下路径 A 型守护精灵的最大值,最后加起来。有点难写。
这里的懒标记定义还是有问题,最好按照例题 \(2\) 的写法。
#include <bits/stdc++.h>
using namespace std;
struct edge
{
long long u,v,d1,d2;
}e[200000];
long long n,m,op,x,y,m1[200000],m2[200000],p[200000],ch[200000][2],fa[200000],re[200000],st[200000],top=0,ans=1e12;
bool cmp(struct edge a,struct edge b)
{
return a.d1<b.d1;
}
void pushup(long long x)
{
if(x>n)m1[x]=e[x-n].d1,p[x]=x,m2[x]=e[x-n].d2;
else m1[x]=m2[x]=0,p[x]=0;
m1[x]=max(m1[x],max(m1[ch[x][0]],m1[ch[x][1]]));
if(m2[ch[x][0]]>m2[x])m2[x]=m2[ch[x][0]],p[x]=p[ch[x][0]];
if(m2[ch[x][1]]>m2[x])m2[x]=m2[ch[x][1]],p[x]=p[ch[x][1]];
}
void pushdown(long long x)
{
if(re[x])re[ch[x][0]]^=1,re[ch[x][1]]^=1,swap(ch[x][0],ch[x][1]),re[x]=0;
}
long long wh(long long x)
{
return x==ch[fa[x]][1];
}
bool isroot(long long x)
{
return (x!=ch[fa[x]][0])&&(x!=ch[fa[x]][1]);
}
void rotate(long long x)
{
long long y=fa[x],z=fa[y],k=wh(x),l=wh(y);
if(!isroot(y))ch[z][l]=x;
fa[x]=z,fa[y]=x,fa[ch[x][k^1]]=y;
ch[y][k]=ch[x][k^1],ch[x][k^1]=y;
pushup(y),pushup(x);
}
void splay(long long x)
{
top=0,st[++top]=x;
for(int i=x;!isroot(i);i=fa[i])st[++top]=fa[i];
while(top)pushdown(st[top]),top--;
while(!isroot(x))
{
long long y=fa[x];
if(!isroot(y))
{
if(wh(x)==wh(y))rotate(y);
else rotate(x);
}
rotate(x);
}
}
void access(long long x)
{
for(long long t=0;x;t=x,x=fa[x])splay(x),ch[x][1]=t,pushup(x);
}
void makeroot(long long x)
{
access(x),splay(x),re[x]^=1;
}
long long findroot(long long x)
{
access(x),splay(x);
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
long long getroot(long long x)
{
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
void link(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(getroot(y)!=x)fa[x]=y;
}
void cut(long long x,long long y)
{
makeroot(x),access(y),splay(y);
if(ch[y][0]==x&&ch[ch[x][0]][1]==0)fa[x]=0,ch[y][0]=0,pushup(y);
}
int main()
{
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++)scanf("%lld%lld%lld%lld",&e[i].u,&e[i].v,&e[i].d1,&e[i].d2);
sort(e+1,e+m+1,cmp);
for(int i=1;i<=m;i++)m1[n+i]=e[i].d1,m2[n+i]=e[i].d2,p[n+i]=n+i;
for(int i=1;i<=m;i++)
{
if(findroot(e[i].u)!=findroot(e[i].v))link(e[i].u,n+i),link(n+i,e[i].v);
else
{
makeroot(e[i].u),access(e[i].v),splay(e[i].v);
if(e[i].d2<m2[e[i].v])
{
long long k=p[e[i].v]-n;
cut(e[k].u,n+k),cut(n+k,e[k].v);
link(e[i].u,n+i),link(n+i,e[i].v);
}
}
makeroot(1),access(n),splay(n);
if(findroot(1)==findroot(n))ans=min(ans,m1[n]+m2[n]);
}
if(ans!=1e12)printf("%lld\n",ans);
else printf("-1\n");
return 0;
}
例题 \(6\) :
做题时我们要注意把题目内容和熟知的算法进行类比迁移。
我们观察这个 \(1\) 操作,把 \(x\) 到根染上一种没有用过的颜色。打通到根,彼此覆盖,这就很像 LCT access 操作。
因此,我们考虑用 LCT 维护原树。然后,我们就会发现每个点到根的颜色数就是从这个点到根经过的虚边数量加 \(1\)。access 操作时,虚实边修改的时候同时改一下子树内的点到根经过的虚边数量。初始值为在树上的深度。
由于每次虚边变成实边或实边变成虚边都对这条边上深度较深的节点的子树内的每个节点的到根的颜色数有影响,且原树固定,所以考虑把原树的 DFS 序求出来,通过 DFS 序转化为序列问题。然后考虑影响,虚边变成实边根据转化相当于区间减 \(1\),实边变成虚边相当于区间加 \(1\)。
注意这里是这条边上深度较深的节点的子树内,也就是下面的链中最浅的节点的子树内,所以 access 的时候需要对 \(t\) 和 \(ch[x][1]\) 进行 findroot 操作。
接下来考虑 \(2\) 操作。注意到路径上的颜色数可以差分,即设 \(d[x]\) 为 \(x\) 到根的颜色数,则 \(x\to y\) 的颜色数为 \(d[x]+d[y]-2\times d[\text{lca}(x,y)]+1\)。
因为 \(x\to\text{lca}(x,y)\) 和 \(\text{lca}(x,y)\to\text{y}\) 的颜色不可能相同,且 \(\text{lca}(x,y)\) 到根的路径上的颜色被计算了两遍,需要减去。但是 \(\text{lca}(x,y)\) 的颜色又是需要计算的,所以再加回来。
上面论证显然漏了一种情况,可能 \(\text{lca}(x,y)\) 到根和 \(\text{lca}(x,y)\) 到 \(x\) 或 \(y\) 的路径上有重复颜色,此时这两个色块一定相连。而这个色块先被算两次,再被减两次,最后再被 \(\text{lca}(x,y)\) 的颜色加回来,刚好算了一次,所以是对的。
\(3\) 操作就简单了,相当于区间 \(\text{max}\)。因此我们需要维护一个区间加区间 \(\text{max}\) 的数据结构,线段树即可。
#include <bits/stdc++.h>
#define lc(x) ((x)<<1)
#define rc(x) ((x)<<1)|1
using namespace std;
struct edge
{
int v,nxt;
}e[200000];
struct node
{
int mx,tg;
}tr[400000];
int n,m,a,b,op,x,y,h[200000],dep[200000],dfn[200000],d[200000],siz[200000],ch[200000][2],fa[200000],f[200000][20],rt=1,dfc=0,cnt=0;
void pushup(int x)
{
tr[x].mx=max(tr[lc(x)].mx,tr[rc(x)].mx);
}
void pushdown(int x)
{
tr[lc(x)].tg+=tr[x].tg,tr[lc(x)].mx+=tr[x].tg;
tr[rc(x)].tg+=tr[x].tg,tr[rc(x)].mx+=tr[x].tg;
tr[x].tg=0;
}
void build(int x,int l,int r)
{
tr[x].tg=0;
if(l==r)
{
tr[x].mx=dep[d[l]];
return;
}
int mid=(l+r)>>1;
build(lc(x),l,mid),build(rc(x),mid+1,r);
pushup(x);
}
void update(int x,int l,int r,int lx,int rx,int k)
{
if(l>=lx&&r<=rx)
{
tr[x].tg+=k,tr[x].mx+=k;
return;
}
pushdown(x);
int mid=(l+r)>>1;
if(lx<=mid)update(lc(x),l,mid,lx,rx,k);
if(rx>=mid+1)update(rc(x),mid+1,r,lx,rx,k);
pushup(x);
}
int query(int x,int l,int r,int lx,int rx)
{
if(l>=lx&&r<=rx)return tr[x].mx;
pushdown(x);
int mid=(l+r)>>1,ans=-1e9;
if(lx<=mid)ans=max(ans,query(lc(x),l,mid,lx,rx));
if(rx>=mid+1)ans=max(ans,query(rc(x),mid+1,r,lx,rx));
return ans;
}
void add_edge(int u,int v)
{
e[++cnt].nxt=h[u];
e[cnt].v=v;
h[u]=cnt;
}
void dfs(int x,int pr)
{
dfn[x]=++dfc,d[dfn[x]]=x,siz[x]=1,dep[x]=dep[pr]+1,fa[x]=pr,f[x][0]=pr;
for(int i=1;i<=19;i++)
if(f[x][i-1])f[x][i]=f[f[x][i-1]][i-1];
else break;
for(int i=h[x];i;i=e[i].nxt)
if(e[i].v!=pr)dfs(e[i].v,x),siz[x]+=siz[e[i].v];
}
int lca(int x,int y)
{
if(dep[x]>dep[y])swap(x,y);
int c=dep[y]-dep[x];
for(int i=19;i>=0;i--)
if(c&(1<<i))y=f[y][i];
if(x==y)return x;
for(int i=19;i>=0;i--)
if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i];
return f[x][0];
}
int wh(int x)
{
return ch[fa[x]][1]==x;
}
bool isroot(int x)
{
return (ch[fa[x]][0]!=x)&&(ch[fa[x]][1]!=x);
}
void rotate(int x)
{
int y=fa[x],z=fa[y],k=wh(x);
if(!isroot(y))ch[z][wh(y)]=x;
fa[x]=z;
fa[ch[x][k^1]]=y,ch[y][k]=ch[x][k^1];
fa[y]=x,ch[x][k^1]=y;
}
void splay(int x)
{
while(!isroot(x))
{
int y=fa[x];
if(!isroot(y))
{
if(wh(x)==wh(y))rotate(y);
else rotate(x);
}
rotate(x);
}
}
int findroot(int x)
{
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
void access(int x)
{
for(int t=0;x;t=x,x=fa[x])
{
splay(x);
if(t)
{
int k=findroot(t);
update(rt,1,n,dfn[k],dfn[k]+siz[k]-1,-1);
splay(t);
}
if(ch[x][1])
{
int k=findroot(ch[x][1]);
update(rt,1,n,dfn[k],dfn[k]+siz[k]-1,1);
splay(x);
}
ch[x][1]=t;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n-1;i++)scanf("%d%d",&a,&b),add_edge(a,b),add_edge(b,a);
dfs(1,0),build(rt,1,n);
for(int i=1;i<=m;i++)
{
scanf("%d",&op);
if(op==1)scanf("%d",&x),access(x);
else if(op==2)scanf("%d%d",&x,&y),printf("%d\n",query(rt,1,n,dfn[x],dfn[x])+query(rt,1,n,dfn[y],dfn[y])-2*query(rt,1,n,dfn[lca(x,y)],dfn[lca(x,y)])+1);
else if(op==3)scanf("%d",&x),printf("%d\n",query(rt,1,n,dfn[x],dfn[x]+siz[x]-1));
}
return 0;
}
后记
真的很喜欢 LCT,学过 LCT 之后基本上就没写过树剖了。但 LCT 的常数确实巨大,\(O(\log n)\) 能跑得比 \(O(\log^2 n)\) 的树剖慢一到两倍。
还有一些很帅的 LCT 用法没有记录,因为我不会。
清浊分 花有灵 双生红尘
星辰变 弦音绝 当舍痴嗔
既不追朝与露 也不悲他年吻
九星书尽 这过往爱恨
【10】LCT学习笔记的更多相关文章
- LCT 学习笔记
LCT学习笔记 前言 自己定的学习计划看起来完不成了(两天没学东西,全在补题),决定赶快学点东西 于是就学LCT了 简介 Link/Cut Tree是一种数据结构,我们用它解决动态树问题 但是LCT不 ...
- 《Java核心技术·卷Ⅰ:基础知识(原版10》学习笔记 第5章 继承
<Java核心技术·卷Ⅰ:基础知识(原版10>学习笔记 第5章 继承 目录 <Java核心技术·卷Ⅰ:基础知识(原版10>学习笔记 第5章 继承 5.1 类.超类和子类 5.1 ...
- 201521123003《Java程序设计》第10周学习笔记
1. 本周学习总结 1.1 以你喜欢的方式(思维导图或其他)归纳总结异常与多线程相关内容. 2. 书面作业 本次PTA作业题集异常.多线程 1.finally 题目4-2 1.1 截图你的提交结果(出 ...
- SPLAY,LCT学习笔记(六)
这应该暂时是个终结篇了... 最后在这里讨论LCT的一个常用操作:维护虚子树信息 这也是一个常用操作 下面我们看一下如何来维护 以下内容转自https://blog.csdn.net/neither_ ...
- SPLAY,LCT学习笔记(五)
这一篇重点探讨LCT的应用 例:bzoj 2631 tree2(国家集训队) LCT模板操作之一,利用SPLAY可以进行区间操作这一性质对维护懒惰标记,注意标记下传顺序和如何下传 #include & ...
- SPLAY,LCT学习笔记(四)
前三篇好像变成了SPLAY专题... 这一篇正式开始LCT! 其实LCT就是基于SPLAY的伸展操作维护树(森林)连通性的一个数据结构 核心操作有很多,我们以一道题为例: 例:bzoj 2049 洞穴 ...
- LCT学习笔记
最近自学了一下LCT(Link-Cut-Tree),参考了Saramanda及Yang_Zhe等众多大神的论文博客,对LCT有了一个初步的认识,LCT是一种动态树,可以处理动态问题的算法.对于树分治中 ...
- MySQL必知必会 前10章学习笔记
1. 在使用用户名和密码登陆MySQL数据库之后,首先需要指定你将要操作的数据库 USE $数据库名称 2. 使用SHOW 命令可以查看数据库和表中的信息 SHOW DATABASES; #列出可用数 ...
- [总结] LCT学习笔记
\(emmm\)学\(lct\)有几天了,大概整理一下这东西的题单吧 (部分参考flashhu的博客) 基础操作 [洛谷P1501Tree II] 题意 给定一棵树,要求支持 链加,删边加边,链乘,询 ...
- SPLAY,LCT学习笔记(二)
能够看到,上一篇的代码中有一段叫做find我没有提到,感觉起来也没有什么用,那么他的存在意义是什么呢? 接下来我们来填一下这个坑 回到我们的主题:NOI 2005维修数列 我们刚刚讨论了区间翻转的操作 ...
随机推荐
- 使用Python解决三体问题
引言 在物理学中,三体问题是一个经典的动态系统问题,它描述了三个天体之间的相互引力作用和运动规律.三体问题最著名的挑战在于它无法通过简单的解析公式来解决,换句话说,三体问题是一个不可解析的问题.尽管如 ...
- python之“if __name__=="__main__"”的代表的意思和用法
创建下方脚本A def print_sum(a): print(a) print_sum(20) if __name__=="__main__": print("test ...
- 为什么 G1 垃圾收集器不维护年轻代到老年代的记忆集?
为什么 G1 垃圾收集器不维护年轻代到老年代的记忆集? 在 G1 垃圾收集器中,不维护年轻代到老年代的记忆集(Remembered Set, RSet)是因为其设计特点和优化策略使得这种记忆集的维护既 ...
- 备份一个迭代查找TreeViewItem的辅助函数
private TreeViewItem FindTreeItem(TreeViewItem item, Func<TreeViewItem, bool> compare) { if (i ...
- 解决多个if-else的方案
参考链接: 遇到大量if记住下面的口诀: 互斥条件表驱动 嵌套条件校验链 短路条件早return 零散条件可组合 解释: 互斥条件,表示几个条件之间是冲突的,不可能同时达成的.比如说一个数字,它不可能 ...
- Vue模板语法——文本插值、指令、缩写
Vue模板语法--文本插值.指令.缩写 插值 文本({{}}.v-text) 数据绑定最常见的形式就是使用"Mustache"语法 (双大括号) 的文本,双大括号会将数据解释为普通 ...
- Axure RP Element UI 2和 Element UI Plus元件库
基于ElementUI2.0及ElementUI Plus3.0二次创作的ElementUI 元件库.2个版本的原型图内容会有所不同,ElementUI Plus3.0的交互更加丰富和高级.你可以同时 ...
- Axure RP大数据可视化大屏原型组件源文件
Axure RP大数据可视化大屏原型模板 大数据BI分析上大屏,在很多大企业和政府单位客户都需要,高新区市场监控等,那使用Axure RP做交互原型是必不可少的,有了大屏原型模板可做出不同风格和行业的 ...
- 获取接口方式(Bean注入方式总结)
一.在工具类中使用SpringContextHolder获取Bean对象,用来调用各个接口 /** * 获取阿里巴巴属性列表映射 * * @author 王子威 * @param alibabaPro ...
- 深入理解 JavaScript 模板引擎
@charset "UTF-8"; .markdown-body { line-height: 1.75; font-weight: 400; font-size: 15px; o ...