最近公共祖先:LCA及其用倍增實現 +POJ1986


 

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;
 } 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM