树上两点的最近公共祖先(lca)

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];
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值