最近公共祖先(LCA)算法

最近公共祖先:设结点x是u,v的祖先并且x的深度是u,v所有共同祖先中最大的,则称x是u,v的最近公共祖先;
LCA的求解算法有三种:
一.tarjan算法求解LCA
这是一种离线算法,基于dfs和并查集,时间复杂度为O(nlogn+q);
算法的基本步骤:
(1),从根开始dfs
(2),如果当前点u还有儿子v,那么递归进行此算法,并且将v合并到u的并查集里面;
(3),遍历和当前点u相关的询问结点v,如果v已经访问过了,那么LCA(u,v) = v在并查集中的祖先;
理解一下就是:考虑查询的两个结点,无非只有两种关系:
(1),在同一条树链上(2),在两棵子树上
对于第一种情况,假设u是v的祖先,很容易知道LCA(u,v)=u,而在这个算法中,我们dfs肯定先遍历到u,再遍历到v时,u已经遍历过,所以二者的LCA=father[u] = u;
对于第二种情况,u,v所在的子树的分叉起点是LCA(u,v),假设先遍历到的点是v,那么遍历u时,有father[v] 等于 u,v所在的子树的分叉起点,也就是LCA(u,v)。反过来看,由点u开始分叉出的x棵子树,这x棵子树可以看成x个集合,分别从任意两个集合中各选一个点,这两个点的LCA就是u,所以用到了并查集。
关键的三步就是递归的过程中标记访问过的顶点,回溯的过程中将当前节点设置为其儿子的父亲,回溯完毕后再更新当前节点和其他节点的LCA;
例题:poj1330

//#include<bits/stdc++.h>
#include<queue>
#include <cmath>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define mod (1000000007)
#define middle (l+r)>>1
#define lowbit(x) (x&(-x))
#define lson (rt<<1)
#define rson (rt<<1|1)
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int inf_max = 0x3f3f3f3f;
const ll Linf = 9e18;
const int maxn = 1e4 + 10;
const long double E = 2.7182818;
const double eps=0.0001;
using namespace std;
inline int read()
{
    int f=1,res=0;
    char ch=getchar();
    while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
    while(ch>='0'&&ch<='9') { res=res*10+ch-'0' ; ch=getchar(); }
    return f*res;
}
struct EDG {
    int v,nxt;
}edge[maxn];
int cnt,n,head[maxn],vis[maxn],fa[maxn],qa,qb,ans,in[maxn];
void Initial() {
    memset(head,-1,sizeof(head));
    memset(vis,0,sizeof(vis));
    for(int i = 0;i <= n; ++i) fa[i] = i,vis[i] = 0,in[i] = 0;
    cnt = 0;
}
void add_edge(int u,int v) {
    edge[cnt].v = v;
    edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
int Find(int x) {
    if(fa[x] == x) return fa[x];
    return fa[x] = Find(fa[x]);
}
void tarjan(int now) {
    int fnow = Find(now);
    vis[now] = 1;
    for(int i = head[now]; ~i; i = edge[i].nxt) {
        int v = edge[i].v;
        tarjan(v);
        int fv = Find(v);
        fa[fv] = fnow;
    }
    if(now == qa) {
        if(vis[qb]) {
            ans = Find(qb);
            return ;
        }
    }else if(now == qb) {
        if(vis[qa]) {
            ans = Find(qa);
            return ;
        }
    }
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--) {
        scanf("%d",&n);
        Initial();
        for(int i = 1;i < n; ++i) {
            int u,v;
            scanf("%d%d",&u,&v);
            add_edge(u,v);
            in[v]++;
        }
        scanf("%d%d",&qa,&qb);
        int start;
        for(int i = 1; i <= n; ++i) {
            if(in[i] == 0) {
                start = i;
                break;
            }
        }
        tarjan(start);
        cout<<ans<<endl;
    }
    return 0;
}

二、倍增算法
这是一种在线算法,预处理时间复杂度O(nlogn),查询O(logn),总的是O(nlogn + qlogn);
首先预处理出每个点的深度depth数组和结点i的第2^j个祖先f[i][j],比如一个结点的父亲就是该节点的第 2^0 = 1 个祖先;有了这两个信息后,从LCA的定义上来看,求u,v的LCA,在分别把u,v向根移动的过程中,二者第一次相遇的点就是他们的最近公共祖先,所以可以考虑模拟这种移动的方式,直接暴力对每两个点来dfs时间就炸了,所以需要一些优化,在不改变结果的条件下增大每次向上移动的距离;
求解f数组:递推式f[i][j] = f[f[i][j-1][j-1],设t1是 i 的第 2 ^ (j-1)个祖先,t2是t1的第 2 ^ (j-1)个祖先,那么t2就是 i 的第 2 ^ (j-1)+2 ^ (j-1) = 2 ^ j 个祖先。
求解LCA(u,.v):首先将u,v移到同一个深度,这样在用f数组的时候才能保证向上移动后的深度也一样。由于我们记录了结点的第2 ^ k个祖先是谁,所以在移动的时候,我们只要枚举k(0=<k<=log2(结点总数)),如果u,v的第2 ^ k个祖先一样,都等于x,那么说明x可能是u,v的LCA,但也可能不是,因为x可能是LCA(u,v)的祖先,所以我们不能移动,只有当u,v的第2 ^ k个祖先不一样时,那么我们就可以把u,v移动上去,直到k=0的时候,得到的就是LCA(u,v)。
例题

//#include<bits/stdc++.h>
#include<queue>
#include <cmath>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define mod (1000000007)
#define middle (l+r)>>1
#define lowbit(x) (x&(-x))
#define lson (rt<<1)
#define rson (rt<<1|1)
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int inf_max = 0x3f3f3f3f;
const ll Linf = 9e18;
const int maxn = 4e4 + 10;
const long double E = 2.7182818;
const double eps=0.0001;
using namespace std;
inline int read()
{
    int f=1,res=0;
    char ch=getchar();
    while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
    while(ch>='0'&&ch<='9') { res=res*10+ch-'0' ; ch=getchar(); }
    return f*res;
}
int dis[maxn],n,m,head[maxn],vis[maxn],cnt,f[maxn][20],dep[maxn];
struct node{
    int v,nxt,val;
}edge[maxn<<1];
void Initial() {
    memset(head,-1,sizeof(head));
    memset(f,-1,sizeof(f));
    cnt = 0;
    memset(vis,0,sizeof(vis));
}
void add_edge(int u,int v,int val) {
    edge[cnt].v = v;
    edge[cnt].val = val;
    edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void dfs(int now,int pre,int depth) {
    //printf("%d %d\n",now,pre);
    dep[now] = depth;
    f[now][0] = pre;
    for(int i = head[now]; ~i ; i = edge[i].nxt) {
        int v = edge[i].v,val = edge[i].val;
        if(v == pre) continue;
        dis[v] = dis[now] + val;
        dfs(v,now,depth + 1);
    }
}
void pre_handle() {
    dis[1] = 0;
    dfs(1,-1,1);
    for(int i = 1; (1 << i) <= n; ++i) {
        for(int j = 1; j <= n; j++) {
            if(f[j][i - 1] > 0) f[j][i] = f[f[j][i-1]][i-1];//如果j的2^(i-1)次方祖先存在才更新f[j][i]
        }
    }
}
int getdis(int u,int v,int ances) {
    int t1 = dis[u] - dis[ances],t2 = dis[v] - dis[ances];
    return t1 + t2;
}
int LCA(int u,int v) {
    if(dep[u] < dep[v]) swap(u,v);
    //调至同一深度
    int tmp = dep[u] - dep[v];
    for(int i = 0;(1 << i) <= tmp; ++i) {
        if((1 << i) & tmp) {
            u = f[u][i];
        }
    }
    if(u == v) return v;
    //u,v同时向上找
    for(int i = (int)log2(n * 1.0);i >= 0; --i) {
        if(f[u][i] != f[v][i]) u = f[u][i],v = f[v][i];
    }
    return f[v][0];
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--) {
        scanf("%d%d",&n,&m);
        Initial();
        for(int i = 1;i < n; ++i) {
            int u,v,val;
            scanf("%d%d%d",&u,&v,&val);
            add_edge(u,v,val),add_edge(v,u,val);
        }
        pre_handle();
        for(int i = 1;i <= m; ++i) {
            int u,v;
            scanf("%d%d",&u,&v);
            printf("%d\n",getdis(u,v,LCA(u,v)));
        }
    }
    return 0;
}

三.转化为rmq求解LCA
这也是一个在线算法,其时间复杂度和rmq一样为O(nlogn+q),只是多了点空间的花费。带图讲解
该算法需要记录下dfs时的信息:
oula数组:记录欧拉序列的数组
dep数组;记录深度序列的数组
firpos数组:每个点在欧拉序列中第一次出现的位置
有了这些数组,我们可以将dfs时的信息记录下来成为一个一维的数组,并且维护dep数组的区间最小值就可对应出LCA(u,v);
在从u,v的简单路径中,我们一定只会经历一次u,v的LCA,那么我们在深度序列的u,v第一次出现的位置之间来查询深度数组,其中最小的值就是LCA(u,v)所处的深度;
这里dp[i][j]数组维护的是区间[i,i + 2 ^ j - 1]中最小值的位置,以便在欧拉序列中查询到u,v的LCA;
例题同上

//#include<bits/stdc++.h>
#include<queue>
#include <cmath>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#define mod (1000000007)
#define middle (l+r)>>1
#define lowbit(x) (x&(-x))
#define lson (rt<<1)
#define rson (rt<<1|1)
typedef long long ll;
typedef unsigned long long ull;
typedef long double ld;
const int inf_max = 0x3f3f3f3f;
const ll Linf = 9e18;
const int maxn = 5e4 + 10;
const long double E = 2.7182818;
const double eps=0.0001;
using namespace std;
inline int read()
{
    int f=1,res=0;
    char ch=getchar();
    while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); }
    while(ch>='0'&&ch<='9') { res=res*10+ch-'0' ; ch=getchar(); }
    return f*res;
}
int dis[maxn],n,m,head[maxn],cnt,dep[maxn<<1],dp[maxn<<1][30],firpos[maxn],num,oula[maxn<<1];
struct node{
    int v,nxt,val;
}edge[maxn<<1];
void Initial() {
    memset(head,-1,sizeof(head));
    cnt = num = 0;
}
void add_edge(int u,int v,int val) {
    edge[cnt].v = v;
    edge[cnt].val = val;
    edge[cnt].nxt = head[u];
    head[u] = cnt++;
}
void dfs(int now,int pre,int depth) {
    firpos[now] = ++num;oula[num] = now;dep[num] = depth;
    for(int i = head[now]; ~i ; i = edge[i].nxt) {
        int v = edge[i].v,val = edge[i].val;
        if(v == pre) continue;
        dis[v] = dis[now] + val;
        dfs(v,now,depth + 1);
        oula[++num] = now,dep[num] = depth;
    }
}
void ST() {
    memset(dp,-1,sizeof(dp));  //dp数组保存区间中最小值的位置
    for(int i = 1;i <= num; ++i) dp[i][0] = i;
    for(int j = 1;(1 << j) <= num; ++j) {
        for(int i = 1;i <= num; ++i) {
            if(i + (1 << (j - 1)) > num) break;
            int a = dp[i][j - 1],b = dp[i + (1 << (j - 1))][j - 1];
            dp[i][j] = dep[a] < dep[b] ? a : b;
        }
    }
}
int getdis(int u,int v,int ances) {
    int t1 = dis[u] - dis[ances],t2 = dis[v] - dis[ances];
    return t1 + t2;
}
int LCA(int u,int v) {
    int ql = min(firpos[v],firpos[u]),qr = max(firpos[v],firpos[u]),k = (int)log2((double)(qr + 1 - ql));
    int a = dp[ql][k],b = dp[qr - (1 << k) + 1][k];
    if(dep[a] < dep[b]) return oula[a];
    else return oula[b];
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--) {
        scanf("%d%d",&n,&m);
        Initial();
        for(int i = 1;i < n; ++i) {
            int u,v,val;
            scanf("%d%d%d",&u,&v,&val);
            add_edge(u,v,val),add_edge(v,u,val);
        }
        dfs(1,-1,1);
        ST();
        for(int i = 1;i <= m; ++i) {
            int u,v;
            scanf("%d%d",&u,&v);
            printf("%d\n",getdis(u,v,LCA(u,v)));
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值