• 数据结构--树链剖分准备之LCA


    有关LCA的模板题    传送门

    题目描述

    如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。

    输入输出格式

    输入格式:

     

    第一行包含三个正整数N、M、S,分别表示树的结点个数、询问的个数和树根结点的序号。

    接下来N-1行每行包含两个正整数x、y,表示x结点和y结点之间有一条直接连接的边(数据保证可以构成树)。

    接下来M行每行包含两个正整数a、b,表示询问a结点和b结点的最近公共祖先。

     

    输出格式:

     

    输出包含M行,每行包含一个正整数,依次为每一个询问的结果。

     

    输入输出样例

    输入样例#1:
    5 5 4
    3 1
    2 4
    5 1
    1 4
    2 4
    3 2
    3 5
    1 2
    4 5
    输出样例#1:
    4
    4
    1
    4
    4
    

    说明

    时空限制:1000ms,128M

    数据规模:

    对于30%的数据:N<=10,M<=10

    对于70%的数据:N<=10000,M<=10000

    对于100%的数据:N<=500000,M<=500000

    样例说明:

    该树结构如下:

    第一次询问:2、4的最近公共祖先,故为4。

    第二次询问:3、2的最近公共祖先,故为4。

    第三次询问:3、5的最近公共祖先,故为1。

    第四次询问:1、2的最近公共祖先,故为4。

    第五次询问:4、5的最近公共祖先,故为4。

    故输出依次为4、4、1、4、4。

     一道在大佬眼中水的不行的题目,然而对于我这样的小白来说还是很有难度,所以就让我们从0开始。

    度娘的解释:

    LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先。 ———来自百度百科

    说真的看不懂也没什么,度娘这个东西纯属科普。

    正常的开始

    首先,我们先来看一张图

    这是一棵树,我相信是个人都能看出来,在图中我们可以很清楚的看出17号节点和8号节点的LCA就是3号节点,而9和7的LCA就是7;

    大致知道了什么是LCA后,下面我们就来看看LCA怎么求 QWQ

    • 暴力算法

          暴力这个东西是个好东西,但是dalao的暴力和你的暴力不是一个暴力,人家会加一些神仙优化,但你就不会。。。如果你还头铁的话,我们来看下复杂度。以17和18为例,如果要求LCA,我们会打暴力让他一个一个往上爬,在这两个点相遇时就停止,手动跑一下,就是

          17号点:17-->14-->10-->7--3

          18号点:18-->16-->12-->8-->5-->3

           虽然最终结果是3没有错,但这样打你也许会听到旁边dalao“你不T谁T”的嘲讽,所以,为了营造良好的机房氛围我们在确定思路后,就要开始优化了,于是就有了那个几乎无人不知无人不晓的倍增求LCA

    • 倍增算法

          倍增这个东西要是明白了就很简单,简单点说就是按照2的次方步来跳如2,4,8,16,32,64,128......只是我是从大往小跳,如果大的步数跳多了在试小一点的,有点像悔棋的感觉,以5为例,如果从小往大跳,5<1+2+4,所以结束后还要回溯才能得到5,但如果5=4+1,这样就很方便了,这也可以从二进制来理解,从高位往低位填数比从低位往高位填简单的多,这用代码实现也比较简单。

         回到图中:17会直接往上跳4步到3,而18会跳4步后再跳1步到3(并非LCA真正的路径只是演示一下),比刚才的无脑暴力不知道快多少倍。

         事实也却实如此此时的复杂度为O(nlogn),正常的题目都够用了,

    算法实现

         要实现也很简单,我们要记录每个点的深度deep,用parents[i][j]来记录i的2j级祖先,所用的大致变量如下

     1 const int maxn=1e6+10;
     2 struct node
     3 {
     4     int to;//连结到的边 
     5     int next;
     6 }way[maxn<<1];
     7 int head[maxn];//邻接表存表的好伙伴 
     8 int parents[maxn][21];//当前点的倍增祖先们,2的21次方足够了 
     9 int tot;
    10 int deep[maxn];//深度 

        然后跑一个DFS来预处理一下

     1 int dfs(int x,int father)//x为当前节点,father为其父节点 
     2 {
     3     deep[x]=deep[father]+1;//当前点的深度为其父节点深度加1 
     4     parents[x][0]=father;//当前点的2^0祖先(也就是上1级祖先)就是其父节点 
     5     for(int i=1;(1<<i)<=deep[x];i++)
     6     {
     7         parents[x][i]=parents[parents[x][i-1]][i-1];
     8         //这里应该是整个预处理阶段中最有灵魂的部分了
     9         //x的2^i级祖先就是x的2^(i-1)级祖先的2^(i-1)级的祖先 。
    10         //2^i==2^(i-1)+2^(i-1),这个式子好像没什么可说的 
    11     }
    12     for(int i=head[x];i;i=way[i].next)
    13     {
    14         int to=way[i].to;
    15         if(to!=father)
    16         dfs(to,x);
    17      } 
    18  }

     重点来了

      接下来就是倍增了,为了方便,我们要先把两个点调到同一深度才统一开始跳

     但是我们千万不可以直接就跳到LCA上,就像前面的图上,我们完全可以把4和8直接跳到1,但1只是公共祖先而不是LCA,然后我们可以跳到LCA的下一层,然后输出他们的共同的父节点这样就会防止误判了。

     1 int lca(int a,int b)//a,b为两个要查询的点 
     2 {
     3     if(deep[a]>deep[b])//我时刻保证a的深度比b的小 
     4     {
     5         swap(a,b); //如果反了就换一下 
     6     }
     7     for(int i=20;i>=0;i--)
     8     {
     9          if(deep[a]<=deep[b]-(1<<i))
    10          b=parents[b][i];//将a和b跳的同一高度 
    11     } 
    12     if(a==b)//如果b在跳上来时和a一样了,那说明a就是a和b的LCA,直接返回就行了 
    13     return a;
    14     for(int i=20;i>=0;i--)
    15     {
    16         if(parents[a][i]==parents[b][i])
    17         continue;
    18         else
    19         {
    20             a=parents[a][i];
    21             b=parents[b][i];//将a和b一起往上跳 
    22         }
    23     }
    24     return parents[a][0];//找出最后的答案 
    25 }

    真正LCA的路径为:

    17号节点: 17--->10--->7--->3

    18号节点: 18--->16--->8--->5--->3

    解释一下,18和17先跳到同一深度,再跳到LCA的下一层,17跳到7,18跳到5,随后的LCA就是5和7的共同父节点

    优化

          在预处理完后,为了跑的更快,可以加一个常数优化

    1     for(int i=1;i<=n;i++)//预先算出log的值,在后来就可直接调用 
    2     {
    3         lg[i]=lg[i-1]+(1<<lg[i-1]==i);//一个很有名的公式,看不懂的可以百度一下推的过程 
    4     }

     

    总结

          LCA就这么多,主要还是要多练一练题目,不然就算告诉你是LCA,你都不会打,除了,开头的模板题,这题也算是半模板吧---->传送门

         最后把完整的代码放一下

     1 #include<iostream>
     2 #include<cstdio>
     3 #include<algorithm>
     4 #include<cstring>
     5 
     6 using namespace std;
     7 
     8 const int maxn=1e6+10;
     9 struct node
    10 {
    11     int to;//连结到的边 
    12     int next;
    13 }way[maxn<<1];
    14 int head[maxn];//邻接表存表的好伙伴 
    15 int parents[maxn][21];//当前点的倍增祖先们,2的21次方足够了 
    16 int tot;
    17 int deep[maxn];//深度 
    18 int n,m,s; 
    19 
    20 int add(int x,int y)//标准的领接表存边 
    21 {
    22     way[++tot].next =head[x];
    23     way[tot].to=y;
    24     head[x]=tot;
    25 } 
    26 void dfs(int x,int father)//x为当前节点,father为其父节点 
    27 {
    28     deep[x]=deep[father]+1;//当前点的深度为其父节点深度加1 
    29     parents[x][0]=father;//当前点的2^0祖先(也就是上1级祖先)就是其父节点 
    30     for(int i=1;(1<<i)<=deep[x];i++)
    31     {
    32         parents[x][i]=parents[parents[x][i-1]][i-1];
    33         //这里应该是整个预处理阶段中最有灵魂的部分了
    34         //x的2^i级祖先就是x的2^(i-1)级祖先的2^(i-1)级的祖先 。
    35         //2^i==2^(i-1)+2^(i-1),这个式子好像没什么可说的 
    36     }
    37     for(int i=head[x];i;i=way[i].next)
    38     {
    39         int to=way[i].to;
    40         if(to!=father)
    41         dfs(to,x);
    42      } 
    43  } 
    44  
    45 int lca(int a,int b)//a,b为两个要查询的点 
    46 {
    47     if(deep[a]>deep[b])//我时刻保证a的深度比b的小 
    48     {
    49         swap(a,b); //如果反了就换一下 
    50     }
    51     for(int i=20;i>=0;i--)
    52     {
    53          if(deep[a]<=deep[b]-(1<<i)) 
    54          b=parents[b][i];//将a和b跳的同一高度 
    55     } 
    56     if(a==b)//如果b在跳上来时和a一样了,那说明a就是a和b的LCA,直接返回就行了 
    57     return a;
    58     for(int i=20;i>=0;i--)
    59     {
    60         if(parents[a][i]==parents[b][i])
    61         continue;
    62         else
    63         {
    64             a=parents[a][i];
    65             b=parents[b][i];//将a和b一起往上跳 
    66         }
    67     }
    68     return parents[a][0];//找出最后的答案 
    69 }
    70 
    71 int main()
    72 {
    73     scanf("%d %d %d",&n,&m,&s);//亲生经验告诉我们cin只能用于调试之类的 
    74     //cin>>n>>m>>s;
    75     for(int i=1;i<=n-1;i++)
    76     {
    77         int a,b;
    78         scanf("%d %d",&a,&b);
    79         //cin>>a>>b;
    80         add(a,b);//因为是树,所以是双向边 
    81         add(b,a);
    82     }
    83     dfs(s,0);
    84     for(int i=1;i<=m;i++)
    85     {
    86         int a,b;
    87         scanf("%d %d",&a,&b);
    88         //cin>>a>>b;
    89         printf("%d
    ",lca(a,b));
    90         //cout<<lca(a,b)<<endl;
    91     }
    92     return 0;
    93  } 
  • 相关阅读:
    java web 入门实例servlet篇(显示后台数据库列表,删除某一条记录并显示)
    我的成长比价系列:java web开发过程中遇到的错误一:sql语句换行错误
    spring mvc + velocity 搭建实例程序maven版本并且使用的是tomcat容器而不是jetty(step by step)
    spring mvc学习笔记(一)web.xml文件配置的一点重要信息
    与数据库连接的页面增删改查 的easyui实现(主要是前端实现)
    oracle 11g 空表导出
    EMCA和EMCTL的简单用法
    vs2010补丁
    CAS 策略已被 .NET Framework 弃用
    sqlserver2008 链接服务器 2000
  • 原文地址:https://www.cnblogs.com/2529102757ab/p/10727254.html
Copyright © 2020-2023  润新知