LCT
动态树
树上查询问题是指,给定一个图论中的树结构,需要对树上的子树或者链进行一系列增删改查的问题。
和序列问题中一般常说的“动态”和“静态”不同。
动态树问题一般指树结构发生改变。
*注意:一般对于纯换根(change root)操作,不视为是动态树问题。
LCT结构
前置知识1:链分解
对于一颗树,可以用若干个互不交叉的连续序列分解一棵树。
序列的下标按照树深度递增的顺序。
然后对于每一条链中的节点,我们用“缩点”的思想可以把它们看成一个节点,这样就做出了一颗“结构树”。
首先你要去画这么一颗“结构树”,因为这就是宏观上LCT的样子
把一条树链当成一个点,把轻边当成树上的边,我们可以的得到一颗结构树。这个地方和树链剖分相同,事实上link-cut tree总是能在log级的时间内把需要查询的树上的简单路径构造到同一条链中。
(在刚刚的例子中,对于当前的一个树链,类比树链剖分你也可以继续叫他“重边、重链、重儿子”,其实有的地方管他们叫“偏好边/路径/儿子”,其实这里就是一个翻译的习惯。)
前置知识2:splay维护序列
首先复习一下如何使用splay维护序列。
splay是一个排序二叉树结构,它是一个Key-Val结构。Key是数组下标,Val是维护的信息。
中序遍历,从左至右是它的key,表示数组下标。
结构
LCT的底层原理叫做,用splay维护动态的链分解。
然后我们可以用splay维护单链,这些链很方便的可以做掉拼接和拆分。
(也就是merge和splite,在lct中我们习惯叫他们link cut)
LCT相对于其他数据结构属于大型数据结构。
原因在于它其实是有层次的,在学习时建议把LCT理解成“结构树套平衡树”以这种方式去理解。
如何去切入呢,就是说分成宏观和微观。
宏观:只关注结构树树链分解的变化。
微观:只关注某一天树链内部splay维护信息的变化。
以上部分就是对于LCT结构上的学习。
这里提出一个小问题:
给定一颗LCT,如何查找当前LCT维护树结构的根节点。
宏观:根节点一定位于LCT外层结构树的根部。
微观:因为LCT中每个节点都是一颗splay,key是相对深度,根节点是相对深度最小的节点,所以是中序遍历的第一个点。
LCT基本操作
\(access(x)(不换根,将根节点到x节点构造到同一条链中)\)
\(change/make/set root(x)(将节点x设置成整棵树的根)\)
\(query path(x,y)(将x,y构造到同一条链中,且链的收尾为x,y)\)
\(link(x,y)(将x,y连接到LCT中)\)
\(cut(将x,y从LCT中断开)\)
\(lca(x,y,z)(查询以x为根,y和z的lca)\)
\(findroot(x)(查询x节点所在树结构的树根是谁,类比并查集)\)
例题
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=1e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int w[maxn];
struct LCT{
int ch[maxn][2],fa[maxn],rev[maxn];
int sum[maxn]; //按题目要求的信息
/*
*/
int get(int x){return ch[fa[x]][1]==x;}
int isr(int x){return ch[fa[x]][get(x)]!=x;}
//同一条重链上的所有点才会被放在一棵𝑠𝑝𝑙𝑎𝑦中
//isr(x)判断x是否是所在splay(重链)上的根(链中深度最浅的)
void update(int x){
//更新信息
/*
*/
sum[x]=sum[ch[x][1]]^sum[ch[x][0]]^w[x];
return ;
}
void pushdown(int x){
if(rev[x]){
rev[x]=0;swap(ch[x][0],ch[x][1]);
rev[ch[x][0]]^=1;rev[ch[x][1]]^=1;
}
/*
其他标记信息
if(){
}
*/
return ;
}
void Rotate(int x){
int y=fa[x],z=fa[y],k=get(x);
if(!isr(y))ch[z][get(y)]=x;fa[x]=z;
ch[y][k]=ch[x][k^1];
if(ch[y][k])fa[ch[y][k]]=y;
ch[x][k^1]=y;fa[y]=x;
update(y); return ;
}
int top,st[maxn];
void splay(int x){
top=0;st[++top]=x;
for(int i=x;!isr(i);i=fa[i])st[++top]=fa[i];
for(int i=top;i;--i)pushdown(st[i]);
//链信息下传
for(;!isr(x);Rotate(x)){
if(!isr(fa[x]))Rotate(get(fa[x])==get(x)?fa[x]:x);
}
update(x);
return;
}
int access(int x){
int pre=0;
for(;x;pre=x,x=fa[x]){
splay(x);ch[x][1]=pre;update(x);
}
return pre;
}
int lca(int root,int x,int y){
//以root为根的lca(x,y)
makeroot(root);
access(x);
return access(y);
}
int findroot(int x){
access(x);splay(x);pushdown(x);
while(ch[x][0]){
pushdown(x);
x=ch[x][0];
}
splay(x);
return x;
}
void makeroot(int x){
access(x);splay(x);
rev[x]^=1;
return ;
}
void link(int x,int y){
makeroot(x);
if(findroot(y)!=x)fa[x]=y;
//如果x和y没有联通就连接
return;
}
void cut(int x,int y){
makeroot(x);access(y);splay(y);
if(findroot(y)==x && fa[y]==x && !ch[y][0]){
//findroot(y)后splay(x)
ch[x][1]=fa[y]=0;
update(x);
}
return ;
}
int query_path(int x,int y){
makeroot(x);
access(y);splay(y);
/*
题目要求内容
*/
return sum[y];
}
}L;
int n,m;
int main(){
n=read();m=read();
for(int i=1;i<=n;i++)w[i]=L.sum[i]=read();
for(int i=1;i<=m;i++){
int opt=read(),x=read(),y=read();
if(opt==0){
printf("%d\n",L.query_path(x,y));
}
else if(opt==1)L.link(x,y);
else if(opt==2)L.cut(x,y);
else L.splay(x),w[x]=y,L.update(x);
}
return 0;
}
2. [HNOI2010]BOUNCE 弹飞绵羊
首先按照初始弹力系数来进行连边,并将弹出的点连向点 ,修改的时候先cut再 link,每个点再记录下size即可
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=1e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int w[maxn];
struct LCT{
int ch[maxn][2],fa[maxn],rev[maxn];
int sum[maxn]; //按题目要求的信息
/*
*/
int get(int x){return ch[fa[x]][1]==x;}
int isr(int x){return ch[fa[x]][get(x)]!=x;}
//同一条重链上的所有点才会被放在一棵𝑠𝑝𝑙𝑎𝑦中
//isr(x)判断x是否是所在splay(重链)上的根(链中深度最浅的)
void update(int x){
//更新信息
/*
*/
sum[x]=sum[ch[x][1]]+sum[ch[x][0]]+1;
return ;
}
void pushdown(int x){
if(rev[x]){
rev[x]=0;swap(ch[x][0],ch[x][1]);
rev[ch[x][0]]^=1;rev[ch[x][1]]^=1;
}
/*
其他标记信息
if(){
}
*/
return ;
}
void Rotate(int x){
int y=fa[x],z=fa[y],k=get(x);
if(!isr(y))ch[z][get(y)]=x;fa[x]=z;
ch[y][k]=ch[x][k^1];
if(ch[y][k])fa[ch[y][k]]=y;
ch[x][k^1]=y;fa[y]=x;
update(y); return ;
}
int top,st[maxn];
void splay(int x){
top=0;st[++top]=x;
for(int i=x;!isr(i);i=fa[i])st[++top]=fa[i];
for(int i=top;i;--i)pushdown(st[i]);
//链信息下传
for(;!isr(x);Rotate(x)){
if(!isr(fa[x]))Rotate(get(fa[x])==get(x)?fa[x]:x);
}
update(x);
return;
}
int access(int x){
int pre=0;
for(;x;pre=x,x=fa[x]){
splay(x);ch[x][1]=pre;update(x);
}
return pre;
}
int lca(int root,int x,int y){
//以root为根的lca(x,y)
makeroot(root);
access(x);
return access(y);
}
int findroot(int x){
access(x);splay(x);pushdown(x);
while(ch[x][0]){
pushdown(x);
x=ch[x][0];
}
splay(x);
return x;
}
void makeroot(int x){
access(x);splay(x);
rev[x]^=1;
return ;
}
void link(int x,int y){
makeroot(x);
if(findroot(y)!=x)fa[x]=y;
//如果x和y没有联通就连接
return;
}
void cut(int x,int y){
makeroot(x);access(y);splay(y);
if(findroot(y)==x && fa[y]==x && !ch[y][0]){
//findroot(y)后splay(x)
ch[x][1]=fa[y]=0;
update(x);
}
return ;
}
int query_path(int x,int y){
makeroot(x);
access(y);splay(y);
return sum[y];
}
}L;
int n,m;
int main(){
n=read();
for(int i=1;i<=n;i++){
w[i]=read();
L.link(i,min(n+1,i+w[i]));
}
m=read();
while(m--){
int i=read(),j=read()+1;
if(i==1){
printf("%d\n",L.query_path(n+1,j)-1);
}
else {
int k=read();
L.cut(j,min(n+1,j+w[j]));
w[j]=k;
L.link(j,min(n+1,j+w[j]));
}
}
return 0;
}
3.魔法森林
这道题的题意就是按照给定的边建一颗树,树上每条边的权值x,y,一条路径的价值为 这条路径上最小的x+最小的y的和,要求从1到n的路径的权值最小
显然最小生成树 的从1到n路径的权值最小。
普通的最小生成树的边只有一个权值,而现在有两个,那我们可以先对x进行从大到小排序,这样就只剩y了。
每次加边,如果两点不联通,则link,否则判断这两点路径的最大值和这条边的y值比较,如果y值小则cut最大值的边,link当前边
因为是边有权值,所以要将边化为点
点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=1e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double eps=1e-12;
ll read(){
ll x=0,f=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
return x*f;
}
int w[maxn];
struct LCT{
int ch[maxn][2],fa[maxn],rev[maxn];
int sum[maxn]; //最大值
int pos[maxn]; //最大值的边的编号
/*
*/
int get(int x){return ch[fa[x]][1]==x;}
int isr(int x){return ch[fa[x]][get(x)]!=x;}
//同一条重链上的所有点才会被放在一棵𝑠𝑝𝑙𝑎𝑦中
//isr(x)判断x是否是所在splay(重链)上的根(链中深度最浅的)
void update(int x){
//更新信息
/*
*/
pos[x]=x;sum[x]=w[x];
if(sum[ch[x][1]]>sum[x])sum[x]=sum[ch[x][1]],pos[x]=pos[ch[x][1]];
if(sum[ch[x][0]]>sum[x])sum[x]=sum[ch[x][0]],pos[x]=pos[ch[x][0]];
return ;
}
void pushdown(int x){
if(rev[x]){
rev[x]=0;swap(ch[x][0],ch[x][1]);
rev[ch[x][0]]^=1;rev[ch[x][1]]^=1;
}
/*
其他标记信息
if(){
}
*/
return ;
}
void Rotate(int x){
int y=fa[x],z=fa[y],k=get(x);
if(!isr(y))ch[z][get(y)]=x;fa[x]=z;
ch[y][k]=ch[x][k^1];
if(ch[y][k])fa[ch[y][k]]=y;
ch[x][k^1]=y;fa[y]=x;
update(y); return ;
}
int top,st[maxn];
void splay(int x){
top=0;st[++top]=x;
for(int i=x;!isr(i);i=fa[i])st[++top]=fa[i];
for(int i=top;i;--i)pushdown(st[i]);
//链信息下传
for(;!isr(x);Rotate(x)){
if(!isr(fa[x]))Rotate(get(fa[x])==get(x)?fa[x]:x);
}
update(x);
return;
}
int access(int x){
int pre=0;
for(;x;pre=x,x=fa[x]){
splay(x);ch[x][1]=pre;update(x);
}
return pre;
}
int lca(int root,int x,int y){
//以root为根的lca(x,y)
makeroot(root);
access(x);
return access(y);
}
int findroot(int x){
access(x);splay(x);pushdown(x);
while(ch[x][0]){
pushdown(x);
x=ch[x][0];
}
splay(x);
return x;
}
void makeroot(int x){
access(x);splay(x);
rev[x]^=1;
return ;
}
void link(int x,int y){
makeroot(x);
if(findroot(y)!=x)fa[x]=y;
//如果x和y没有联通就连接
return;
}
void cut(int x,int y){
makeroot(x);access(y);splay(y);
if(findroot(y)==x && fa[y]==x && !ch[y][0]){
//findroot(y)后splay(x)
ch[x][1]=fa[y]=0;
update(x);
}
return ;
}
int query_path(int x,int y){
makeroot(x);
access(y);splay(y);
return pos[y];
}
}L;
int n,m;
struct wzq{
int x,y,a,b;
}c[maxn];
int main(){
n=read();m=read();
for(int i=1;i<=m;i++){
c[i].x=read();c[i].y=read();
c[i].a=read();c[i].b=read();
}
sort(c+1,c+m+1,[](wzq i,wzq j){return i.a<j.a;});
int ans=inf;
for(int i=1;i<=m;i++){
int x=c[i].x,y=c[i].y;
w[i+n]=c[i].b; //边化点
if(L.findroot(x)!=L.findroot(y)){
L.link(x,i+n);L.link(i+n,y);
}
else {
int now=L.query_path(x,y);
if(w[now]>c[i].b){
L.cut(c[now-n].x,now);L.cut(now,c[now-n].y);
L.link(x,i+n);L.link(i+n,y);
}
}
if(L.findroot(1)==L.findroot(n)){
ans=min(ans,c[i].a+w[L.query_path(1,n)]);
}
}
printf("%d\n",(ans==inf)?-1:ans);
return 0;
}