学习笔记——LCA

LCA

LCA,即最近公共祖先,在图论中应用比较广泛。

LCA的定义如下:给定一个有根树,若节点z同时是节点x和节点y的祖先,则称 z 是 x,y 的公共祖先;在 x,y 的所有公共祖先当中深度最大的称为 x,y 的最近公共祖先。下面给出三个最近公共祖先的例子:
在这里插入图片描述

显然,从上面的例子可以得出, LCA(x,y)即为 x,y 到根节点的路径的交汇点,也是 x 到 y 的路径上深度最小的节点。

向上标记法求LCA

求LCA最直接的方法,单次查询的时间复杂度最坏为 O(n)(看起来好像还挺快的,不过什么题会只有一次查询呢)
查询方式是从一个点向上搜,到和另一个点深度相同的地方就一起搜直到搜到。代码简单因为太慢了没人用我也懒得打了。
我见没人打

树上倍增求LCA(在线算法)

稍微思考一下,向上标记法之所以求LCA慢,是因为这dd一次只能爬一格,那么我们一次爬很多格不就OK了,根据二进制拆分有了这个算法。

设F(x,y)表示x的 2 y 2^y 辈父节点,就是x向上走 2 y 2^y 到的节点,如果这个节点不存在那么特判为0。
我们可以建二维数组F[n][m]

往根节点走 2 y 2^y 步就是走两个 2 y 1 2^{y-1} ,这实质上是一个动态规划,深度就是此时的阶段。

有F[x][y]=f[f[x][k-1]][k-1],

就是x的 2 y 1 2^{y-1} 辈父节点再往上走 2 y 1 2^{y-1} .

当我们要求两点的 LCA 时, 先让它们到同一高度. 这个过程我们使用二进制拆分来加速. 比如当两点高度相差 5 时, ( 5 ) 10 = ( 101 ) 2 (5)_{10}=(101)_2 , 那么我们就让高度较小的那个节点先往上爬 2 2 2^2 =4 步, 再往上 2 0 2^0 =1 步. 此时两点即在同一高度.

如果爬到同一高度后两点相同, 显然这个点就是它们的 LCA, 直接返回即可.

如果两点不同, 就一起往上爬. 这是一个无限逼近的过程, 直到找到它们的 LCA 的子节点为止.

预处理O(nlogn),每次查询O(logn)

例题在洛谷P1084
模板题
外国友人模板题

/喜闻乐见抄的代码

#include<iostream>
#include<cstdio>
#include<queue>
#include<cmath>
using namespace std;
const int N=6e5;
int n,m,s,t,tot=0,f[N][20],d[N],ver[2*N],Next[2*N],head[N];
queue<int> q;
void add(int x,int y)
{
    ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
}//邻接表存边操作。由于只求LCA时不关心边权,因此可以不存边权
void bfs()
{
    q.push(s);
    d[s]=1;//将根节点入队并标记
    while(q.size())
    {
        int x=q.front();q.pop();//取出队头
        for(int i=head[x];i;i=Next[i])
        {
            int y=ver[i];
            if(d[y])
                continue;
            d[y]=d[x]+1;
            f[y][0]=x;//初始化,因为y的父亲节点就是x
            for(int j=1;j<=t;j++)
                f[y][j]=f[f[y][j-1]][j-1];//递推f数组
            q.push(y);
        }
    }
}
int lca(int x,int y)
{
    if(d[x]>d[y])
        swap(x,y);
    for(int i=t;i>=0;i--)
        if(d[f[y][i]]>=d[x])
            y=f[y][i];//尝试上移y
    if(x==y)
        return x;//若相同说明找到了LCA
    for(int i=t;i>=0;i--)
        if(f[x][i]!=f[y][i])
        {
            x=f[x][i],y=f[y][i];
        }//尝试上移x、y并保持它们不相遇
    return f[x][0];//当前节点的父节点即为LCA
}
int main()
{
    cin>>n>>m>>s;
    t=log2(n)+1;
    for(int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    bfs();
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        printf("%d\n",lca(a,b));
    }
    return 0;
}

Tarjan求LCA(离线)

众所周知,Tarjan是精心设计的dfs,通过dfs的遍历特性巧妙的合并已查询点,从而实现寻找LCA。
有集合之间的关系所以用到并查集

算法思路

1.任选一个点为根节点,从根节点开始。

2.遍历该点u所有子节点v,并标记这些子节点v已被访问过。

3.若是v还有子节点,返回2,否则下一步。

4.合并v到u上。

5.寻找与当前点u有询问关系的点v。

6.若是v已经被访问过了,则可以确认u和v的最近公共祖先为v被合并到的父亲节点a。

是不是有些抽象,那么来看我找的一个超棒的算法演示

链接

具体实现也是从里面

#include<bits/stdc++.h>
using namespace std;
int n,k,q,v[100000];
map<pair<int,int>,int> ans;//存答案
int t[100000][10],top[100000];//存储查询关系
struct node{
    int l,r;
};
node s[100000];
/*并查集*/
int fa[100000];
void reset(){
    for (int i=1;i<=n;i++){
        fa[i]=i;
    }
}
int getfa(int x){
    return fa[x]==x?x:getfa(fa[x]);
}
void marge(int x,int y){
    fa[getfa(y)]=getfa(x);
}
/*------*/
void tarjan(int x){
    v[x]=1;//标记已访问
    node p=s[x];//获取当前结点结构体
    if (p.l!=-1){
        tarjan(p.l);
        marge(x,p.l);
    }
    if (p.r!=-1){
        tarjan(p.r);
        marge(x,p.r);
    }//分别对l和r结点进行操作
    for (int i=1;i<=top[x];i++){
        if (v[t[x][i]]){
            cout<<getfa(t[x][i])<<endl;
        }//输出
    }
}
int main(){
    cin>>n>>q;
    for (int i=1;i<=n;i++){
        cin>>s[i].l>>s[i].r;
    }
    for (int i=1;i<=q;i++){
        int a,b;
        cin>>a>>b;
            t[a][++top[a]]=b;//存储查询关系
            t[b][++top[b]]=a;
    }
    reset();//初始化并查集
    tarjan(1);//tarjan 求 LCA
}

U1S1 Tarjan是真的强,在学习SCC时老师就讲这个算法的潜力,现在总算是窥出冰山一角。时间复杂度 O ( n α ( n ) ) O(nα(n)) ,就是并查集的时间复杂度。

树链剖分求LCA

猜你喜欢

转载自blog.csdn.net/m0_46207148/article/details/107921201
lca
今日推荐