LCA 最近公共祖先 ( Tarjan算法詳解+模板代碼 )

歡迎訪問https://blog.csdn.net/lxt_Lucia~~

宇宙第一小仙女\(^o^)/~~萌量爆表求帶飛=≡Σ((( つ^o^)つ~ dalao們點個關注唄~~

 

 

一.LCA詳解

 

轉自:https://www.cnblogs.com/ECJTUACM-873284962/p/6613379.html

 

              首先是最近公共祖先的概念(什麼是最近公共祖先?):

    在一棵沒有環的樹上,每個節點肯定有其父親節點和祖先節點,而最近公共祖先,就是兩個節點在這棵樹上深度最大公共祖先節點

    換句話說,就是兩個點在這棵樹上距離最近的公共祖先節點

    所以LCA主要是用來處理當兩個點僅有唯一一條確定的最短路徑時的路徑。

    有人可能會問:那他本身或者其父親節點是否可以作爲祖先節點呢?

    答案是肯定的,很簡單,按照人的親戚觀念來說,你的父親也是你的祖先,而LCA還可以將自己視爲祖先節點

    舉個例子吧,如下圖所示最近公共祖先是2最近公共祖先最近公共祖先。 

    這就是最近公共祖先的基本概念了,那麼我們該如何去求這個最近公共祖先呢?

    通常初學者都會想到最簡單粗暴的一個辦法:對於每個詢問,遍歷所有的點,時間複雜度爲O(n*q),很明顯,n和q一般不會很小

    常用的求LCA的算法有:Tarjan/DFS+ST/倍增

    後兩個算法都是在線算法,也很相似,時間複雜度在O(logn)~O(nlogn)之間,我個人認爲較難理解。

    有的題目是可以用線段樹來做的,但是其代碼量很大,時間複雜度也偏高,在O(n)~O(nlogn)之間,優點在於也是簡單粗暴

    這篇博客主要是要介紹一下Tarjan算法(其實是我不會在線...)。

    什麼是Tarjan(離線)算法呢?顧名思義,就是在一次遍歷中把所有詢問一次性解決,所以其時間複雜度是O(n+q)

    Tarjan算法的優點在於相對穩定,時間複雜度也比較居中,也很容易理解。

    下面詳細介紹一下Tarjan算法的基本思路:

      1.任選一個點爲根節點,從根節點開始。

      2.遍歷該點u所有子節點v,並標記這些子節點v已被訪問過。

      3.若是v還有子節點,返回2,否則下一步。

      4.合併v到u上。

      5.尋找與當前點u有詢問關係的點v。

      6.若是v已經被訪問過了,則可以確認u和v的最近公共祖先爲v被合併到的父親節點a。

    遍歷的話需要用到dfs來遍歷(我相信來看的人都懂吧...),至於合併,最優化的方式就是利用並查集來合併兩個節點。

    下面上僞代碼:

Tarjan(u)//marge和find爲並查集合並函數和查找函數
{
    for each(u,v)    //訪問所有u子節點v
    {
        Tarjan(v);        //繼續往下遍歷
        marge(u,v);    //合併v到u上
        標記v被訪問過;
    }
    for each(u,e)    //訪問所有和u有詢問關係的e
    {
        如果e被訪問過;
        u,e的最近公共祖先爲find(e);
    }
}

             

              個人感覺這樣還是有很多人不太理解,所以我打算模擬一遍給大家看。

    建議拿着紙和筆跟着我的描述一起模擬!!

    假設我們有一組數據 9個節點 8條邊 聯通情況如下:

    1--2,1--3,2--4,2--5,3--6,5--7,5--8,7--9 即下圖所示的樹

    設我們要查找最近公共祖先的點爲9--8,4--6,7--5,5--3;

   設f[ ]數組爲並查集的父親節點數組,初始化f [ i ] = i,vis[ ]數組爲是否訪問過的數組,初始爲0; 

    下面開始模擬過程:

    取1爲根節點往下搜索發現有兩個兒子2和3;

    先搜2,發現2有兩個兒子4和5,先搜索4,發現4沒有子節點,則尋找與其有關係的點;

    發現6與4有關係,但是vis[6]=0,即6還沒被搜過,所以不操作

    發現沒有和4有詢問關係的點了,返回此前一次搜索,更新vis[4]=1

    

    表示4已經被搜完,更新f[4]=2,繼續搜5,發現5有兩個兒子7和8;

    先搜7,發現7有一個子節點9,搜索9,發現沒有子節點,尋找與其有關係的點;

    發現8和9有關係,但是vis[8]=0,即8沒被搜到過,所以不操作;

    發現沒有和9有詢問關係的點了,返回此前一次搜索,更新vis[9]=1

    表示9已經被搜完,更新f[9]=7,發現7沒有沒被搜過的子節點了,尋找與其有關係的點;

    發現5和7有關係,但是vis[5]=0,所以不操作

    發現沒有和7有關係的點了,返回此前一次搜索,更新vis[7]=1

    

    表示7已經被搜完,更新f[7]=5,繼續搜8,發現8沒有子節點,則尋找與其有關係的點;

    發現9與8有關係,此時vis[9]=1,則他們的最近公共祖先find(9)=5

      (find(9)的順序爲f[9]=7-->f[7]=5-->f[5]=5 return 5;)

    發現沒有與8有關係的點了,返回此前一次搜索,更新vis[8]=1

 

    表示8已經被搜完,更新f[8]=5,發現5沒有沒搜過的子節點了,尋找與其有關係的點;

    

    發現7和5有關係,此時vis[7]=1,所以他們的最近公共祖先find(7)=5

      (find(7)的順序爲f[7]=5-->f[5]=5 return 5;)

    又發現5和3有關係,但是vis[3]=0,所以不操作,此時5的子節點全部搜完了;

    返回此前一次搜索,更新vis[5]=1,表示5已經被搜完,更新f[5]=2

    發現2沒有未被搜完的子節點,尋找與其有關係的點;

    又發現沒有和2有關係的點,則此前一次搜索,更新vis[2]=1

    

    表示2已經被搜完,更新f[2]=1,繼續搜3,發現3有一個子節點6;

    搜索6,發現6沒有子節點,則尋找與6有關係的點,發現4和6有關係;

    此時vis[4]=1,所以它們的最近公共祖先find(4)=1;

      (find(4)的順序爲f[4]=2-->f[2]=2-->f[1]=1 return 1;)

    發現沒有與6有關係的點了,返回此前一次搜索,更新vis[6]=1,表示6已經被搜完了;

    

    更新f[6]=3,發現3沒有沒被搜過的子節點了,則尋找與3有關係的點;

    發現5和3有關係,此時vis[5]=1,則它們的最近公共祖先find(5)=1

      (find(5)的順序爲f[5]=2-->f[2]=1-->f[1]=1 return 1;)

    發現沒有和3有關係的點了,返回此前一次搜索,更新vis[3]=1

  

    更新f[3]=1,發現1沒有被搜過的子節點也沒有有關係的點,此時可以退出整個dfs了。

    經過這次dfs我們得出了所有的答案,有沒有覺得很神奇呢?是否對Tarjan算法有更深層次的理解了呢?

 

 

二.Tarjan算法模板代碼

 

轉自星博https://blog.csdn.net/Akatsuki__Itachi/article/details/81279173

 

模板題:Nearest Common Ancestors ( POJ1330

 

Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 33678   Accepted: 17093

 

 

Description

A rooted tree is a well-known data structure in computer science and engineering. An example is shown below: 

 
In the figure, each node is labeled with an integer from {1, 2,...,16}. Node 8 is the root of the tree. Node x is an ancestor of node y if node x is in the path between the root and node y. For example, node 4 is an ancestor of node 16. Node 10 is also an ancestor of node 16. As a matter of fact, nodes 8, 4, 10, and 16 are the ancestors of node 16. Remember that a node is an ancestor of itself. Nodes 8, 4, 6, and 7 are the ancestors of node 7. A node x is called a common ancestor of two different nodes y and z if node x is an ancestor of node y and an ancestor of node z. Thus, nodes 8 and 4 are the common ancestors of nodes 16 and 7. A node x is called the nearest common ancestor of nodes y and z if x is a common ancestor of y and z and nearest to y and z among their common ancestors. Hence, the nearest common ancestor of nodes 16 and 7 is node 4. Node 4 is nearer to nodes 16 and 7 than node 8 is. 

For other examples, the nearest common ancestor of nodes 2 and 3 is node 10, the nearest common ancestor of nodes 6 and 13 is node 8, and the nearest common ancestor of nodes 4 and 12 is node 4. In the last example, if y is an ancestor of z, then the nearest common ancestor of y and z is y. 

Write a program that finds the nearest common ancestor of two distinct nodes in a tree. 
 

Input

The input consists of T test cases. The number of test cases (T) is given in the first line of the input file. Each test case starts with a line containing an integer N , the number of nodes in a tree, 2<=N<=10,000. The nodes are labeled with integers 1, 2,..., N. Each of the next N -1 lines contains a pair of integers that represent an edge --the first integer is the parent node of the second integer. Note that a tree with N nodes has exactly N - 1 edges. The last line of each test case contains two distinct integers whose nearest common ancestor is to be computed.

Output

Print exactly one line for each test case. The line should contain the integer that is the nearest common ancestor.

Sample Input

2
16
1 14
8 5
10 16
5 9
4 6
8 4
4 10
1 13
6 15
10 11
6 7
10 2
16 3
8 1
16 12
16 7
5
2 3
3 4
3 1
1 5
3 5

Sample Output

4
3

Source

Taejon 2002

 


1.vector模擬鄰接表:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<vector>
#include<queue>
#define eps 1e-8
#define memset(a,v) memset(a,v,sizeof(a))
using namespace std;
typedef long long int LL;
const int MAXL(1e4);
const int INF(0x7f7f7f7f);
const int mod(1e9+7);
int dir[4][2]= {{-1,0},{1,0},{0,1},{0,-1}};
int father[MAXL+50];
bool is_root[MAXL+50];
bool vis[MAXL+50];
vector<int>v[MAXL+50];
int root;
int cx,cy;
int ans;
int Find(int x)
{
    if(x!=father[x])
        father[x]=Find(father[x]);
    return father[x];
}
 
void Join(int x,int y)
{
    int fx=Find(x),fy=Find(y);
    if(fx!=fy)
        father[fy]=fx;
}
 
void LCA(int u)
{
    for(int i=0; i<v[u].size(); i++)
    {
        int child=v[u][i];
        if(!vis[child])
        {
            LCA(child);
            Join(u,child);
            vis[child]=true;
        }
    }
    if(u==cx&&vis[cy]==true)
        ans=Find(cy);
    if(u==cy&&vis[cx]==true)
        ans=Find(cx);
 
}
 
void init()
{
    memset(is_root,true);
    memset(vis,false);
    int n;
    scanf("%d",&n);
    for(int i=0; i<=n; i++)
        v[i].clear();
    for(int i=1; i<=n; i++)
        father[i]=i;
    for(int i=1; i<n; i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        v[x].push_back(y);
        is_root[y]=false;
    }
    scanf("%d%d",&cx,&cy);
    for(int i=1; i<=n; i++)
    {
        if(is_root[i]==true)
        {
            root=i;
            break;
        }
    }
 
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        init();
        LCA(root);
        cout<<ans<<endl;
    }
}

 

 

2.鏈式前向星寫法:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<vector>
#include<queue>
#define eps 1e-8
#define memset(a,v) memset(a,v,sizeof(a))
using namespace std;
typedef long long int LL;
const int MAXL(1e6);
const int INF(0x7f7f7f7f);
const int mod(1e9+7);
int dir[4][2]= {{-1,0},{1,0},{0,1},{0,-1}};
struct node
{
    int to;
    int next;
}edge[MAXL+50];
int head[MAXL+50];
int father[MAXL+50];
bool vis[MAXL+50];
bool is_root[MAXL+50];
int n;
int cnt;
int cx,cy;
int ans;
int root;
 
 
int Find(int x)
{
    if(x!=father[x])
        father[x]=Find(father[x]);
    return father[x];
}
 
void Join(int x,int y)
{
    int fx=Find(x),fy=Find(y);
    if(fx!=fy)
        father[fy]=fx;
}
 
void add_edge(int x,int y)
{
    edge[cnt].to=y;
    edge[cnt].next=head[x];
    head[x]=cnt++;
}
 
void init()
{
    cnt=0;
    memset(head,-1);
    memset(vis,false);
    memset(is_root,true);
    scanf("%d",&n);
    for(int i=0;i<=n;i++)
        father[i]=i;
    for(int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add_edge(x,y);
        is_root[y]=false;
    }
    for(int i=1;i<=n;i++)
        if(is_root[i]==true)
            root=i;
}
 
void LCA(int u)
{
    for(int i=head[u];~i;i=edge[i].next)
    {
        int v=edge[i].to;
        LCA(v);
        Join(u,v);
        vis[v]=true;
 
    }
    if(cx==u&&vis[cy]==true)
        ans=Find(cy);
    if(cy==u&&vis[cx]==true)
        ans=Find(cx);
}
void solve()
{
    scanf("%d%d",&cx,&cy);
    LCA(root);
}
int main()
{
    int T;
    scanf("%d",&T);
    while(T--)
    {
        init();
        solve();
        cout<<ans<<endl;
    }
}

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章