czl的知识点整理3——LCA

本文介绍了最近公共祖先(LCA)问题及其两种解法:欧拉序解法和Trajan解法。通过具体实例和代码详细解释了如何使用这两种方法解决LCA问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

->知识点整理3-最近公共祖先<-

典型题目:

HDU 2586 How far away?
洛谷 P3379 LCA模板题

知识点:

首先我们先了解一下什么是祖先。学习过树的结构之后我们可以知道每一个除了根节点以外的节点都有一个父亲节点,那么父亲节点的父亲是什么呢?通常的,我们都把一个节点所有的父辈节点都称之为祖先,同时还有一点,在LCA中,自己也可以作为自己的祖先(手动滑稽)。而最近公共祖先就是一棵树中的两个点的公共的祖先中,到两个点的距离最小的节点。
知道了这些之后,我们就可以进行下一步的了解了。求最近公共祖先的方法有很多,比如说倍增,RMQ,线段树等等。而我在这里讲的是用DFS序和Trajan解法来解决最近公共祖先问题。

欧拉序解法:

首先,我们都知道欧拉序,知道欧拉序之后,我们就可以用一种复杂度并不是很优的但是比较好理解的做法来解这题。假设我们有这样一棵树:

那么这棵树的欧拉序就是:1 2 4 2 5 2 1 3 1
那么怎么用欧拉序去寻找两个点的最近公共祖先呢。假设我们要找4和3的最近公共祖先,那么我们就需要找到在欧拉序中4和3第一次出现的位置,那么他们的最近公共祖先就一定在这两个位置之间(这个需要自己去思考和理解一下),所以我们找到的位置是第3个和第8个之间,那么只要找到这两个位置之间深度最小的一个点就行了(因为深度最小的点只有一个,并且它是连接所询问的两个点所在子树的节点,所以一定是他们的最近公共祖先)。

下面是代码(题目是HDU 2586),由于题目的数据比较水,所以用这个解法就过了:

#include <bits/stdc++.h>
using namespace std;
const int maxn=40005;
typedef long long ll;

struct edge
{
    int to;
    int len;
    edge(int to,int len):to(to),len(len) { }
};

struct node
{
    int deep;
    int start;//这里的start和end并没有什么意义,只是另一题要记录,就随便捞过来了,可以删去
    int end;
    ll dis;
} no[maxn];

vector<int>oula;//存欧拉序
vector<edge>e[maxn];
int n,m;
int ti;
int nd1[205],nd2[205];//存每一个询问
int pos1[205],pos2[205];//存每一个询问的两个点在欧拉序中的位置

void DFS(int fa,int now,int deep)
{
//  printf("%d ",now);
    oula.push_back(now);
    no[now].start=++ti;
    no[now].deep=deep;//记录深度
    for(int i=0; i<(int)e[now].size(); i++)
    {
        int u=e[now][i].to;//遍历每一个节点
        if(u==fa)continue;
        else
        {
            no[u].dis+=no[now].dis+e[now][i].len;//记录该节点到根节点的距离
            DFS(now,u,deep+1);
            oula.push_back(now);
//          printf("%d ",now);
        }
    }
    no[now].end=ti;
    return ;
}

int find(int pos1,int pos2)//寻找位置,之前把minn的值赋得太小了,WA了很多次,感觉自己很智障
{
    // printf("pos1:%d pos2:%d\n",pos1,pos2);
    int minn=0x3f3f3f3f,poss;
    if(pos1>pos2)swap(pos1,pos2);
    for(int i=pos1; i<=pos2; i++)
    {
        // printf("i:%d %d \n",i,no[oula[i-1]].deep);
        if(no[oula[i-1]].deep<minn)
        {
            minn=no[oula[i-1]].deep;
            poss=oula[i-1];
        }
    }
    return poss;
}

int T;

int main()
{
    scanf("%d",&T);
    while(T--)
    {
        scanf("%d%d",&n,&m);
        for(int i=0;i<maxn;i++)
        {
            e[i].clear();
        }
        oula.clear();
        for(int i=1;i<=n;i++)
        {
            no[i].dis=0;
            no[i].start=0;
            no[i].end=0;
        }
        for(int i=1; i<=n-1; i++)
        {
            int f,t,len;
            scanf("%d%d%d",&f,&t,&len);
            e[f].push_back(edge(t,len));
            e[t].push_back(edge(f,len));
        }
//  for(int i=1;i<=n;i++)
//  {
//      no[i].pos=i;
//  }
        for(int i=1; i<=m; i++)
        {
            scanf("%d%d",&nd1[i],&nd2[i]);
        }
        memset(pos1,-1,sizeof pos1);
        memset(pos2,-1,sizeof pos2);
        DFS(1,1,0);
        for(int i=0; i<(int)oula.size(); i++)
        {
        // printf("%d ",oula[i]);
            for(int j=1; j<=m; j++)
            {
                if(oula[i]==nd1[j]&&pos1[j]==-1)pos1[j]=i+1;
                if(oula[i]==nd2[j]&&pos2[j]==-1)pos2[j]=i+1;
            }
        }
    // puts("");
    // for(int i=1;i<=n;i++)
    // {
    //  printf("%d %d\n",i,no[i].dis);
    // }
        for(int i=1; i<=m; i++)
        {
            int gf=find(pos1[i],pos2[i]);
            // printf("%d\n",gf);
            ll dis=no[nd1[i]].dis-no[gf].dis+no[nd2[i]].dis-no[gf].dis;//利用已经记录的距离来算两点之间的距离
            printf("%lld\n",dis);
        }
    }
}

Trajan解法:

Trajan是一种比较优越的解法,同时也并不难理解所需要的知识点也不多。首先,我们需要两个数组:bool类型的vis数组,记录每个节点是否已经被访问过了,然后是f数组,记录每个节点的父亲。
有了这些之后,我们就可以进行DFS了。开始时先把vis都设为0,f都设为自己。每当收到一个节点的时候,更新vis数组和f数组,并且都做一次查看,查看该节点node1是否在询问的节点当中,如果是,则查看另一个询问的节点node2,若另一个节点node2也已经被访问过了,那么就从另一个节点node2开始搜父亲,知道搜到一个节点的父亲为自己或者搜到节点node1即可。最终搜到的节点即为两点的最近公共祖先。这样子只需要把数DFS一遍就可以得出答案了。
如果觉得这里讲的还是不够清楚的话,可以去看这个大佬的博客,讲的非常清楚,保证一遍就懂:
大佬~
下面是代码:

#include <bits/stdc++.h>
using namespace std;
const int maxn=1005;

int n,m;
int f[maxn];
bool vis[maxn];
int q1[maxn],q2[maxn];//记录每组询问
int ans[maxn];//记录答案

vector<int >e[maxn];

void init()//初始化
{
    for(int i=1;i<=n;i++)
    {
        f[i]=i;
        vis[i]=0;
    }
}

int find(int now,int tra)//Tra为目标的点,如果搜到目标点或者搜到f[i]=i的点,就返回这个值
{
    printf("now:%d\n",now);
    if(f[now]==now)return now;
    else if(f[now]==tra)return tra;
    return f[now]=find(f[now],tra);
}

void DFS(int now,int fa)//很普通的DFS
{

    for(int i=0;i<(int)e[now].size();i++)
    {
        int u=e[now][i];
        if(fa==u)continue;
        else 
        {
            DFS(u,now);

        }
    }
    f[now]=fa;
    vis[now]=1;
    for(int i=1;i<=m;i++)//查看询问当中是否有这个节点
    {
        if(q1[i]==now)
        {
            if(vis[q2[i]])//如果另一节点也被访问过了,那么就进行find
            {
                ans[i]=find(q2[i],now);
                printf("%d\n",ans[i]);
            }
        }
        else if(q2[i]==now)
        {
            if(vis[q1[i]])
            {
                ans[i]=find(q1[i],now);
                printf("%d\n",ans[i]);
            }
        }
    }

    return ;
}

int main()
{
    scanf("%d%d",&n,&m);
    init();
    for(int i=1;i<=n-1;i++)
    {
        int f,t;
        scanf("%d%d",&f,&t);
        e[f].push_back(t);
        e[t].push_back(f);
    }
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&q1[i],&q2[i]);
    }
    DFS(1,1);
    for(int i=1;i<=m;i++)
    {
        printf("%d\n",ans[i]);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值