最近公共祖先 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++ 标程(差分时记得去重)

    


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

原文地址:https://www.cnblogs.com/t-s-y/p/10317060.html