- 例题 :
https://www.luogu.org/problem/P3379
- 算法:
首先我们能想出一种暴力算法:先把深度高的点跳到和深度低的点的同一层,然后他们俩一起往上跳,如果两个点相遇了,当前点就是他们的最近公共祖先。但可惜会超时,于是我们考虑一下优化。
- 优化:
我们可以把跳的过程优化一下,原来是一个一个往上跳,速度太慢,我们就可以用二进制优化一下,2的n次方这样往上跳。已知fa[u][i]表示u的第2的i次方个祖先(fa[u][0]就是u的父亲)。时间复杂度O(n log n)
- 流程:
- 我们先预处理出每个点的2的i(0 <= i <= 上限(上限直接用20也行))次方的父亲和每个点的深度
- 然后将两个点中深度最深的点往上跳,直到与另一点在同样的深度
- 进行特判是否两个点重合了(重合了就不用向上跳了)
- 然后两个点一起向上跳2的i(i从大到小,因为这样保证有正确性)次方(前提是不能重合),最后他们会跳到他们的LCA的儿子上(自行理解)
- 最后返回其中一个点的父亲就是他们两个的LCA
- 代码:
1 //maxn都为上限,都可以用20代替(20就够了) 2 #include <bits/stdc++.h> 3 #define INF 0x3f3f3f3f 4 using namespace std; 5 int n, m, root, head[500001], num, depth[500001], fa[500001][101]; 6 struct node 7 { 8 int next, to; 9 }stu[1000001]; 10 inline void add(int x, int y)//存树 11 { 12 stu[++num].next = head[x]; 13 stu[num].to = y; 14 head[x] = num; 15 return; 16 } 17 inline void dfs(int u, int father)//预处理 18 { 19 fa[u][0] = father;//2的0次方等于1所以就是他的父亲 20 depth[u] = depth[father] + 1;//深度 = 他的父亲的深度 + 1 21 int maxn = ceil(log(depth[u]) / log(2)); 22 for(register int i = 1; i <= maxn; ++i)//2的0次方已经处理过了,所以i从1开始 23 { 24 fa[u][i] = fa[fa[u][i - 1]][i - 1];//dp方程(①) 25 } 26 for(register int i = head[u]; i; i = stu[i].next) 27 { 28 int k = stu[i].to; 29 if(k != father)//注意判断到叶节点时返回它的父亲的情况 30 { 31 dfs(k, u); 32 } 33 } 34 return; 35 } 36 inline void up(int &u/*注意,由于要改变x的值(要跳到与y同样深度的地方),那么不要忘了加&*/, int step)//把深度深的点跳到深度浅的点同样的深度 37 { 38 int maxn = ceil(log(n) / log(2)); 39 for(register int i = 0; i <= maxn; ++i) 40 { 41 if(step & (1 << i))//(②) 42 { 43 u = fa[u][i]; 44 } 45 } 46 return; 47 } 48 inline int lca(int x, int y)//求LCA的函数 49 { 50 if(depth[x] < depth[y])//把x始终变为深度最深的点 51 { 52 swap(x, y); 53 } 54 up(x, depth[x] - depth[y]);//x是当前要变得点,后面的是深度差(步数,及需要多少步) 55 if(x == y)//如果跳完之后x与y重合了,那么最近公共祖先就是x了(y也行) 56 { 57 return x; 58 } 59 int maxn = ceil(log(depth[x]) / log(2)); 60 for(register int i = maxn; i >= 0; --i)//从大到小可以快速找出并省去很多冗余的操作 61 { 62 if(fa[x][i] != fa[y][i])//如果两个点重合了,那么他们的父亲肯定就相同了 63 { 64 x = fa[x][i];//否则就向上跳 65 y = fa[y][i]; 66 } 67 } 68 return fa[x][0];//最后肯定他们的父亲就是最近公共祖先 69 } 70 signed main() 71 { 72 scanf("%d %d %d", &n, &m, &root); 73 for(register int i = 1, x, y; i < n; ++i)//n - 1条边 74 { 75 scanf("%d %d", &x, &y); 76 add(x, y);//存树是双向边 77 add(y, x); 78 } 79 dfs(root, root);//根节点的父亲是它本身 80 for(register int i = 1, x, y; i <= m; ++i) 81 { 82 scanf("%d %d", &x, &y); 83 printf("%d\n", lca(x, y));//直接输出 84 } 85 return 0; 86 }
- 关于代码部分重要地方讲解:
①:首先我们的循环遍历顺序是从1 ~ maxn的,所以fa[u][i - 1]肯定是已知的;而我们的树的遍历顺序是从根节点到下的,所以fa[u][i - 1](它是u的祖先所以肯定已经遍历过了)的i - 1次方肯定也是已知的,所以fa[fa[u][i - 1]][i - 1]就是fa[u][i](直白点就是2的i - 1次方 + 2的i - 1次方 = 2的i次方)
从蓝线跳到2的2次方祖先 = 先从橙线跳2的1次方祖先再跳一次
②:首先任何一个数都可以用二进制表示,而任何一个数都可以被2的整次幂分解(当前位是1的话就是2的当前位数次幂,但如果是0的话就不用管)。为什么要想到这一点?是因为我们已经已知了当前点的2的i次幂的祖先,所以我们可以利用这个一个一个往上跳,知道把当前深度差跳为止。
代码解释一下:&表示当前数与另一个数的与运算如果当前位都是1,那么得1,反之就是0(我们可以利用这一点来判断当前位是否为1,可以把它&一下一个除当前位是1以外的数都是0的数就可以了),<< 表示左移(即把百位变为千位,千位变为万位……),那么我们把1左移i位不就是一个除当前位是1以外的数都是0的数了吗?
(也有其他写法)