POJ 1330 Nearest Common Ancestors(LCA,在線處理三種方式)

題目鏈接:
POJ 1330 Nearest Common Ancestors
題意:
給一個n 點和n1 條邊的樹,第n 行是要查詢的兩個節點的最近公共祖先,輸出要查詢的最近公共祖先。
數據範圍:n104
分析:
可以學習《挑戰程序設計競賽》P328P331

暴力求解

記節點v 到根的深度爲depth[v] 。那麼如果節點wuv 的公共祖先的話,讓u 向上走depth[u]depth[w] 步,讓v 向上走depth[v]depth[w] 步,都將走到w 。因此,首先讓uv 中較深的一個向上走|depth[u]depth[v]| 步,再一起一步步向上走,直到走到同一個節點即是LCA
時間複雜度:dfs :O(n) ,單次查詢:O(n)

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAX_N = 10010;

int T, n, total, root;
int head[MAX_N], in[MAX_N], fa[MAX_N], depth[MAX_N];

struct Edge {
    int v, next;
}edge[MAX_N];

void AddEdge(int u, int v) 
{
    edge[total].v = v;
    edge[total].next = head[u];
    head[u] = total++;
}

void dfs(int u, int p, int cur)
{
    fa[u] = p;
    depth[u] = cur;
    for (int i = head[u]; i != -1; i = edge[i].next) {
        dfs (edge[i].v, u, cur + 1);
    }
}

int LCA(int u, int v)
{
    while (depth[u] > depth[v]) u = fa[u];
    while (depth[v] > depth[u]) v = fa[v];
    while (u != v) {
        u = fa[u];
        v = fa[v];
    }
    return u;
}

int main()
{
    scanf("%d", &T);
    while (T--) {
        total = 0;
        memset(head, -1, sizeof (head));
        memset(in, 0, sizeof (in));
        scanf("%d", &n);
        int u, v;
        for (int i = 1; i <= n; ++i) {
            scanf("%d%d", &u, &v);
            if (i != n) {
                AddEdge(u, v);
                in[v]++;
            }
        }
        for (int i = 1; i <= n; ++i) {
            if (in[i] == 0) {
                root = i;
                break;
            }
        }
        dfs (root, -1, 0);
        printf("%d\n", LCA(u, v));
    }
    return 0;
}

二分搜索

首先對於任意節點v ,利用其父節點parent[v][1] 信息,可以通過parent[v][2]=parent[parent[v][1]][1] 得到其向上走兩步所到的節點,再利用這一信息,又可以通過parent[v][4]=parent[parent[v][2]][2] 得到其向上走四步所到的節點。依此類推,就能夠得到其向上走2k 步所到的節點parent[v][k] 。有了k=floor(log2(n)) 以內的所有信息後,就可以二分搜索了。
預處理parent[v][k]O(nlogn) ,單次查詢:O(logn)

#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <math.h>
using namespace std;
typedef long long ll;
const int MAX_N = 10010;
const int MAX_LOG_N = 20; // MAX_LOG_N = log2(MAX_N)

int T, n, total, root;
int head[MAX_N], in[MAX_N], depth[MAX_N], fa[MAX_N][MAX_LOG_N];

struct Edge {
    int v, next;
}edge[MAX_N];

void AddEdge(int u, int v)
{
    edge[total].v = v;
    edge[total].next = head[u];
    head[u] = total++;
}

void dfs(int u, int p, int d)
{ // 獲取每個節點的深度和直接父親
    depth[u] = d;
    fa[u][0] = p;
    for (int i = head[u]; i != -1; i = edge[i].next) {
        dfs(edge[i].v, u, d + 1);
    }
}

void RMQ() // 時間複雜度: O(nlog(n))
{ // 倍增處理每個節點的祖先
    for (int k = 0; k + 1 < MAX_LOG_N; ++k) { 
        for (int i = 1; i <= n; ++i) {
            if (fa[i][k] == -1) fa[i][k + 1] = -1;
            else fa[i][k + 1] = fa[fa[i][k]][k];
        }
    }   
}

int LCA(int u, int v) // 時間複雜度:O(log(n))
{ 
    // 先把兩個節點提到同一深度
    if (depth[u] > depth[v]) swap(u, v);
    for (int k = 0; k < MAX_LOG_N; ++k) {
        if (((depth[v] - depth[u]) >> k) & 1) {
            v = fa[v][k];
        }
    }
    if (u == v) return v;
    // 二分搜索計算LCA
    for (int k = MAX_LOG_N - 1; k >= 0; --k) {
        if (fa[u][k] != fa[v][k]) {
            u = fa[u][k];
            v = fa[v][k];
        }
    }
    return fa[u][0];
}

int main()
{
    scanf("%d", &T);
    while (T--) {
        memset(head, -1, sizeof (head));
        memset(in, 0, sizeof(in));
        memset(depth, 0, sizeof(depth));
        memset(fa, -1, sizeof(fa));
        total = 0;
        scanf("%d", &n);
        int u, v;
        for (int i = 1; i <= n; ++i) {
            scanf("%d%d", &u, &v);
            if (i != n) {
                AddEdge(u, v);
                in[v]++;
            }
        }
        // 找到根節點
        for (int i = 1; i <= n; ++i) { 
            if (in[i] == 0) {
                root = i;
                break;
            }
        }
        dfs(root, -1, 0);
        RMQ();
        printf("%d\n", LCA(u, v));
    }
    return 0;
}

基於RMQ的算法

其實上面的二分搜索算法已經有了RMQ 的意思了。
將樹轉爲從根DFS 標號後得到的序列處理。
首先按從根DFS 訪問的順序得到頂點序列vis[i] 和對應的深度depth[i] 。對於每個頂點v ,記其在vis[] 中首次出現的下標爲id[v] 。而LCA(u,v) 就是訪問u 之後到訪問v 之前所經過的節點中離根最近的那個,假設id[u]id[v] ,那麼有:

LCA(u,v)=vis[k],kid[u]iid[v]depth[i]i

預處理:O(nlogn) ,單次查詢:O(1)
#include <stdio.h>
#include <string.h>
#include <math.h>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAX_N = 10010;

int T, n, total;
int head[MAX_N], vis[MAX_N * 2], id[MAX_N], depth[MAX_N * 2], dp[MAX_N * 2][20], in[MAX_N];

struct Edge {
    int v, next;
} edge[MAX_N * 2];

void AddEdge (int u, int v)
{
    edge[total].v = v;
    edge[total].next = head[u];
    head[u] = total++;
}

void dfs(int u, int p, int d, int& k)
{
    vis[k] = u; // dfs訪問順序
    id[u] = k; // 節點在vis中首次出現的下標
    depth[k++] = d; // 節點對應的深度
    for (int i = head[u]; i != -1; i = edge[i].next) {
        int v = edge[i].v;
        if (v == p) continue;
        dfs(v, u, d + 1, k); // 遞歸訪問子節點
        vis[k] = u; // 再次訪問
        depth[k++] = d; // 標記vis的深度
    }
}

void RMQ(int root) // 處理區間深度最小值,保存最小值的下標
{ // 就是區間左右端點最近公共祖先的下標,即:vs[Min] = LCA
    int k = 0;
    dfs(root, -1, 0, k);
    // printf ("k = %d\n", k);
    int m = k; // m = 2 * n - 1
    int e = (int)(log2(m + 1.0)); // 區間長度m + 1
    for (int i = 0; i < m; ++i) dp[i][0] = i;
    for (int j = 1; j <= e; ++j) {
        for (int i = 0; i + (1 << j) - 1 < m; ++i) {
            int nxt = i + (1 << (j - 1));
            if (depth[dp[i][j - 1]] < depth[dp[nxt][j - 1]]) {
                dp[i][j] = dp[i][j - 1];
            } else {
                dp[i][j] = dp[nxt][j - 1];
            }
        }
    }
}

int LCA(int u, int v)
{
    int left = min(id[u], id[v]), right = max(id[u], id[v]);
    int k = (int)(log2(right - left + 1.0)); // 區間長度,注意用log2!
    int pos, nxt = right - (1 << k) + 1; // nxt 分界點
    if (depth[dp[left][k]] < depth[dp[nxt][k]]) {
        pos = dp[left][k];
    } else {
        pos = dp[nxt][k];
    }
    return vis[pos];
}

int main()
{
    scanf("%d", &T);
    while (T--) {
        memset(head, -1, sizeof(head));
        memset(in, 0, sizeof(in));
        total = 0;
        int u, v;
        scanf("%d", &n);
        for (int i = 1; i <= n; ++i) {
            scanf("%d%d", &u, &v);
            if (i < n) {
                AddEdge(u, v);
                in[v]++;
            }
        }
        int root;
        for (int i = 1; i <= n; ++i) {
            if (in[i] == 0) {
                root = i;
                break;
            }
        }
        RMQ(root);
        printf("%d\n", LCA(u, v));
    }
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章