P3379 【模板】最近公共祖先(LCA)
解题思路
本题是经典的最近公共祖先(LCA)问题,使用倍增算法进行高效求解。倍增算法通过预处理每个节点的各层祖先信息,使得查询时能够快速跳跃查找,达到O(logN)的时间复杂度。
关键步骤
-
预处理阶段:
-
使用DFS遍历树结构,记录每个节点的深度和直接父节点
-
预处理每个节点的2^i级祖先(倍增思想)
-
-
查询阶段:
-
先将两个节点调整到同一深度
-
然后同时向上跳跃查找,直到找到公共祖先
-
#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)和查询阶段确实采用了相反的方向(预处理从小到大,查询从大到小),这是由倍增算法的特性决定的。
为什么预处理从小到大而查询从大到小?
预处理阶段(从小到大)
-
依赖关系:计算
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的幂次和,我们从高次幂开始尝试更高效。
记忆方法
形象比喻
-
预处理:像搭积木,必须从底层开始一层层往上搭(从小到大)
-
查询:像下楼梯,从最高处一步步往下走更安全(从大到小)
二进制视角
-
预处理:构建二进制表(类似建乘法表)
-
查询:二进制分解(类似把一个数表示为二进制)
实用记忆口诀
"预处理从小到大建表,查询从大到小跳跃"
具体例子说明
假设我们要找深度差为13的两个节点的LCA:
-
预处理:计算了1,2,4,8,16,...级祖先
-
查询:
-
尝试16(太大,跳过)
-
尝试8(13-8=5)
-
尝试4(5-4=1)
-
尝试2(1<2,跳过)
-
尝试1(1-1=0)
-
这样只需3步(8+4+1)就完成了调整,如果从小到大尝试会需要更多步骤。
为什么不能统一方向?
如果查询也从小到大:
-
可能需要多次回退(比如先+1,发现不够又+2,然后又需要+4...)
-
效率降低,最坏情况下需要O(logN)步变成O(N)步
而从大到小可以保证在O(logN)步内完成。

浙公网安备 33010602011771号