P3379 【模板】最近公共祖先(LCA)

解题思路

本题是经典的最近公共祖先(LCA)问题,使用倍增算法进行高效求解。倍增算法通过预处理每个节点的各层祖先信息,使得查询时能够快速跳跃查找,达到O(logN)的时间复杂度。

关键步骤

  1. 预处理阶段

    • 使用DFS遍历树结构,记录每个节点的深度和直接父节点

    • 预处理每个节点的2^i级祖先(倍增思想)

  2. 查询阶段

    • 先将两个节点调整到同一深度

    • 然后同时向上跳跃查找,直到找到公共祖先

#include<bits/stdc++.h>
using namespace std;
const int N = 5e5 + 10;
int n,m;
int f[N][26],dep[N]; //f[x][i]:x往上跳2^i步到达的位置 
vector<int> g[N];
void dfs(int x,int fa) //x是当前节点编号,fa是x的父节点 
{
    dep[x] = dep[fa] + 1; //x等于父节点fa的深度 + 1
    f[x][0] = fa; //x往上跳1步到父节点
    //那么x除了往上跳1步,也可能跳2^1,2^2,2^3,...,2^20
    for(int i = 1; i <= 20; i++) //注意,次数i代表2的次方数,但是不从0开始,因为2^0已经处理了
    {
        int y = f[x][i - 1];
        f[x][i] = f[y][i - 1]; //先跳2^i-1步到中转站y,再跳上剩下的2^i-1步,原理:2^i = 2^(i-1) + 2^(i-1) 
    } 
    for(int i = 0; i < g[x].size(); i++)
    {
        int y = g[x][i]; //获取x的邻接节点
        if(y != fa) dfs(y,x); //如果y不是x的父节点,那么继续去y建树,x为y的父节点 
    }
}
int lca(int x,int y)
{
    if(dep[x] < dep[y]) swap(x,y); //保证x深度较深 
    for(int i = 20; i >= 0; i--) //处理让x往上跳到和y深度相同
    {
        int fx = f[x][i]; //找到fx是x往上跳了2^i步的祖先
        if(dep[fx] >= dep[y]) //如果x的祖先fx深度还是比y要大,那么说明要继续 往上跳
            x = fx; //更新x变成祖先fx,然后继续for循环的往上跳 
    }
    if(x == y) return x; //如果跳完发现x==y,说明当前的x是最近公共祖先lca
    //不等于说明x和y仅是深度相同,需要一起往上跳才能找到lca
    for(int i = 20; i >= 0; i--)
    {
        int fx = f[x][i],fy = f[y][i];
        if(fx != fy) //如果往上跳了2^i步还不相同,那么更新x,y继续for循环往上跳 
            x = fx,y = fy;
    } 
    return f[x][0]; //只需要再往上跳1步就能到达祖先 
}
int main()
{
    int rt;
    cin >> n >> m >> rt;
    for(int i = 1; i < n; i++) //题面说了是树,所以只有n-1条边 
    {
        int x,y; scanf("%d%d",&x,&y);
        g[x].push_back(y);
        g[y].push_back(x);
    }
    dfs(rt,0); //边存储完后就要dfs建树 build
    while(m--)
    {
        int x,y; scanf("%d%d",&x,&y);
        printf("%d\n",lca(x,y));
    }
    return 0;
}

 

 

倍增法预处理和查询方向差异的解释

在倍增法求LCA时,预处理阶段(DFS)和查询阶段确实采用了相反的方向(预处理从小到大,查询从大到小),这是由倍增算法的特性决定的。

为什么预处理从小到大而查询从大到小?

预处理阶段(从小到大)

  1. 依赖关系:计算f[x][i](x的2^i级祖先)需要先知道f[x][i-1](x的2^(i-1)级祖先),因为f[x][i] = f[f[x][i-1]][i-1]。这种递推关系决定了必须从小到大计算。

  2. 基础构建:类似于动态规划,需要先计算小规模的子问题,再组合成大规模的解。

查询阶段(从大到小)

  1. 贪心策略:从最大的可能步长开始尝试,可以快速缩小范围。如果从小的步长开始,可能需要多次尝试。

  2. 二进制分解:类似于把一个数分解为2的幂次和,我们从高次幂开始尝试更高效。

记忆方法

形象比喻

  • 预处理:像搭积木,必须从底层开始一层层往上搭(从小到大)

  • 查询:像下楼梯,从最高处一步步往下走更安全(从大到小)

二进制视角

  • 预处理:构建二进制表(类似建乘法表)

  • 查询:二进制分解(类似把一个数表示为二进制)

实用记忆口诀

"预处理从小到大建表,查询从大到小跳跃"

具体例子说明

假设我们要找深度差为13的两个节点的LCA:

  • 预处理:计算了1,2,4,8,16,...级祖先

  • 查询:

    1. 尝试16(太大,跳过)

    2. 尝试8(13-8=5)

    3. 尝试4(5-4=1)

    4. 尝试2(1<2,跳过)

    5. 尝试1(1-1=0)

这样只需3步(8+4+1)就完成了调整,如果从小到大尝试会需要更多步骤。

为什么不能统一方向?

如果查询也从小到大:

  • 可能需要多次回退(比如先+1,发现不够又+2,然后又需要+4...)

  • 效率降低,最坏情况下需要O(logN)步变成O(N)步

而从大到小可以保证在O(logN)步内完成。

posted @ 2025-04-29 16:50  CRt0729  阅读(90)  评论(0)    收藏  举报