最近公共祖先LCA / 倍增法+Tarjan法

LCA

LCA(Least Common Ancestors)最近公共祖先问题。指在有根树中,找出某两个结点u和v最近的公共祖先。

暴力法

最原始的思想就是先将两个节点中深度较大的节点不断向父节点移动,使两个节点在同一深度,再一起向父节点寻找,如果找到同一个点则为它们的最近公共祖先。但是时间复杂度高。

倍增法

例题:景区导游【蓝桥杯】
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
优化思想:要将深度较大的节点向上跳,那一次可以跳多一点,f[i][j]表示从i节点向上跳 2 j 2^j 2j次到达的节点,而假设我们要跳7格,则7 = 2 2 2^2 22 + 2 1 2^1 21 + 2 0 2^0 20,每一个数字都可以表示成 2 i 2^i 2i的和

倍增法是在线算法,每个查询都要重新搜索得到结果

#include<iostream>
#include<vector>
using namespace std;
typedef long long LL;
typedef pair<int,LL> PII;
vector<PII> v[100005];
LL dep[100005],dist[100005],f[100005][25];
void dfs(int u,int father,LL d)
{
    //dep记录深度
    dep[u]=dep[father]+1;
    //dist记录到达根节点的距离
    dist[u]=d;
    f[u][0]=father;
    //(1<<i)即2的i次方
    //f[u][i]=f[f[u][i-1]][i-1]可以理解成从u跳2^(i-1)格再跳2^(i-1)格即跳了2^i格
    for(int i=1;(1<<i)<=dep[u];i++) f[u][i]=f[f[u][i-1]][i-1];
    for(auto &p:v[u])
    {
        if(p.first==father) continue;
        dfs(p.first,u,d+p.second);
    }
    return ;
}
int lca(int a,int b)
{
    //令a的深度较大
    if(dep[a]<dep[b]) swap(a,b);
    //从大的往前试探
    for(int i=20;i>=0;i--)
    {
        if(dep[f[a][i]]>=dep[b]) a=f[a][i];
        if(a==b) return a;
    }
    //到达同一深度后向上寻找
    for(int i=20;i>=0;i--)
    {
        if(f[a][i]!=f[b][i])
        {
            a=f[a][i];
            b=f[b][i];
        }
    }
    return f[a][0];
}
LL get(int a,int b)
{
    //两个节点之间的距离的距离即两个节点到根节点的距离减去2倍的LCA到根节点的距离
    return dist[a]+dist[b]-2*dist[lca(a,b)];
}
int main()
{
    int n,k;
    cin>>n>>k;
    for(int i=1;i<n;i++)
    {
        int a,b;
        LL c;
        cin>>a>>b>>c;
        //v[a]记录a节点可以到达的节点以及距离
        v[a].push_back({b,c});
        v[b].push_back({a,c});
    }
    dfs(1,0,0);
    vector<int> a(k);
    for(auto &x:a) cin>>x;
    LL sum=0;
    for(int i=0;i<k-1;i++)
    {
        sum+=get(a[i],a[i+1]);
    }
    for(int i=0;i<k;i++)
    {
        LL ans=sum;
        if(i!=0) ans-=get(a[i],a[i-1]);
        if(i!=k-1) ans-=get(a[i],a[i+1]);
        if(i!=0&&i!=k-1) ans+=get(a[i-1],a[i+1]);
        cout<<ans<<" ";
    }
    return 0;
}

Tarjan法

例题:P3379 最近公共祖先(LCA)洛谷

题目描述
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

输入格式
第一行包含三个正整数 N,M,S,分别表示树的结点个数、询问的个数和树根结点的序号。

接下来 N−1 行每行包含两个正整数 x,y,表示 x 结点和
y 结点之间有一条直接连接的边(数据保证可以构成树)。

接下来 M 行每行包含两个正整数 a,b,表示询问 a 结点和 b 结点的最近公共祖先。

输出格式
输出包含 M 行,每行包含一个正整数,依次为每一个询问的结果。

输入输出样例
输入
5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5
输出
4
4
1
4
4

说明/提示
对于 30% 的数据,N≤10,M≤10。

对于 70% 的数据,N≤10000,M≤10000。

对于 100% 的数据,1≤N,M≤500000,1≤x,y,a,b≤N,不保证 a ≠ b。

样例说明:
该树结构如下:
在这里插入图片描述

第一次询问:2,4 的最近公共祖先,故为 4。

第二次询问:3,2 的最近公共祖先,故为 4。

第三次询问:3,5 的最近公共祖先,故为 1。

第四次询问:1,2 的最近公共祖先,故为 4。

第五次询问:4,5 的最近公共祖先,故为 4。

故输出依次为 4,4,1,4,4。

思路:Tarjan是离线算法,即在一次搜索中就将所有待查询的记录都算出来。
下面这张图可以很好地模拟这个过程:
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
vector<int> edge[500005];
vector<pair<int,int>> query[500005];
int ans[500005],vis[500005],fa[500005];
//使用并查集维护祖先节点
//路径压缩
int find(int u)
{
    if(u==fa[u]) return u;
    fa[u]=find(fa[u]);
    return fa[u];
}
void tarjan(int u)
{
    //vis数组记录是否访问过
    vis[u]=1;
    for(auto p:edge[u])
    {
        if(!vis[p])
        {
            tarjan(p);
            fa[p]=u;
        }
    }
    //看该节点是否有待查询的记录
    for(auto &p:query[u])
    {
        //记录答案
        //这个为什么能记录正确的答案,因为在已经vis的节点中,找到p.first的祖先节点就是公共祖先,可以模拟一下
        if(vis[p.first]) ans[p.second]=find(p.first);
    }
}
int main()
{
    int n,m,s;
    cin>>n>>m>>s;
    for(int i=0;i<n-1;i++)
    {
        int a,b;
        cin>>a>>b;
        //edge记录该点连接着的点
        edge[a].push_back(b);
        edge[b].push_back(a);
    }
    for(int i=0;i<m;i++)
    {
        int a,b;
        cin>>a>>b;
        //query记录待查询的记录,i表示第几个记录
        //两个都要记录是后面可能访问到一个时另一个没有vis,故只能访问到另一个时才能记录答案
        query[a].push_back({b,i});
        query[b].push_back({a,i});
    }
    //fa数组记录节点的祖先节点
    //别忘了这个!!
    for(int i=1;i<=n;i++) fa[i]=i;
    tarjan(s);
    for(int i=0;i<m;i++) cout<<ans[i]<<endl;
    return 0;
}
  • 14
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值