POJ 1330 Nearest Common Ancestors 最近公共祖先 欧拉序列RMQ

本文介绍了一种利用欧拉序列和RMQ(区间最值查询)解决最近公共祖先(LCA)问题的方法。通过DFS遍历构建欧拉序列,并使用ST表进行区间查询,高效解决了树上节点间的LCA问题。

题意:给出一个树,让你求出两个节点的最近公共祖先。

思路:这里直接转换成了欧拉序列的RMQ问题。

          欧拉序列是按照DFS顺序遍历整棵数形成的序列。 如下图的欧拉序列就是121343531.

可以发现,在欧拉序列中,一段子串就对应了一条路径,而且是连续不断的路径。

而对于两个点的公共祖先,就是连接两个点的路径中深度最小的点。而最小且是连续区间,正好是ST表对应的RMQ。

所以用ST表解决公共祖先的问题的方法如下:

1.dfs得到欧拉序列,同时记录每个节点最先在欧拉序列中出现的下标在first数组中。

2.询问任意两点u,v的最近公共祖先,就等价于求欧拉序列中,区间[first[u],first[v]]中深度最低的节点的编号。

代码如下:

#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;

const int MAX = 10010;

int head[MAX],nxt[MAX<<2],to[MAX<<2],len[MAX<<2];
int tot,tot1;
int fath[MAX];
int dep[MAX],dfsnum[MAX<<2],first[MAX],p[MAX<<2],dp[MAX<<2][20];


void init()
{
    tot = 0; tot1 = 1;
    memset(head,-1,sizeof(head));
    memset(fath,-1,sizeof(fath));
}

void addedge(int u, int v)
{
    to[tot] = v;
    nxt[tot] = head[u], head[u] = tot++;
    to[tot] = u;
    nxt[tot] = head[v], head[v] = tot++;
}

void dfs(int u, int fa,int d)
{
    dep[u] = d;
    first[u] = tot1;
    dfsnum[tot1] = u;tot1++;
    for(int i = head[u]; ~i; i = nxt[i]){
        int v = to[i];
        if(v == fa) continue;
        dfs(v,u,d+1);
        dfsnum[tot1] = u;tot1++;
    }
}

int lca(int u, int v)
{
    p[0] = -1;
    for(int i = 1; i < tot1; ++i)
        p[i] = i & i - 1? p[i-1]:p[i-1] + 1;
    for(int i = 1;i < tot1; ++i) dp[i][0] = dfsnum[i];
    for(int j = 1; j <= p[tot1 - 1]; ++j)
        for(int i = 1; i + (1<<j) - 1 < tot1; ++i)
            if(dep[dp[i][j-1]] < dep[dp[i + (1<<j-1)][j-1]])
                dp[i][j] = dp[i][j-1];
            else
                dp[i][j] = dp[i + (1<<j-1)][j-1];
    u = first[u], v = first[v];
    int l = min(u,v), r = max(u,v);
    int k = p[r - l + 1];
    return dep[dp[l][k]] < dep[dp[r - (1<<k) + 1][k]]?dp[l][k]:dp[r - (1<<k)+1][k];
}

int main(void)
{
    //freopen("input.txt","r",stdin);
    int T,N,u,v;
    scanf("%d",&T);
    while(T--){
        init();
        scanf("%d",&N);
        for(int i = 0; i < N - 1; ++i){
            scanf("%d %d",&u,&v);
            addedge(u,v);
            fath[v] = u;
        }
        int root = 1;
        while(fath[root] != -1)
            root = fath[root];
        dfs(root,-1,0);
        scanf("%d%d",&u,&v);
        printf("%d\n",lca(u,v));
    }
    return 0;
}

04-28
### POJ 1330 题解 POJ 1330 是一道经典的最近公共祖先 (Least Common Ancestor, LCA) 问题。该题的核心在于如何高效地求解两节点之间的最近公共祖先。 #### 倍增算法简介 倍增方法是一种高效的在线求解 LCA 的方式,其时间复杂度为 \(O((n+q)\log n)\),其中 \(n\) 表示树中的节点数量,\(q\) 表示查询次数。此方法通过预处理的方式记录每个节点向上跳跃若干步后的父节点位置,从而加速查询过程。 具体来说,定义数组 `fa[i][j]` 表示从节点 \(i\) 向上跳 \(2^j\) 步所到达的节点编号。为了支持这一操作,我们需要满足如下递推关系: \[ \text{fa}[i][j] = \begin{cases} \text{parent}(i), & j = 0 \\ \text{fa}[\text{fa}[i][j-1]][j-1], & j > 0 \end{cases} \] 这种预处理可以通过深度优先搜索 (DFS) 完成,在 DFS 过程中同时计算每个节点的深度以及它们的倍增父节点表。 #### 查询阶段 在实际查询过程中,假设我们要查找节点 \(a\) 和 \(b\) 的最近公共祖先,则分为以下几个部分: 1. **调整深度一致** 如果当前两个节点的深度不相同,则将较深的节点不断向上提升至两者深度相等的位置。这一步利用了倍增数组 `fa[i][j]` 来快速跳跃多个层次。 2. **同步爬升** 当两个节点处于同一深度时,让它们逐步向上移动直至相遇于某个共同祖先处。为了避免逐层遍历带来的效率低下问题,同样采用倍增策略按指数级跳跃。 最终返回的结果即为最后一次未分叉前的状态作为答案。 以下是基于以上逻辑的一个简单实现代码片段: ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 5e4 + 5; vector<int> adj[MAXN]; int depth_[MAXN]; // 存储每个结点的深度 int fa[MAXN][20]; // 记录每个结点向上跳2^k步的父亲 void dfs(int u, int parent){ depth_[u]=depth_[parent]+1; fa[u][0]=parent; for(auto v : adj[u]){ if(v != parent){ dfs(v,u); } } } // 初始化fa[][]表格 void preprocess(int N){ memset(fa,-1,sizeof(fa)); for(int k=1;k<20;k++){ for(int i=1;i<=N;i++){ if(fa[i][k-1]!=-1){ fa[i][k]=fa[fa[i][k-1]][k-1]; } } } } int get_lca(int a,int b){ if(depth_[a]<depth_[b]) swap(a,b); // 将a提到和b相同的高度 for(int k=19;k>=0;k--){ if(fa[a][k]!=-1 && depth_[fa[a][k]] >= depth_[b]){ a=fa[a][k]; } } if(a==b)return a; // 同步爬升 for(int k=19;k>=0;k--){ if(fa[a][k]!=-1 && fa[b][k]!=-1 && fa[a][k]!=fa[b][k]){ a=fa[a][k]; b=fa[b][k]; } } return fa[a][0]; } ``` 上述程序实现了完整的倍增法用于解决LCA问题的功能模块化设计[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值