题目描述
Description
wy 和 wjk 是好朋友。
今天他们在一起聊天,突然聊到了以前一起唱过的《到不了》。
“说到到不了,我给你讲一个故事吧。”
“嗯?”
“从前,神和凡人相爱了,愤怒的神王把他们关进了一个迷宫里,迷宫是由许多棵有根树组 成的。神王每次把两个人扔进其中的某一棵有根树里面,两个相邻节点的距离为 1,两人的 每一步都只能从儿子走到父亲,不能从父亲走到儿子,他们约定,走到同一个节点相见,由 于在迷宫里面行走十分消耗体力,他们决定找出那个使得他们走的总路程最少的节点,他们 当然会算出那个节点了,可是神王有时候会把两棵有根树合并为一棵,这下就麻烦了。。。”
“唔。。。”
[已经了解树,森林的相关概念的同学请跳过下面一段]
树:由 n 个点,n-1 条边组成的无向连通图。
父亲/儿子:把树的边距离定义为 1,root 是树的根,对于一棵树里面相邻的两个点 u,v,到 root 的距离近的那个点是父亲,到 root 距离远的那个点是儿子
森林:由若干棵树组成的图
[简化版题目描述]
维护一个森林,支持连边操作和查询两点 LCA 操作Input
第一行一个整数 N,M,代表森林里的节点总数和有根树的数目。
第二行 M 个整数,第 i 个整数 ri 代表第 i 棵有根树的根是编号为 ri 的节点
接下来 N-M 行,每行两个整数 u,v 表示 u 和 v 相邻
接下来一行一个整数 Q,表示 Q 个事件发生了
接下来 Q 行,每行若干个整数,表示一个事件
如果第一个数 op=1,接下来两个整数 u,v,代表神王把 u 号节点所在的树和 v 号节点所在的树 合并到一起(即 u 到 v 连了一条边),新的根为原来 u 号节点所在的树的根(如果 u,v 已经联通, 忽略这个事件)。
如果第一个数 op=2,接下来两个整数 u,v,代表一次询问,当一个人在 u 号节点,一个人 在 v 号节点,询问他们找到的那个节点的编号Output
对于每一个询问(op=2 的操作),输出一行一个整数,代表节点编号,如果 u,v 不联通,输 出 orzorz。
Sample Input
【样例 1】
2 2
1 2
2
1 1 2
2 1 2
【样例 2】
2 2
1 2
2
1 2 1
2 1 2Sample Output
【样例 1】
1
【样例 2】
2Data Constraint
对于 30%的数据 1 ≤ N ≤ 1000 1 ≤ Q ≤ 1000
对于 100%的数据 1 ≤ N ≤ 100000 1 ≤ Q ≤ 100000
题目大意
给你一个森林,接下来有好多个操作,分别是连边和询问两点的LCA。
其中,如果是将点和点连起来,是将的根节点作为连完边后的根。
思考历程
如果这题没有连边操作,那就十分happy了。
想过是否可以用链剖,发现好像不能。
但是,既然有边的修改,那么——
当然是用LCT了!
思考了半天,我想到一个结论:
对于两个点和,它们到的路径之交中,深度最大的,就是它们的。
所以,我一开始的想法是这样的:
access(u)
将u到root的路径上的所有点打个标记
access(v)
找到v所在splay中带标记的最深的点即LCA
打着打着,突然发现很麻烦。
想一下,access
其实就是一个从当前点的splay上沿着虚边往上跳,将虚边改为实边的过程。
先access(u)
,如果再access(v)
,就可以发现:
过程中修改的最后一个虚边的父亲,必定就是它们的LCA。
原因很显然,因为跳到LCA后,再跳就没了。
我在打access
时,总喜欢设一个返回值,这个返回值是最后修改的点(也就是最后修改虚边的父亲)。因为这个点同时也是在过程后,根所在的splay中的根节点。(如果不记录,翻转的时候还需要splay
一遍,多浪费啊!)
其实可以发现,就算是和同在一条链上,结果也是正确的。
所以,想要切掉这题,只是一个LCT模板……
还有,对于连通性,直接并查集就好。
时间
题解&正解
题解的做法有两种:
离线做法
有一个神奇的性质:
在一棵以任意点为根的树中,和对于为根的LCA为、、中最深的那个。同时,这三个数值中至少有两个是重复的,那个不重复的(或者三个都一样时的随意一个)就是LCA,可以写作。
具体怎么证明,请自行分类讨论,很好证的。
那么我们可以处理出森林最后的样子,随便以一个点为根。
在处理操作的过程中,用并查集记录每个点当时所处的树中的根是谁。
直接利用上面的性质就好,求LCA用倍增或链剖。
时间
在线做法
和上面的做法本质差不多,要用倍增的方法。
在合并两棵树的时候,简单粗暴地将那棵较小的树安在那棵较大的树下面,暴力更新它的倍增数组。
暴力重建的个数不超过个,所以时间是。
代码
LCT做法
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define MAXN 100000
int n,m;
int root[MAXN+1];
struct Node{//LCT的节点
Node *fa,*c[2];
bool res,is_root;
inline bool getson() {return fa->c[0]!=this;}
inline void reserve(){
swap(c[0],c[1]);
res^=1;
}
inline void pushdown(){
if (res){
c[0]->reserve();
c[1]->reserve();
res=0;
}
}
inline void push(){
if (!is_root)
fa->push();
pushdown();
}
inline void rotate(){
Node *y=fa,*z=y->fa;
bool k=getson();
fa=z;
if (y->is_root){
y->is_root=0;
is_root=1;
}
else
z->c[y->getson()]=this;
y->c[k]=c[!k];
c[!k]->fa=y;
c[!k]=y;
y->fa=this;
}
inline void splay(){
push();
while (!is_root){
if (!fa->is_root){
if (getson()!=fa->getson())
rotate();
else
fa->rotate();
}
rotate();
}
}
inline Node *access();
inline Node *mroot();
inline void link(Node *v);
} d[MAXN+1];
Node *null=d;
inline Node *Node::access(){
Node *x=this,*y=null;
for (;x!=null;y=x,x=x->fa){
x->splay();
x->c[1]->is_root=1;
x->c[1]=y;
y->is_root=0;
}
return y;//返回最后修改的点
}
inline Node *Node::mroot(){
Node *sr=access();
sr->reserve();
return sr;
}
inline void Node::link(Node *v){
v->mroot()->fa=this;
}
int bcj[MAXN+1];//并查集
int gettop(int x){
if (bcj[x]==x)
return x;
return bcj[x]=gettop(bcj[x]);
}
int main(){
freopen("arrival.in","r",stdin);
freopen("arrival.out","w",stdout);
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i){
d[i].fa=d[i].c[0]=d[i].c[1]=null;
d[i].is_root=1;
}
for (int i=1;i<=n;++i)
bcj[i]=i;
for (int i=1;i<=m;++i)
scanf("%d",&root[i]);
for (int i=1;i<=n-m;++i){
int u,v;
scanf("%d%d",&u,&v);
d[u].link(&d[v]);
bcj[gettop(u)]=gettop(v);
}
for (int i=1;i<=m;++i)
d[root[i]].mroot();
int Q;
scanf("%d",&Q);
for (int i=1;i<=Q;++i){
int op,u,v;
scanf("%d%d%d",&op,&u,&v);
if (op==1){
if (gettop(u)!=gettop(v)){
bcj[gettop(u)]=gettop(v);
d[u].link(&d[v]);
}
}
else{
if (gettop(u)==gettop(v)){
d[u].access();
printf("%d
",d[v].access()-d);
}
else
printf("orzorz
");
}
}
return 0;
}
题解做法(离线做法)
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#define MAXN 100000
#define MAXQ 100000
int n,m;
int root[MAXN+1];
struct EDGE{
int to;
EDGE *las;
} e[MAXN*2+1];
int ne;
EDGE *last[MAXN+1];
inline void link(int u,int v){
e[++ne]={v,last[u]};
last[u]=e+ne;
}
int bcj[MAXN+1],tmp[MAXN+1];
int gettop(int x){
if (bcj[x]==x)
return x;
return bcj[x]=gettop(bcj[x]);
}
int fa[MAXN+1][17];//倍增数组
int dep[MAXN+1];
void dfs(int);
inline int lca(int,int);
struct Operation{
int ty;
int u,v;
} o[MAXQ+1];
int main(){
freopen("arrival.in","r",stdin);
freopen("arrival.out","w",stdout);
scanf("%d%d",&n,&m);
for (int i=1;i<=n;++i)
bcj[i]=i;
for (int i=1;i<=m;++i)
scanf("%d",&root[i]);
for (int i=1;i<=n-m;++i){
int u,v;
scanf("%d%d",&u,&v);
link(u,v),link(v,u);
bcj[gettop(u)]=gettop(v);
}
for (int i=1;i<=m;++i){
bcj[gettop(root[i])]=root[i];
bcj[root[i]]=root[i];
}
memcpy(tmp,bcj,sizeof bcj);//bcj要用来处理连边时判断是否出现环,所以暂存一下
int Q;
scanf("%d",&Q);
for (int i=1;i<=Q;++i){
scanf("%d%d%d",&o[i].ty,&o[i].u,&o[i].v);
if (o[i].ty==1){
if (gettop(o[i].u)!=gettop(o[i].v)){
bcj[gettop(o[i].v)]=o[i].v;
bcj[o[i].v]=gettop(o[i].u);
link(o[i].u,o[i].v),link(o[i].v,o[i].u);
}
}
}
for (int i=1;i<=m;++i)
if (gettop(root[i])==root[i])
dfs(root[i]);
swap(bcj,tmp);//这样交换只会交换指针,不会交换整个数组
for (int i=1;i<=Q;++i)
if (o[i].ty==1){
if (gettop(o[i].u)!=gettop(o[i].v)){
bcj[gettop(o[i].v)]=o[i].v;
bcj[o[i].v]=gettop(o[i].u);
}
}
else{
int rt=gettop(o[i].u);
if (rt!=gettop(o[i].v)){
printf("orzorz
");
continue;
}
printf("%d
",lca(o[i].u,o[i].v)^lca(o[i].u,rt)^lca(o[i].v,rt));
}
return 0;
}
void dfs(int x){
dep[x]=dep[fa[x][0]]+1;
for (int i=1;1<<i<dep[x];++i)
fa[x][i]=fa[fa[x][i-1]][i-1];
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa[x][0]){
fa[ei->to][0]=x;
dfs(ei->to);
}
}
inline int lca(int u,int v){
if (dep[u]<dep[v])
swap(u,v);
for (int k=dep[u]-dep[v],i=0;k;k>>=1,++i)
if (k&1)
u=fa[u][i];
if (u==v)
return u;
for (int i=16;i>=0;--i)
if (fa[u][i]!=fa[v][i])
u=fa[u][i],v=fa[v][i];
return fa[u][0];
}
总结
LCT大法好,比题解的在线做法好多了。
如果题目加上个删边操作……题解的两个方法都做不了,除非有什么变态的改进。
其实LCT也挺好打的,很好手推。