1. 程式人生 > >LCA (最近公共祖先)詳解 模板

LCA (最近公共祖先)詳解 模板

LCA 最近公共祖先  此部落格主要介紹 Tajan(現在只瞭解這個演算法)

需要用到很多鋪墊  比如前向星了  鏈式前向星了  並查集了   下面我們一一來介紹一下

首先看一下前向星 

前向星是一種資料結構,以儲存邊的方式來儲存圖  用到兩個陣列 len[i]是來表示以i為起點邊的個數  head[i]是表示i點第一條邊的下標

用前向星時要先按照起點大小排序  會增加很多複雜度  所以引申了鏈式前向星

鏈式前向星

首先需要定義一個結構體

struct EDGE{
	int next;   //下一條邊的儲存下標(初值為-1) 
	int to;     //這條邊的終點 
	int w;      //權值 
}edge[500010];

1 結構體陣列edge存邊,edge[i]表示第i條邊,

2 head[i]存以i為起點的第一條邊(在edge中的下標)

主要程式碼

typedef long long ll;
void add(ll u, ll v, ll w) {  //起點u, 終點v, 權值w 
	//cnt為邊的計數,從1開始計 
	edge[cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].w = w;	
	head[u] = cnt++;    //第一條邊為當前邊 
} 

舉個例子 輸入起點  終點(先不考慮權值)

1 2
2 3
3 4
1 3
4 1
1 5
4 5

按照程式碼走一遍  會出現下表

下標 to next head
1 2 -1 head[1]=1
2 3 -1 head[2]=2
3 4 -1

head[3]=3;

4 3 1 head[1]=4
5 1 -1 head[4]=5
6 5 4 head[1]=6
7 5 5 head[4]=7

遍歷以u為起點的邊

for(int i=head[u];i!=-1;i=edge[i].next)

下面一個簡單輸出的程式碼  如有問題  歡迎私信

輸入

4 5
1 2 1
2 3 3
2 4 2
4 3 2
1 4 2

2

輸出

Start: 2
End: 4
W: 2

Start: 2
End: 3
W: 3

程式碼

#include <iostream>
using namespace std;

#define MAXM 500010
#define MAXN 10010

struct EDGE
{
    int next;   //下一條邊的儲存下標
    int to;     //這條邊的終點
    int w;      //權值
}edge[MAXM];

int n, m, cnt;
int head[MAXN];  //head[i]表示以i為起點的第一條邊

void Add(int u, int v, int w)    //起點u, 終點v, 權值w
{
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    edge[cnt].w = w;
    head[u] = cnt++;    //第一條邊為當前邊
}
void Print()
{
    int st;
    cout << "Begin with[Please Input]: \n";
    cin >> u;
    for(int i=head[u]; i!=0; i=edge[i].next)  //i開始為第一條邊,每次指向下一條(以-1為結束標誌)
    {
        cout << "Start: " << u << endl;
        cout << "End: " << edge[i].to << endl;
        cout << "W: " << edge[i].w << endl << endl;
    }
}

int main()
{
    int s, t, w;
    cin  >>n >> m;//n為幾個點 m為幾條邊
    for(int i=1; i<=m; i++)
    {
        cin >> s >> t >> w;
        Add(s, t, w);
    }
    Print();
    return 0;
}

鏈式前向星介紹完啦  要想用lca求最近公共祖先還差一個並查集

並查集  是一種非常精巧而實用的資料結構,它主要用於處理一些不相交集合的合併問題

並查集的基本操作有兩個:

  merge(x, y):把元素x 和元素y 所在的集合合併,要求x 和y 所在的集合不相交,如果相交則不合並。

  find(x):找到元素x 所在的集合的代表,該操作也可以用於判斷兩個元素是否位於同一個集合,只要將它們各自的代表比較一下就可以了。

#include<iostream>
#include<stack>
#include<queue>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
int pre[1010];
int rank[1010];
int find(int x)//使用遞迴寫find函式,同時有路徑壓縮
{
    if(x!=pre[x])
        pre[x]=find(pre[x]);
    return pre[x];
}
void merge(int x,int y)
{
    x = find(x);
    y = find(y);
    if(rank[x]<rank[y])//rank為樹的高度,這裡為按秩合併
        pre[x] = y;
    else
    {
        pre[y] = x;
        if(rank[x]==rank[y])
            rank[x]++;
    }
}
int main()
{
    int m,n;
    while(1)
    {
        cin>>n;
        if(n==0) break;
        cin>>m;
        for(int i=1; i<=n; i++)//初始化陣列
        {
            pre[i]=i;
            rank[i]=0;
        }
        int x,y;
        for(int i=0; i<m; i++)
        {
            cin>>x>>y;
            merge(x,y);
        }
        int sum=0;
        for(int i=1; i<=n; i++)
        {
            if(pre[i]==i)
                sum++;
        }
        cout<<sum-1<<endl;
    }
    return 0;
} 

下面進入正題  tajian(離線)求最近公共祖先

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

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

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

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

      4.合併v到u上。

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

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

題目   Nearest Common Ancestors

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

 鏈式前向星寫法

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

上面連結中還有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);  //回溯的時候更新father用的
            //例如把題目中的[9]更新為5
            vis[child]=true;  //訪問過的點vis陣列為true
        }
    }
    if(u==cx&&vis[cy]==true) //若和u有關係的點被訪問過,
        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;   //y點有入度,所以y點不是根結點
    }
    scanf("%d%d",&cx,&cy);  //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;
    }
}