最近公共祖先:LCA及其用倍增实现 +POJ1986

空扰寡人 提交于 2020-01-22 11:04:23

 

Q:为什么我在有些地方看到的是最小公共祖先?

A:最小公共祖先是LCA(Least Common Ancestor)的英文直译,最小公共祖先与最近公共祖先只是叫法不同。

Q:什么是最近公共祖先(LCA)?

A:最近公共祖先的概念是很好理解的。首先,你需要脑补出一棵树(它可以是二叉树,也可以是多叉树。)之后,请你再在你脑补出的树上任取两个点。每个点都可以到达树根,且到达的路径是唯一的,既然两个点都可以到达树根,那么根无疑是这两个点的公共祖先。然而,根却不一定是这两个点的最近公共祖先,相反,离根距离最远且在两条点到根路径上的点才是最近公共祖先(最近公共父节点)。

 

实现求LCA的方法有很多种,无论是离线还是在线,是TARJAN还是RMQ等等都可以实现。在此,安利一种PO主钟爱的方法:倍增,来实现LCA。

首先,你不可以认为倍增实现LCA和RMQ实现LCA是指的同一回事情,哪怕RMQ的完成是用了倍增思想。事实上,RMQ实现LCA的程序比较繁琐,并且需要你考虑到众多细节。而这些细节的调试在比赛有限的时间内无疑是要爆炸的。比如说,PO主就在某次D2T3跪在了RMQ实现LCA上QWQ。与RMQ相反,倍增实现的代码要比RMQ实现简单一些,并且好脑补,而且容易调试,相信大家一定可以弄明白倍增实现LCA的QWQ

在没有学习倍增写LCA之前,你是怎么样求LCA的呢?至少,我是老老实实地让这两个点一步一步往上移并找出它们的路径第一次交汇的地方。这种方法固然可行、好想,但它的效率实在不高。但是,我们完全可以通过提高“这两个点一步一步往上移”来提高效率。

所以,我们采用倍增的思路来预处理,分别记录这点的祖先,记录为anc[i][j]。即为第i个点往上2^j个祖先。比如说,当j=0时,2^j=1,anc[i][j]是第i个点的上一个节点,即它的父亲节点。

那么该如何预处理出anc数组呢?

 

int anc[1005][25];
int fa[1005];
vector <int > tree[1005];
int deep[1005];

void dfs(int x)
{
    anc[x][0]=fa[x];
    for (int i=1;i<=22;i++)
    {
        anc[x][i]=anc[anc[x][i-1]][i-1];//倍增思想的体现。不妨在纸上试着画一棵树,脑补一下QWQ
    }
    
    for (int i=0;i<tree[x].size();i++)
    {
        if (tree[x][i]!=fa[x])
        {
            int y=tree[x][i];
            fa[y]=x;//记录父亲节点
            deep[y]=deep[x]+1;//记录深度
            dfs(y);
        }
    }
}

 

通过从根节点开始的DFS,我们就预处理好了ANC数组。

 

下面,我们来考虑如何处理LCA查询。即每次给你两点X和Y,求出它们的LCA(X,Y)。在有了ANC数组之后,求出最近公共祖先就会变得很简单。

首先,让X,Y在同一深度上。在大多数情况下,查询给你的两个点X和Y它们的深度是不同的。但是,如果两点的深度相同,我们就可以实现两个点同时倍增比较何时祖先相同。所以,第一步是使X,Y中深度较深的点往上移动直到与另一个点深度相同。当然,点的移动也可以用倍增完成。

然后,当两点深度相同后,同时向上倍增两个点,当它们祖先刚好相同时,这个祖先就是它们的LCA。

如果你还是有一些不理解的话,不妨看LCA实现的代码QAQ

int lca(int x,int y)
{
    if (deep[x]<deep[y]) _swap(x,y);//我们希望X是较深的点。

    for (int i=22;i>=0;i--)//这个循环在完成第一步。
    {
        if (deep[y]<=deep[anc[x][i]]) //不可以丢掉“=“哦Q^Q
        {
            x=anc[x][i];
        }
    }
    
    if (x==y) return x;//如果Y是X的祖先,就可以直接返回结果了。
    
    for (int i=22;i>=0;i--)
    {
        if (anc[x][i]!=anc[y][i]) //第二步。
        {
            x=anc[x][i];
            y=anc[y][i];
        }
    }
    
    return anc[x][0];//注意第二步IF语句的条件。
}

 

Q为什么可以保证倍增就可以刚好到达那个我想要的点呢?

A:你不妨假设你现在点的位置到你想要的点的位置之间距离为D,那么D是整数,那么D一定可以用二进制表示,还记得ANC的第二维代表什么意思吗?二进制数D为1的地方就是我们需要往上翻的地方(比较抽象,适合画一画QAQ),为0的地方我们不动就好。

 

同时,LCA还可以用于求树上两点之间的距离。比如说POJ1986.

Distance Queries
Farmer John's cows refused to run in his marathon since he chose a path much too long for their leisurely lifestyle. He therefore wants to find a path of a more reasonable length. The input to this problem consists of the same input as in "Navigation Nightmare",followed by a line containing a single integer K, followed by K "distance queries". Each distance query is a line of input containing two integers, giving the numbers of two farms between which FJ is interested in computing distance (measured in the length of the roads along the path between the two farms). Please answer FJ's distance queries as quickly as possible!
 

Input

* Lines 1..1+M: Same format as "Navigation Nightmare"

* Line 2+M: A single integer, K. 1 <= K <= 10,000

* Lines 3+M..2+M+K: Each line corresponds to a distance query and contains the indices of two farms.

 

Output

* Lines 1..K: For each distance query, output on a single line an integer giving the appropriate distance.

 

Sample Input

7 6
1 6 13 E
6 3 9 E
3 5 7 S
4 1 3 N
2 4 20 W
4 7 2 S
3
1 6
1 4
2 6

 

Sample Output

13
3
36

Hint

Farms 2 and 6 are 20+3+13=36 apart

 

这道题目可以直接忽视方向哦QAQ

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <set>
#include <vector>
#include <cstring> 
using namespace std;

int n,m,q;
int up[233333],de[233333],dp[233333][23],fa[233333];
int pn[233333],pg[233333],pv[233333],st[233333];
int tot=0;

void init()
{
    memset(up,0,sizeof(up));
    memset(de,0,sizeof(de));
    memset(dp,0,sizeof(dp));
    memset(pn,0,sizeof(pn));
    memset(pg,0,sizeof(pg));
    memset(pv,0,sizeof(pv)); 
    memset(fa,0,sizeof(fa));
    
    return ;
}

void ins(int x,int y,int w)
{
    pv[++tot]=y;
    pg[tot]=w;
    pn[tot]=st[x];
    st[x]=tot;
    
    return;
}

void dfs(int x)
{
        dp[x][0]=fa[x];
        for(int i=1;i<20;i++)
        {
            dp[x][i]=dp[dp[x][i-1]][i-1];
         } 
         
        for (int i=st[x];i;i=pn[i])
        {
             int cur=pv[i];
             if (cur==fa[x]) continue;
             de[cur]=de[x]+1;
             up[cur]=up[x]+pg[i];
             fa[cur]=x;
             dfs(cur); 
        }
         
    return;
}

int lca(int x,int y)
{
    if (de[x]<de[y]) {
        int t=x;
        x=y;
        y=t;
    }
    
    for (int i=19;i>=0;i--)
    {
        if (de[dp[x][i]]>=de[y]) x=dp[x][i]; 
     } 
     
    if (x==y) return x;
    
    for (int i=19;i>=0;i--)
    {
        if (dp[x][i]!=dp[y][i]) 
        {
            x=dp[x][i];
            y=dp[y][i];
        }
     } 
     
    return dp[x][0];
}

int main()
{

    while(~scanf("%d%d",&n,&m))
    {
        init();
        int a,b,c;
        for (int i=1;i<=m;i++)
        {
            char s[10];
            scanf("%d%d%d%s",&a,&b,&c,s);
            ins(a,b,c); ins(b,a,c);
         } 
         
         fa[1]=1;
         de[1]=up[1]=0;
         dfs(1); 
         
         scanf("%d",&q);
         
         while(q--)
         {
             scanf("%d%d",&a,&b);
         
             printf("%d\n",up[a]+up[b]-2*up[lca(a,b)]);//求树上距离。
          } 
    }
    
    return 0;
 } 

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!