【10】LCT学习笔记
前言
老早就想写了,但是一直抽不出时间。借助集训的契机把这篇学习笔记写出来。
时间跨度比较长,可能有一些代码不是现在的码风,我会标注出来的。
长文警告:本文一共
前置知识:【8】平衡树学习笔记 中的 Splay 部分。
UPD on
LCT
LCT 是一种动态树,能在
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 的旋转本质相同,但需要特判如果
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 的根。注意特判 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);
}
}
打通到根
把一个点
并且这个
记得 pushup。
void access(int x)
{
for(int t=0;x;t=x,x=fa[x])splay(x),ch[x][1]=t,pushup(x);
}
换根
把
因为把 access 操作打通到根是
void makeroot(int x)
{
access(x),splay(x),re[x]^=1;
}
由于时间久远,这个翻转标记并不是一般翻转标记的打法,正常的翻转标记应该是例题
提取路径
先把
int query(int x)
{
makeroot(x),access(y),splay(y);
return v[y];
}
找根
找节点
int findroot(int x)
{
splay(x);
while(ch[x][0])x=ch[x][0];
splay(x);
return x;
}
连边
连接节点
此时如果 findroot 就能判断。
void link(int x,int y)
{
makeroot(x),access(y),splay(y);
if(findroot(y)!=x)fa[x]=y;
}
删边
删除节点 pushup。
特别的,如果
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);
}
LCT 的各种应用参见例题。
例题
例题
P3690 【模板】动态树(LCT)
LCT 模板题,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;
}
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;
}
例题
P1501 [国家集训队] Tree II
老生常谈的标记下传顺序问题。
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;
}
例题
P3203 [HNOI2010] 弹飞绵羊
LCT 做法非常无脑啊。
首先弹力系数是一个正整数,所以如果我们把每一个装置看作一个节点,每一次弹射看作一条边,从
操作
这里的懒标记定义也有问题,最好按照例题
#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;
}
例题
P2486 [SDOI2011] 染色
比较复杂的 pushup。
对于每个节点,我们维护其 Splay 子树内对应链的区间的颜色数,最浅的点颜色数,最深的点颜色数。pushup 的时候,由于左儿子是深度较浅的点,所以如果左儿子深度最深的点的颜色与当前节点颜色相同,那么说明合并后这种颜色被重复计算了一次,应该减掉。右儿子深度最浅的点的颜色与当前节点颜色相同也是同理。对于和最浅的点颜色数,最深的点颜色数,直接继承左、右儿子的。
覆盖操作也比较简单。使用覆盖懒标记,打标记时更新子树内的颜色数为
这里的懒标记定义也有问题,最好按照例题
#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;
}
例题
P2387 [NOI2014] 魔法森林
本题是 LCT 维护动态最小生成树和边权 LCT 的经典应用。
首先,我们把边按照需要 A 型守护精灵的数量从小到大排序,然后从小到大遍历这些边依次加入图中。这样,我们就不需要考虑 A 型守护精灵的限制。考虑反证,假设存在最优路径在当前 A 型守护精灵的限制下,如果我们为了让 B 型守护精灵最大值最小导致 A 型守护精灵的最大值并非新加入的边,那这条路径会在之前的 A 型守护精灵的限制下被计算,所以不会有问题。而如果 A 型守护精灵的最大值是新加入的边,那相当于我们枚举 A 型守护精灵的最大值计算,覆盖了所有方案,也不会有问题。
根据最小生成树 Kruskal 的过程,最小生成树上的边一定是使某两个点连通的最小边。因此,我们在加边的时候维护最小生成树即可。假设加入的边连接了
然后就是如何维护边权 LCT 了。我们把每条边拆成一个点,分别连接它连接的两个端点。在 pushup 时如果是边拆成的点就加入边的信息并合并信息,否则值合并左右儿子的信息。
细节有点多。为了方便删边,我们需要维护 B 型守护精灵的最大值对应的边是哪条。同时,
这里的懒标记定义还是有问题,最好按照例题
#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;
}
例题
P3703 [SDOI2017] 树点涂色
做题时我们要注意把题目内容和熟知的算法进行类比迁移。
我们观察这个 access 操作。
因此,我们考虑用 LCT 维护原树。然后,我们就会发现每个点到根的颜色数就是从这个点到根经过的虚边数量加 access 操作时,虚实边修改的时候同时改一下子树内的点到根经过的虚边数量。初始值为在树上的深度。
由于每次虚边变成实边或实边变成虚边都对这条边上深度较深的节点的子树内的每个节点的到根的颜色数有影响,且原树固定,所以考虑把原树的 DFS 序求出来,通过 DFS 序转化为序列问题。然后考虑影响,虚边变成实边根据转化相当于区间减
注意这里是这条边上深度较深的节点的子树内,也就是下面的链中最浅的节点的子树内,所以 access 的时候需要对 findroot 操作。
接下来考虑
因为
上面论证显然漏了一种情况,可能