• 最近公共祖先 lca (即将施工完成)


    声明

     


      咳咳,进入重难点的图论算法之一 : lca!(敲黑板)

      题目: 洛谷 P3379 


      一、定义

        LCA 指的是最近公共祖先(Least Connon Ancestors)。具体地,给定一棵有根树,若节点 z 既是 x 的祖先,也是节点 y 的祖先,则称 z 是 x , y 的公共祖先。在 x , y 的公共祖先中,深度最大的一个节点称为 x , y 的最近公共祖先,记为 LCA(x , y)。

         我们来举个例子,如图所示:LCA(4 , 5)= 2,LCA(5 , 6)= 1,LCA(2 , 3)= 1。

                                                

      二、如何求 LCA

        我们考虑“暴力”要怎么实现找两点的 LCA。

          举个例子,如图: 7 , 5 的 LCA 是 2。

                                     

          先 DFS 一遍找出每个点的 DEP (深度)。然后先从深度大的 7 往上跳,跳到和 5 深度相同的点 4,发现还是没有到同一个点,那么 4、5 继续往上跳,直到跳到 2 位置,发现点一样了,那么 2 就是它们的 LCA 了。


      三、如何优化这个方法

          这里一共有两种方法:

          1.离线 tarjan 求 LCA: 这里就贴一贴别人的博客吧

                      
     1 var
     2         i,j,k,n,m,s,t:longint;
     3         first,next,en,qfirst,qnext,qen,b,f,ans:array[0..2000001] of longint;
     4 procedure add(k,x,y:longint);
     5 begin
     6         next[k]:=first[x];
     7         first[x]:=k;
     8         en[k]:=y;
     9 end;
    10 procedure qadd(k,x,y:longint);
    11 begin
    12         qnext[k]:=qfirst[x];
    13         qfirst[x]:=k;
    14         qen[k]:=y;
    15 end;
    16 function find(x:longint):longint;
    17 begin
    18         if f[x]=x then exit(x) else begin f[x]:=find(f[x]); exit(f[x]) end;
    19 end;
    20 procedure tarjan(x:longint);
    21 var
    22         j,k,t:longint;
    23 begin
    24         b[x]:=1;
    25         t:=first[x];
    26         while t>0 do
    27         begin
    28                 if b[en[t]]=0 then
    29                 begin
    30                         tarjan(en[t]);
    31                         j:=find(x);
    32                         k:=find(en[t]);
    33                         f[k]:=j;
    34                 end;
    35                 t:=next[t];
    36         end;
    37         t:=qfirst[x];
    38         while t>0 do
    39         begin
    40                 if b[qen[t]]=2 then
    41                 begin
    42                         ans[t]:=find(qen[t]);
    43                         if t mod 2=1 then ans[t+1]:=ans[t]
    44                                 else ans[t-1]:=ans[t];
    45                 end;
    46                 t:=qnext[t];
    47         end;
    48         b[x]:=2;
    49 end;
    50 begin
    51         readln(n,m,s);
    52         for i:=1 to n-1 do
    53         begin
    54                 readln(j,k);
    55                 add(i*2-1,j,k);
    56                 add(i*2,k,j);
    57         end;
    58         for i:=1 to m do
    59         begin
    60                 readln(j,k);
    61                 qadd(i*2-1,j,k);
    62                 qadd(i*2,k,j);
    63         end;
    64         for i:=1 to n do
    65                 f[i]:=i;
    66         tarjan(s);
    67         for i:=1 to n do
    68                 writeln(ans[i*2]);
    69 end.
    再附上我以前的 Pascal 标程 (虽然现在不太理解)

            2.倍增求 LCA:我们考虑这个方法慢在哪里,当然是对于每个点,一次往上跳一步,导致了效率低。那么如何优化呢?只要一次能向上跳多步,效率自然就高了。

          树上倍增法

            树上倍增法是一个很重要的算法。设 f [ x , k ] 表示 x 的 2 k 辈祖先,即从 x 向根节点走 2 k 步到达的节点。特别地,若该节点不存在,则令 f [ x , k ] = 0 。f [ x , 0 ] 就是 x 的父节点。

            因为 x 向根节点走 2 k⇔ 向根走 2 k-1 步,再走 2 k-1 步(2 k = 2 k-1 + 2 k-1)。所以对于 k ∈ [ 1 , log n ],有 f [ x , k ] = f [ f [ x , k-1 ] , k-1 ]。

            这类似于一个动态规划的过程,“阶段”就是节点的深度。因此,我们可以对树进行遍历,由此得到 f [ x , 0 ] ,再计算 f 数组的所有值。

            以上部分是预处理,时间复杂度为 O(n log n)。之后可以多次对不同的 x , y 计算 LCA,每次询问的时间复杂度为 O(log n)。

            然后基于 f 数组计算 LCA(x , y)分为以下几步:

            ① 设 dep [ x ] 表示 x 的深度。不妨设 dep [ x ] ≥ dep [ y ](否则,可交换 x , y)。

            ② 用二进制拆分思想,把 x 向上调整到与 y 同一深度。

            具体来说,就是依次尝试从 x 向上走 k = 2 log n……2 1,2 0 步,若到达的节点比 y 深,则令 x = f [ x , k ] 。

            ③ 若此时 x = y,说明已经找到了 LCA,LCA 就等于 y 。

            ④ 若此时 x ≠ y,那么 x , y 就继续往上跳,用二进制拆分思想,把 x , y 同时向上调整,并保持深度一致且两者不相会。

            具体来说,就是依次尝试把 x , y 同时向上走 k = 2 log n……2 1,2 步,若 f [ x , k ] ≠ f [ y , k ](即仍未相会),则令 x = f [ x , k ],y = f [ y , k ] 。

            ⑤ 此时 x , y 必定只差一步就相会了,所以它们的父节点 f [ x , 0 ](或 f [ y , 0 ]) 就是它们的 LCA !

         【代码实现

           预处理:

              void deal_first(int u,int father)
              {
                  dep[u]=dep[father]+1;
                  for (int i=0;i<=19;i++)
                      f[u][i+1]=f[f[u][i]][i];
                  for (int e=first[u];e;e=next[e])
                  {
                      int v=en[e];
                      if (v==father) continue;
                      f[v][0]=u;                // v 向上跳 20=1 就是 u
                      deal_first(v,u);
                  }
              }

            

           查询 x , y 的 LCA:      

              int lca(int x,int y)
              {
                  if (dep[x]<dep[y]) swap(x,y);            //让 x 的深度较大
                     //我们用“暴力”的思想:先将 x,y 跳到一个深度,然后一起往上跳
                  for (int i=20;i>=0;i--)                  //一定要倒着 for 
                  {
                      if (dep[f[x][i]]>=dep[y]) x=f[x][i]; //先跳到同一层 
                      if (x==y) return x; 
                  } 
                  for (int i=20;i>=0;i--)                  //此时 x,y 已跳到同一层
                      if (f[x][i]!=f[y][i])                //如果 f[x][i]和 f[y][i] 不同才跳 
                      {
                          x=f[x][i];
                          y=f[y][i];    
                      }
                  return f[x][0];                          
               // x,y 是深度最浅且不同的点,即 lca 的子节点           }

           然后附完整的 Pascal 标程(以前写的,步骤排列不太一样,但总体思路是差不多的):

                 
     1 var
     2         rmq:array[0..1000001,0..21] of longint;
     3         first,next,en,one,b:array[0..2000001] of longint;
     4         deep,a:array[0..10000001] of longint;
     5         i,j,k,m,n,s,t,l,r:longint;
     6 procedure add(k,x,y:longint);
     7 begin
     8         next[k]:=first[x];
     9         first[x]:=k;
    10         en[k]:=y;
    11 end;
    12 procedure getrmq;
    13 begin
    14         for i:=1 to k do
    15                 rmq[i,0]:=i;
    16         for j:=1 to 20 do
    17                 for i:=1 to k do
    18                         if i+(1<<j)-1<=k then
    19                                 if deep[rmq[i,j-1]]<deep[rmq[i+(1<<(j-1)),j-1]]
    20                                         then rmq[i,j]:=rmq[i,j-1]
    21                                                 else rmq[i,j]:=rmq[i+(1<<(j-1)),j-1];
    22 end;
    23 function que:longint;
    24 begin
    25         t:=trunc(ln(r-l+1)/ln(2));
    26         if deep[rmq[l,t]]<deep[rmq[r-(1<<t)+1,t]] then exit(a[rmq[l,t]])
    27                 else exit(a[rmq[r-(1<<t)+1,t]]);
    28 end;
    29 procedure dfs(x,y:longint);
    30 var
    31         t:longint;
    32 begin
    33         inc(k);
    34         deep[k]:=y;
    35         b[x]:=1;
    36         a[k]:=x;
    37         t:=first[x];
    38         while t>0 do
    39         begin
    40                 if b[en[t]]=0 then
    41                 begin
    42                         dfs(en[t],y+1);
    43                         inc(k);
    44                         a[k]:=x;
    45                         deep[k]:=y;
    46                 end;
    47                 t:=next[t];
    48         end;
    49         b[x]:=0;
    50 end;
    51 begin
    52         readln(n,m,s);
    53         for i:=1 to n-1 do
    54         begin
    55                 readln(j,k);
    56                 add(i*2-1,j,k);
    57                 add(i*2,k,j);
    58         end;
    59         k:=0;
    60         dfs(s,1);
    61         for i:=1 to k do
    62                 if b[a[i]]=0 then
    63                 begin
    64                         one[a[i]]:=i;
    65                         b[a[i]]:=1;
    66                 end;
    67         getrmq;
    68         for i:=1 to m do
    69         begin
    70                 readln(j,k);
    71                 l:=one[j];
    72                 r:=one[k];
    73                 if l>r then
    74                 begin
    75                         j:=l;
    76                         l:=r;
    77                         r:=j;
    78                 end;
    79                 writeln(que);
    80         end;
    81 end.
    自己现在不理解的 Pascal 标程

      四、习题

          1 . 洛谷 P3258:松鼠的新家(LCA+树上差分,当然也可以用树链剖分做)    

              
     1 #include<cstdio>
     2 #include<algorithm>
     3 #include<cmath>
     4 
     5 using namespace std;
     6 
     7 int n,x,y,tot,s,t;
     8 int first[1000005],next[1000005],en[1000005];
     9 int ans[300005],dep[300005],f[300005][21],a[300005];
    10 
    11 void deal_first(int u,int father)
    12 {
    13     dep[u]=dep[father]+1;
    14     for (int i=0;i<=19;i++)
    15         f[u][i+1]=f[f[u][i]][i];
    16     for (int e=first[u];e;e=next[e])
    17     {
    18         int v=en[e];
    19         if (v==father) continue;
    20         f[v][0]=u;
    21         deal_first(v,u);
    22     }
    23 }
    24 
    25 int lca(int x,int y)
    26 {
    27     if (dep[x]<dep[y]) swap(x,y);        
    28     for (int i=20;i>=0;i--)    
    29     {
    30         if (dep[f[x][i]]>=dep[y]) x=f[x][i]; 
    31         if (x==y) return x; 
    32     } 
    33     for (int i=20;i>=0;i--)        
    34         if (f[x][i]!=f[y][i])            
    35         {
    36             x=f[x][i];
    37             y=f[y][i];    
    38         }
    39     return f[x][0];        
    40 }
    41 
    42 void add(int x,int y)
    43 {
    44     next[++tot]=first[x];
    45     first[x]=tot;
    46     en[tot]=y;
    47 }
    48 
    49 void updata(int u,int father)
    50 {
    51     for (int e=first[u];e;e=next[e])
    52     {
    53         int v=en[e];
    54         if (v==father) continue;
    55         updata(v,u);
    56         ans[u]+=ans[v];
    57     }
    58 }
    59 
    60 int main()
    61 {
    62     scanf("%d
    ",&n);
    63     for (int i=1;i<=n;i++)
    64         scanf("%d",&a[i]);
    65     for (int i=1;i<n;i++)
    66     {
    67         scanf("%d%d",&x,&y);
    68         add(x,y);
    69         add(y,x);
    70     }
    71     deal_first(1,0);
    72     for (int i=1;i<n;i++)
    73     {
    74         int LCA=lca(a[i],a[i+1]);
    75         ans[a[i]]++;
    76         ans[a[i+1]]++;
    77         ans[LCA]--;
    78         ans[f[LCA][0]]--;
    79     }
    80     updata(1,0);
    81     
    82     //每条路的终点会同时作为下条路的起点重复+1
    83     //最后的终点不会重复,但题目说不要+1
    84     //于是每条路的终点我们都加多了一次
    85     //所以现在将每条路的终点都-1 
    86     for (int i=2;i<=n;i++)   
    87         ans[a[i]]--;
    88         
    89     for (int i=1;i<=n;i++)
    90         printf("%d
    ",ans[i]);
    91     return 0;
    92 }
    C++ 标程(差分时记得去重)

        


      以上内容部分借鉴于:《信息学奥赛一本通 · 提高篇(第一版)》

  • 相关阅读:
    Delphi 简体 繁体 转换
    简单地为DBNavigator填加Caption
    TEdit的 Clear 和 赋值 ''
    SSH服务端
    动态模块导入示例、断言
    异常处理
    反射、getattr
    类的各种自带方法,静态方法,属性方法,类方法等
    类的继承,深度优先于广度优先
    类变量与实例变量、析构函数、私有属性与私有方法
  • 原文地址:https://www.cnblogs.com/t-s-y/p/10317060.html
Copyright © 2020-2023  润新知