【LCA|Tarjan】POJ-1330 Nearest Common Ancestors

Nearest Common Ancestors
Time Limit: 1000MS Memory Limit: 10000K

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


題意: T組數據,每組數據給出N個點和N-1條邊,每條邊先給出父結點,最後一行查詢一對結點,輸出它們的最近公共祖先。
思路: 因爲只有一個詢問,所以對輸出順序沒有要求。可以使用離線的Tarjan算法(dfs+並查集)來解決此題。
圖片來自hihoCoder

建樹完成之後,找到root,開始dfs,每dfs到一個點的時候,例如u,將其祖先1標記爲u(這個時候u結點還沒有處理完畢,我們稱之爲灰色結點,相應的,沒有遍歷到的結點稱爲白色結點,處理完畢的結點稱之爲黑色結點),然後遍歷它的兒子。如上圖的D結點,它的兒子C結點處理完畢之後,將C子樹和D結點併爲同一個集合(擁有相同的fa[]:D),然後將該集合的祖先標記爲D2

回溯到D之後(子樹C的遍歷結束後)纔算做處理完了C點,vis標記一下3。這個時候進入D的最後一個兒子B。繼續向下直到A結點,因爲A結點沒有兒子,因此直接成爲黑色結點。此時遍歷A所關聯的查詢,例如A和C的lca,這個時候C是黑色的,則可以斷定答案就是C所在集合的祖先(C上面的第一個灰色結點D,C的祖先之一)了。再例如A和B的lca,這個時候B是灰色,因此一直要回溯到D(子樹B被染成黑色),遍歷B所關聯的查詢的時候發現A是黑色結點4

綜上所述,應該可以理解到Tarjan之所以離線的原因了。它是先儲存所有的詢問,然後在一次dfs的過程中,利用並查集(維護灰色)和dfs序(維護黑色和白色)“順便”求出lca的。
代碼如下

/*
 * ID: j.sure.1
 * PROG:
 * LANG: C++
 */
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <ctime>
#include <cmath>
#include <stack>
#include <queue>
#include <vector>
#include <map>
#include <set>
#include <string>
#include <climits>
#include <iostream>
#define PB push_back
#define LL long long
using namespace std;
const int INF = 0x3f3f3f3f;
const double eps = 1e-8;
/****************************************/
const int N = 1e4 + 5, M = 2e4 + 5;
struct Edge {
    int v, next;
    Edge(){}
    Edge(int _v, int _next):
        v(_v), next(_next){}
}e[M];
int head[N], tot, fa[N];
int n, x, y, anc[N];
bool vis[N], son[N];
vector <int> Q[N];

void init()
{
    memset(head, -1, sizeof(head));
    tot = 0;
    for(int i = 1; i <= n; i++) fa[i] = i;
    memset(vis, 0, sizeof(vis));
    memset(son, 0, sizeof(son));
    memset(anc, 0, sizeof(anc));
    for(int i = 1; i <= n; i++) Q[i].clear();
}

void add(int u, int v)
{
    e[tot] = Edge(v, head[u]);
    head[u] = tot++;
}

int Find(int x)
{
    if(x != fa[x]) return fa[x] = Find(fa[x]);
    return x;
}

void Union(int x, int y)
{
    int fx = Find(x), fy = Find(y);
    if(fy != fx) fa[fy] = fx;
}

void dfs(int u)
{
    anc[u] = u;
    for(int i = head[u]; ~i; i = e[i].next) {
        int v = e[i].v;
        dfs(v);
        Union(u, v);
        anc[Find(u)] = u;
    }
    vis[u] = true;
    int sz = Q[u].size();
    for(int i = 0; i < sz; i++) {
        int v = Q[u][i];
        if(vis[v]) {
            printf("%d\n", anc[Find(v)]);
            return ;
        }
    }
}

int main()
{
#ifdef J_Sure
    freopen("000.in", "r", stdin);
    //freopen("999.out", "w", stdout);
#endif
    int T;
    scanf("%d", &T);
    while(T--) {
        scanf("%d", &n);
        int u, v;
        init();
        for(int i = 0; i < n-1; i++) {
            scanf("%d%d", &u, &v);
            add(u, v);
            son[v] = true;
        }
        int root;
        for(int i = 1; i <= n; i++) {
            if(!son[i]) {
                root = i;
                break;
            }
        }
        scanf("%d%d", &x, &y);
        Q[x].push_back(y);
        Q[y].push_back(x);
        dfs(root);
    }
    return 0;
}

  1. 祖先數組anc只是一個臨時數組,用來存放此時某結點向上的第一個灰色結點,也就是集合的fa[]。
  2. 祖先標記爲u意即染上u的顏色。此時這個集合都是灰色的。
  3. vis = 1意即回溯完畢,儘管暫時還沒有退出整個dfs,但是下面的函數僅僅是處理查詢,因此可以當作處理完畢。
  4. 因此對於每個查詢我們需要添加x-y意即y-x這兩條邊,防止遺漏。
發佈了437 篇原創文章 · 獲贊 20 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章