Tarjan算法不仅可以用来缩点,还可以用来求LCA。
先介绍一下LCA,LCA(least common ancestors),是最近公共祖先的英文。指的是在一棵树中,点u和点v的最近的祖先。
如图,以黄点为树根:紫点和黑点的最近公共祖先就是绿点,白点和橙点的最近公共祖先就是橙点,灰点和蓝点的最近公共祖先是黄点。简而言之,LCA就是点u和v的父亲的父亲的父亲……第一个相交的父亲。
LCA的算法有很多,例如倍增RMQ,树剖。用Tarjan算法来求LCA,理论上时间复杂度为(N+Q),但在实际操作中会稍稍慢一些。Tarjan求LCA是一个离线算法,并基于并查集(union-find-set)。
我们用vis记录该点是否被访问过,fa记录该点的父亲,从根节点向下搜索,遍历每一个儿子。如果它的儿子全都遍历完了,就去查询与该点有关的点,也就是所查询的点组中有该点的那些组,如果另一个点v已经被访问过,那么这两个点的最近公共祖先就是find(v),否则先不记录。(证明略)
基本思路就是这五步:
1.从根节点开始搜索。
2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。
3.若是v还有子节点,遍历v的子节点,否则合并u,v。
4.寻找与当前点u有询问关系的点v。
5.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。
伪代码:
void lca(int x){
now[x]=1;
for x的所有儿子{
lca(v);
vis[v]=1;fa[v]=x;
}
for 与x有关的查询
if(vis[v])
ans=find(v);
}
模板:
void lca(int x){
now[x]=1;
for(int i=0;i<f[x].size();i++){
int go=f[x][i];
if(now[go]) continue;
lca(go);
vis[go]=1;fa[go]=x;
}
for(int i=0;i<query[x].size();i++){
int go=query[x][i];
if(vis[go]) ans=find(go);
}
}
2018.5.25更新:
这里再谈谈LCA,一般来说LCA有四种求法:在线ST表,在线倍增,在线树剖和离线Tarjan。
离线Tarjan这里已经详细地讲述了,ST表和倍增算法是比较普遍的算法,这些算法网上都有详细的讲解,这里就贴一个倍增求LCA的函数:
int lca(int x,int y){
register int i;
if(dep[x]>dep[y]) swap(x,y);
for(i=19;i>=0;i--)
if(dep[x]<=dep[gran[y][i]]) y=gran[y][i];
for(i=19;i>=0;i--)
if(gran[x][i]!=gran[y][i]) x=gran[x][i],y=gran[y][i];
if(x!=y) y=gran[y][0];
return y;
}
这里主要讲一讲树剖的方法,由于树剖维护了重链,所以每一次重链顶较深的点往上跳,直跳到两个点在同一个链上,则此时深度较小的点就是LCA。
int LCA(int x,int y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]]) swap(x,y);
y=fa[top[y]];
}
if(dep[x]>dep[y]) swap(x,y);
return x;
}