POJ 1330 Nearest Common Ancestors(LCA,在线处理三种方式)

题目链接:
POJ 1330 Nearest Common Ancestors
题意:
给一个 n 点和n1条边的树,第 n 行是要查询的两个节点的最近公共祖先,输出要查询的最近公共祖先。
数据范围:n104
分析:
可以学习《挑战程序设计竞赛》 P328P331

暴力求解

记节点 v 到根的深度为depth[v]。那么如果节点 w u v 的公共祖先的话,让u向上走 depth[u]depth[w] 步,让 v 向上走depth[v]depth[w]步,都将走到 w 。因此,首先让u v 中较深的一个向上走|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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值