求最近公共祖先(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\n",lca(x,y)); 66 } 67 return 0; 68 }
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 }
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)的了。
安利一下自己博客
找时间搞搞吧......