zoukankan      html  css  js  c++  java
  • 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
      ", 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的数了吗?

    (也有其他写法)

  • 相关阅读:
    修改mysql密码的四种方法
    phpcms模板生成原理
    如何给虚拟主机安装phpMyAdmin
    如何修改数据库密码
    web 服务器、PHP、数据库、浏览器是如何实现动态网站的
    编写shell时,提示let/typeset:not found
    Linux下采用VI编辑器删除复制或移动多行文本内容
    BASH 学习笔记小结
    list容器的C++代码实现
    Groovy入门教程
  • 原文地址:https://www.cnblogs.com/qqq1112/p/11448165.html
Copyright © 2011-2022 走看看