[ BZOJ 4668 ] 冷战
有\(N\)个点,开始没有边相连,进行按顺序给出的\(M\)个操作:
- \(0\ u\ v\) 将\(u,v\)两点连一条边
- \(1\ u\ v\) 查询\(u,v\)两点最早在第几条边连接的时候被连通
每次询问输出一个边的编号,强制在线。
- \(N,M\in [1,5\times 10^5]\)
\(\\\)
\(Solution\)
在线并查集树上查询\(Lca\)。
维护连通性的时候并查集不进行路径压缩,只进行按秩合并。考虑到并查集是树形结构,定义连通块的秩为块内树高\((\)其实定义为块的大小表现也不错\()\)。这样我们得到的是一棵真正的通过并集来连接的并查集树。
这棵树上有什么好的性质?答案是两点到\(Lca\)的路径上的边集,是真正将这两点连接起来的边集。也就是说,对于两点查询的答案,一定是两点到\(Lca\)路径上的最大边编号。
考虑如何求\(Lca\)。因为是在线,所以显然不能建立倍增数组等基于固定的树形态的做法。考虑暴力标记,先将两点跳到同一高度,再共同跳到\(Lca\)处既可,这样也能很方便的处理路径\(max\)的问题。
关于复杂度,其实它是合法的。根据按秩合并的原理,在查询连通块代表元素时复杂度是\(log\)级别的,同理都是跳父节点的过程,所以查询\(Lca\)复杂度也是\(log\)级别的。
\(\\\)
\(Code\)
并查集需要维护:当前节点深度,当前节点父节点编号,到父节点的边权,以及以当前节点为根子树大小。
注意当前节点深度这个部分,在查询\(Lca\)之前我们需要先更新一遍以确保深度是正确的,这个过程可以在查询代表元素的时候同时进行。
#include<cmath>
#include<cstdio>
#include<cctype>
#include<cstring>
#include<cstdlib>
#include<iostream>
#include<algorithm>
#define N 500010
#define R register
#define gc getchar
using namespace std;
int n,m,ans,cnt;
struct UFS{
int f[N],g[N],d[N],sz[N];
UFS(){for(R int i=1;i<N;++i)f[i]=i,d[i]=sz[i]=1;}
int find(int x){
if(x==f[x])return x;
int ans=find(f[x]); d[x]=d[f[x]]+1; return ans;
}
inline void merge(int x,int y){
int fx=find(x),fy=find(y);
if(sz[fx]>sz[fy]) fx^=fy^=fx^=fy;
sz[fy]+=sz[fx]; f[fx]=fy; g[fx]=cnt; d[fx]=d[fy]+1;
}
inline int lca(int x,int y){
int ans=0;
if(d[x]>d[y]) x^=y^=x^=y;
while(d[y]>d[x]) ans=max(ans,g[y]),y=f[y];
if(x==y) return ans;
while(x!=y){
ans=max(ans,max(g[x],g[y]));
x=f[x]; y=f[y];
}
return ans;
}
}ufs;
inline int rd(){
int x=0; bool f=0; char c=gc();
while(!isdigit(c)){if(c=='-')f=1;c=gc();}
while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=gc();}
return f?-x:x;
}
int main(){
n=rd(); m=rd();
for(R int i=1,op,x,y;i<=m;++i){
op=rd(); x=rd()^ans; y=rd()^ans;
if(op==0){
++cnt;
if(ufs.find(x)!=ufs.find(y)) ufs.merge(x,y);
}
else if(ufs.find(x)!=ufs.find(y)){ans=0;puts("0");}
else printf("%d\n",(ans=ufs.lca(x,y)));
}
return 0;
}