LCA树上倍增算法

   求LCA的算法——树上倍增

  • 例题 :

https://www.luogu.org/problem/P3379

  • 算法:

首先我们能想出一种暴力算法:先把深度高的点跳到和深度低的点的同一层,然后他们俩一起往上跳,如果两个点相遇了,当前点就是他们的最近公共祖先。但可惜会超时,于是我们考虑一下优化。

  • 优化:

我们可以把跳的过程优化一下,原来是一个一个往上跳,速度太慢,我们就可以用二进制优化一下,2的n次方这样往上跳。已知fa[u][i]表示u的第2的i次方个祖先(fa[u][0]就是u的父亲)。时间复杂度O(n log n)

  • 流程:
  1. 我们先预处理出每个点的2的i(0 <= i <= 上限(上限直接用20也行))次方的父亲和每个点的深度
  2. 然后将两个点中深度最深的点往上跳,直到与另一点在同样的深度
  3. 进行特判是否两个点重合了(重合了就不用向上跳了)
  4. 然后两个点一起向上跳2的i(i从大到小,因为这样保证有正确性)次方(前提是不能重合),最后他们会跳到他们的LCA的儿子上(自行理解)
  5. 最后返回其中一个点的父亲就是他们两个的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的数了吗?

(也有其他写法)

posted @ 2019-09-05 21:48  louis_11  阅读(484)  评论(0编辑  收藏  举报