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

水一发题解。

我只是想存一下树剖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
",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
",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
",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)的了。

安利一下自己博客

找时间搞搞吧......

原文地址:https://www.cnblogs.com/cervusy/p/9502559.html