1. 程式人生 > >求最近公共祖先(LCA)的各種算法

求最近公共祖先(LCA)的各種算法

host .cn 提取 模擬 最小值 以及 play 樹鏈剖分 pla

水一發題解。

我只是想存一下樹剖LCA的代碼......

以洛谷上的這個模板為例:P3379 【模板】最近公共祖先(LCA)

1.樸素LCA

就像做模擬題一樣,先dfs找到基本信息:每個節點的父親、深度。

把深的節點先往上跳。

深度相同了之後,一起往上跳。

最後跳到一起了就是LCA了。

預處理:O(n)

每次查詢:O(n)

2.倍增LCA

樸素LCA的一種優化。

一點一點跳,顯然太慢了。

如果要跳x次,可以把x轉換為二進制。

每一位都是1或0,也就是跳或者不跳。

在第i位,如果跳,就向上跳2(i-1)次。

至於跳或者不跳,判斷很簡單。

如果跳了之後還沒在一起,就跳。

預處理:算出每個點上跳2n

次後的位置。(已知上跳20次的位置就是它的父親)O(nlogn)

每次詢問:O(logn)

技術分享圖片
 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 
 6 int n,m,s;
 7 int hd[500005],nx[1000005],to[1000005],cnt;
 8 
 9 void add(int af,int at)
10 {
11     to[++cnt]=at;
12     nx[cnt]=hd[af];
13     hd[af]=cnt;
14 } 15 16 int d[500005],f[500005][25]; 17 18 void pre(int p,int fa) 19 { 20 f[p][0]=fa; 21 d[p]=d[fa]+1; 22 for(int i=hd[p];i;i=nx[i]) 23 { 24 if(to[i]!=fa)pre(to[i],p); 25 } 26 } 27 28 int lca(int x,int y) 29 { 30 if(d[x]<d[y])swap(x,y); 31 for(int i=20;i>=0;i--) 32
{ 33 if(d[f[x][i]]>=d[y])x=f[x][i]; 34 } 35 if(x==y)return x; 36 for(int i=20;i>=0;i--) 37 { 38 if(f[x][i]!=f[y][i])x=f[x][i],y=f[y][i]; 39 } 40 return f[x][0]; 41 } 42 43 int main() 44 { 45 scanf("%d%d%d",&n,&m,&s); 46 for(int i=1;i<n;i++) 47 { 48 int aa,bb; 49 scanf("%d%d",&aa,&bb); 50 add(aa,bb); 51 add(bb,aa); 52 } 53 pre(s,0); 54 for(int i=1;i<=20;i++) 55 { 56 for(int j=1;j<=n;j++) 57 { 58 f[j][i]=f[f[j][i-1]][i-1]; 59 } 60 } 61 for(int i=1;i<=m;i++) 62 { 63 int x,y; 64 scanf("%d%d",&x,&y); 65 printf("%d\n",lca(x,y)); 66 } 67 return 0; 68 }
倍增LCA

3.歐拉序+RMQ

歐拉序,就是dfs時,無論是進入該點的子樹,還是從該點的子樹中出來,都記錄一遍這個點。這樣得到一個序列,就是歐拉序。

比如說點A為根,BCD為A的兒子的一顆簡單的樹,加上一個E作為C的兒子。

其歐拉序就是A B A C E C A D A

那麽,任取兩點,它們的LCA,就是歐拉序中,這兩個點之間深度最小的點。

如果一個點在歐拉序中出現了多次,任取一個位置就好。

區間深度最小點,用RMQ。O(nlogn)預處理後,每次詢問O(1)求出。

技術分享圖片
 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 using namespace std;
 5 
 6 int m,n,ecnt,root;
 7 int head[500005],nx[1000005],to[1000005];
 8 int euler[1500005],eucnt,ps[1500005],high[1500005][25];
 9 int fa[500005],dep[500005];
10 int log[1500005];
11 
12 int add(int af,int at)
13 {
14     to[++ecnt]=at;
15     nx[ecnt]=head[af];
16     head[af]=ecnt;
17 }
18 
19 void dfs(int pos,int fat)
20 {
21     dep[pos]=dep[fat]+1;
22     euler[++eucnt]=pos;
23     ps[pos]=eucnt;
24     fa[pos]=fat;
25     for(int i=head[pos];i;i=nx[i])
26     {
27         if(to[i]!=fat)
28         {
29             dfs(to[i],pos);
30             euler[++eucnt]=pos;
31         }
32     }
33 }
34 
35 void prelca()
36 {
37     for(int i=2;i<=3*n;i++)log[i]=log[i/2]+1;
38     for(int i=1;i<=eucnt;i++)high[i][0]=euler[i];
39     for(int i=1;i<=27;i++)
40     {
41         for(int j=1;j+(1<<i)-1<=eucnt;j++)
42         {
43             if(dep[high[j][i-1]]>dep[high[j+(1<<(i-1))][i-1]])
44                 high[j][i]=high[j+(1<<(i-1))][i-1];
45             else
46                 high[j][i]=high[j][i-1];
47         }
48     }
49 }
50 
51 int lca(int x,int y)
52 {
53     int ll=ps[x];
54     int rr=ps[y];
55     if(ll>rr)int t=ll; ll=rr; rr=t;
56     int len=rr-ll+1;
57     if(dep[high[ll][log[len]]]>dep[high[rr-(1<<log[len])+1][log[len]]])
58         return high[rr-(1<<log[len])+1][log[len]];
59     else
60         return high[ll][log[len]];
61 }
62 
63 int main()
64 {
65     scanf("%d%d%d",&n,&m,&root);
66     for(int i=1;i<n;i++)
67     {
68         int a,b;
69         scanf("%d%d",&a,&b);
70         add(a,b);
71         add(b,a);
72     }
73     dfs(root,0);
74     prelca();
75     for(int i=1;i<=m;i++)
76     {
77         int q,w;
78         scanf("%d%d",&q,&w);
79         printf("%d\n",lca(q,w));
80     }
81     return 0;
82 }
歐拉序+RMQ

4.樹鏈剖分

把樹分成輕鏈和重鏈。

先一遍dfs找到重兒子,即子樹最大的兒子。

每個點與重兒子的連邊組成重鏈。

第二遍dfs記錄每個點的tp值:所在重鏈的頂端。

如果在輕鏈上,tp就是它自己。

求LCA;類似倍增。

讓tp較深的點上跳,跳到fa[tp]。

最後tp[x]==tp[y]的時候,二者在同一重鏈上,LCA即為深度較淺的那個點。

預處理:O(n)

每次詢問:O(logn)

技術分享圖片
 1 #include<cstdio>
 2 
 3 int hd[500005],to[1000005],nx[1000005],cnt;
 4 int hs[500005],tp[500005],f[500005],d[500005],sz[500005];
 5 
 6 int n,m,s;
 7 
 8 void add(int af,int at)
 9 {
10     to[++cnt]=at;
11     nx[cnt]=hd[af];
12     hd[af]=cnt;
13 }
14 
15 void dfs(int p,int fa)
16 {
17     f[p]=fa;
18     d[p]=d[fa]+1;
19     sz[p]=1;
20     for(int i=hd[p];i;i=nx[i])
21     {
22         if(to[i]==fa)continue;
23         dfs(to[i],p);
24         sz[p]+=sz[to[i]];
25         if(sz[to[i]]>sz[hs[p]])hs[p]=to[i];
26     }
27 }
28 
29 void findtp(int p)
30 {
31     if(p==hs[f[p]])tp[p]=tp[f[p]];
32     else tp[p]=p;
33     for(int i=hd[p];i;i=nx[i])
34         if(to[i]!=f[p])findtp(to[i]);
35 }
36 
37 int lca(int a,int b)
38 {
39     while(tp[a]!=tp[b])d[tp[a]]>d[tp[b]]?a=f[tp[a]]:b=f[tp[b]];
40     return d[a]<d[b]?a:b;
41 }
42 
43 int main()
44 {
45     scanf("%d%d%d",&n,&m,&s);
46     for(int i=1;i<n;i++)
47     {
48         int x,y;
49         scanf("%d%d",&x,&y);
50         add(x,y);
51         add(y,x);
52     }
53     dfs(s,0);
54     findtp(s);
55     for(int i=1;i<=m;i++)
56     {
57         int a,b;
58         scanf("%d%d",&a,&b);
59         printf("%d\n",lca(a,b));
60     }
61     return 0;
62 }
樹鏈剖分

5.離線tarjan

(待填坑)

6.歐拉序+約束RMQ

洛谷上的玄學操作。應該是歐拉序+RMQ的優化。

把原歐拉序分塊,塊內預處理,塊間ST表。(我並不知道ST表是什麽......)

摘自洛谷題解:

分塊大小定為L=log(n)/2,這樣共分D=n/L塊,對這D個數(塊內最小值)做正常ST表,建表復雜度O(Dlog(D))=O((n/L)(log(n)-log(L))=O(n)

我們要保證每個步驟都是O(n)的,log(n)/2的塊正好消去了ST建表時的log

但在此之前,我們得處理出塊內的最小值,該怎麽做呢?一個正常想法就是枚舉每個數,一共是O(n)復雜度

但是,這樣做雖然留下了每塊的最小值以及其取到的位置,若考慮查詢塊的一個區間,而這個區間恰好取不到最小值,這時候只能暴力枚舉,就破壞了查詢O(1)了

至此我們仍沒有使用其±1的特殊性質,現在考慮一下。

塊內一共log(n)/2個數,由乘法原理可知,本質不同的塊有U=2^(log(n)/2)=n^(1/2)個,我們不妨處理出每個這種塊,復雜度Ulog(n)/2,這個函數增長是小於線性的,可以認為是O(n)

這樣,處理出每個塊內兩元素的大小關系,就可以用01唯一表示一個塊了,可以用二進制存下來,作為一個塊的特征,這一步復雜度O(n)

這樣有一個好處,即使查詢塊內一個區間,我們只需要提取這個區間對應的二進制數,就可以在預處理的數組中O(1)查詢了

(怎麽做呢?把這段二進制數提出來,移到最右邊,由於我們規定0表示小於,1表示大於,所以會貪心地選取前面的數,查表減去偏移量就可以了)

查詢時,類似分塊,邊角的塊直接查表,中間部分ST表查詢,查詢是O(1)的。

至此我們完成了O(n)建表,O(1)查詢的約束RMQ。

一般地,對於任何一個序列,可以在O(n)時間內建成一顆笛卡爾樹,把查詢該序列RMQ轉化為求笛卡爾樹LCA,就變成O(1)的了。

安利一下自己博客

找時間搞搞吧......

求最近公共祖先(LCA)的各種算法