LCA(Least Common Ancestors)问题是指对于有根树T的两个节点u,v,最近公共祖先LCA(T,u,v)表示一个节点x,满足x是u,v的祖先且x的深度尽可能大。对于x点来说,有一点非常特殊,那就是从u到v的路径一定经过x。
对于LCA问题,一共有三种解法:1.离线算法Tarjan-LCA算法 2.在线算法,基于RMQ的算法 3.基于二分搜索的算法,也称为倍增算法
以传送门为例
1.离线算法Tarjan-LCA算法
Tarjan算法基于深度优先搜索的框架,对于新搜索到的一个节点,首先创建由这个节点构成的集合,再对当前节点的每个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已经解决(询问的两个点都在这个子树中)。其他的LCA询问的结果必然在这个子树之外(询问的一个点不在这个子树中),这时把子树所形成的集合与当前节点的集合合并,并将当前节点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前节点的所有子树搜索完。这时把当前节点也设为已被检查过的,同时可以处理有关当前节点的LCA询问,如果有一个从当前节点到节点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前节点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包含v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。
对于每一点u:
(1).建立以u为代表元素的集合
(2).遍历与u相连的节点v,如果没有被访问过,对于v使用Tarjan-LCA算法,结束后,将v的集合并入u的集合
(3).对于与u有关的询问(u,v),如果v被访问过,则结果就是v所在集合的代表元素
算法复杂度分析:由于深度优先搜索会遍历到每条边,也就是说深度优先搜索的时间复杂度是O(m)。而对于每个询问都要应答,每个应答在两个点都被搜索到之后应答,也就是说每个询问应答一次,路径压缩后的并查集的查询效率可以认为是O(1),所以应答的时间效率为O(q),总体时间效率为O(m+q)。而在树上,m=n-1,也就是时间复杂度为O(n+q),可以说是非常高效的。算法的缺点在于需要记录所有的询问后再应答,是离线的算法。
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=40010;
const int M=210;
struct edge{
int u,v,w,next;
};
edge edges[2*N];
int head[N];
struct ask{
int u,v,lca,next;
};
ask question[2*M];
int _head[N];
int tot,fa[N],vis[N],dir[N];
inline void add_edge(int u,int v,int w)
{
edges[tot].u=u;
edges[tot].v=v;
edges[tot].w=w;
edges[tot].next=head[u];
head[u]=tot++;
swap(u,v);
edges[tot].u=u;
edges[tot].v=v;
edges[tot].w=w;
edges[tot].next=head[u];
head[u]=tot++;
}
inline void add_ask(int u,int v)
{
question[tot].u=u;
question[tot].v=v;
question[tot].lca=-1;
question[tot].next=_head[u];
_head[u]=tot++;
swap(u,v);
question[tot].u=u;
question[tot].v=v;
question[tot].lca=-1;
question[tot].next=_head[u];
_head[u]=tot++;
}
int find(int u)
{
if(fa[u]==u){
return u;
}else{
return fa[u]=find(fa[u]);
}
}
void tarjan(int u)
{
vis[u]=true;
fa[u]=u;
for(int k=head[u];k!=-1;k=edges[k].next){
if(!vis[edges[k].v]){
int v=edges[k].v,w=edges[k].w;
dir[v]=dir[u]+w;
tarjan(v);
fa[v]=fa[u];
}
}
for(int k=_head[u];k!=-1;k=question[k].next){
if(vis[question[k].v]){
int v=question[k].v;
question[k].lca=question[k^1].lca=find(v);
}
}
}
int main()
{
int casen,n,q;
scanf("%d",&casen);
while(casen--){
scanf("%d%d",&n,&q);
memset(head,-1,sizeof(head));
memset(_head,-1,sizeof(_head));
tot=0;
for(int i=1;i<n;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w);
}
tot=0;
for(int i=1;i<=q;i++){
int u,v;
scanf("%d%d",&u,&v);
add_ask(u,v);
}
memset(vis,0,sizeof(vis));
dir[1]=0;
tarjan(1);
for(int i=0;i<q;i++){
int s=2*i;
int u=question[s].u;
int v=question[s].v;
int lca=question[s].lca;
printf("%d\n",dir[u]+dir[v]-2*dir[lca]);
}
}
return 0;
}
2.在线算法,基于RMQ的算法(了解DFS序传送门)
对于涉及到有根树的问题,将树转化成从根DFS标号后得到的序列处理的技巧十分有效。对于LCA,利用该技巧也能够高效求解。首先,将从根DFS访问的顺序得到的顶点序号vs[i]和对应的深度depth[i]。对于每个顶点v,记其在vs中首次出现的下标为id[v]。
这些都可以在O(n)时间内求得,而LCA(u,v)就是访问u之后到访问v之前所经过顶点中离根最近的那个,假设id[u]<=id[v],那么有
LCA(u,v)=vs[id[u]<=i<=id[v]中令depth(i)最小的i]
这些可以利用RMQ高效求得
算法复杂度分析:通过O(n)的处理转化成RMQ问题,在O(nlogn)的时间内做预处理后形成在线的算法
#include<iostream>
#include<cstdio>
#include<cmath>
#include<cstring>
using namespace std;
const int N=40010;
const int M=25;
struct edge{
int u,v,w,next;
};
edge edges[2*N];
int head[N];
int tot,vs[2*N],depth[2*N],first[N],dir[N],_pow[M];
bool vis[N];
int dp[2*N][M];
inline void add(int u,int v,int w)
{
edges[tot].u=u;
edges[tot].v=v;
edges[tot].w=w;
edges[tot].next=head[u];
head[u]=tot++;
swap(u,v);
edges[tot].u=u;
edges[tot].v=v;
edges[tot].w=w;
edges[tot].next=head[u];
head[u]=tot++;
}
void dfs(int u,int dep)
{
vis[u]=true;
vs[++tot]=u;
first[u]=tot;
depth[tot]=dep;
for(int k=head[u];k!=-1;k=edges[k].next){
if(!vis[edges[k].v]){
int v=edges[k].v,w=edges[k].w;
dir[v]=dir[u]+w;
dfs(v,dep+1);
vs[++tot]=u;
depth[tot]=dep;
}
}
}
void st(int len)
{
int k=(int)(log((double)len)/log(2.0));
for(int i=1;i<=len;i++){
dp[i][0]=i;
}
for(int j=1;j<=k;j++){
for(int i=1;i+_pow[j]-1<=len;i++){
int a=dp[i][j-1],b=dp[i+_pow[j-1]][j-1];
if(depth[a]<depth[b]){
dp[i][j]=a;
}else{
dp[i][j]=b;
}
}
}
}
int rmq(int x,int y)
{
int k=(int)(log((double)(y-x+1))/log(2.0));
int a=dp[x][k],b=dp[y-_pow[k]+1][k];
if(depth[a]<depth[b]){
return a;
}else{
return b;
}
}
int lca(int u,int v)
{
int x=first[u],y=first[v];
if(x>y){
swap(x,y);
}
int res=rmq(x,y);
return vs[res];
}
int main()
{
for(int i=0;i<M;i++){
_pow[i]=(1<<i);
}
int casen;
scanf("%d",&casen);
while(casen--){
int n,q;
tot=0;
scanf("%d%d",&n,&q);
memset(head,-1,sizeof(head));
for(int i=1;i<n;i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
}
tot=0;
dir[1]=0;
memset(vis,false,sizeof(vis));
dfs(1,1);
st(2*n-1);
while(q--){
int u,v;
scanf("%d%d",&u,&v);
int lcaa=lca(u,v);
printf("%d\n",dir[u]+dir[v]-2*dir[lcaa]);
}
}
return 0;
}
3.基于二分搜索的算法,也成为倍增算法
设节点v到根的深度为depth(v)。那么,如果节点w是u和v的公共祖先的话,让u向上走depth(u)-depth(w)步,让v向上走depth(v)-depth(w)步,都将走到w。因此,首先让u和v中较深的一方向上走|depth(u)-depth(v)|步,再一步一步向上走,直到走到同一个节点,就可以在O(depth(u)+depth(v))时间内求出LCA。
vector<int>G[max_v];//图的邻接表表示
int root;
int parent[max_v];
int depth[max_v];
void dfs(int v,int p,int d)
{
parent[v]=p;
depth[v]=d;
for(int i=0;i<G[v].size();i++){
if(G[v][i!=p]){
dfs(G[v][i],v,d+1);
}
}
}
void init()
{
dfs(root,-1,0);
}
int lca(int u,int v)
{
//让u和v走到同一深度
while(depth[u]>depth[v]){
u=parent[u];
}
while(depth[v]>depth[u]){
v=parent[v];
}
//让u和v走到同一节点
while(u!=v){
u=parent[u];
v=parent[v];
}
return u;
}
节点的最大深度是O(n),所以该算法的复杂度也是O(n)。如果只需要计算一次LCA的话,这便足够了。但如果计算多对点的LCA的话就不行了,刚才的算法,通过不断向上走到同一节点来计算u和v的LCA。这里,到达了同一节点后,不论怎么向上走,到达的显然还是同一节点。利用这一点,我们使用二分搜索求出到达共同祖先所需的最小步数吗?事实上,只要利用如下预处理,就可以实现二分搜索。
首先,对于任意顶点v,利用其父亲节点信息,可以通过parent2[v]=parent[parent[v]]得到其向上走两步所到的顶点。再利用这一信息,又可以通过parent4[v]=parent2[parent2[v]]得到其向上走四步所到的顶点。依此类推,就能够得到其向上走2^k步所到的顶点parent[k][v]。有了k=floor(logn)以内的所有信息后,就可以进行二分搜索了,每次的复杂度是O(logn)。另外,预处理parent[k][v]的复杂度是O(nlogn)。
vector<int>G[max_v];
int root;
int parent[max_log_v][max_v];
int depth[max_v];
void dfs(int v,int p;int d)
{
parent[0][v]=p;
depth[v]=d;
for(int i=;i<G[v].size();i++){
if(G[v][i]!=p){
dfs(G[v][i],v,d+1);
}
}
}
void init(int V)
{
dfs(root,-1,0);
for(int k=0;k+1<max_log_v;k++){
for(int v=0;v<V;v++){
if(parent[k][v]<0){
parent[k+1][v]=-1;
}else{
parent[k+1][v]=parent[k][parent[k][v]];
}
}
}
}
int lca(int u,int v)
{
if(depth[u]>depth[v]){
swap(u,v);
}
for(int k=0;k<max_log_v;k++){
if((depth[v]-depth[u])>>k&1){
v=parent[k][v];
}
}
if(u==v){
return u;
}
for(int k=max_log_v-1;k>=0;k--){
if(parent[k][u]!=parent[k][v]){
u=parent[k][u];
v=parent[k][v];
}
}
return parent[0][u];
}