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号
浙公网安备 33010602011771号