题目描述:
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
题解:
LCA一般有四种求法:
1.倍增LCA
2.LCA转RMQ
3.树链剖分求LCA
4.tarjan求LCA(注意只能离线)
不过呢,我比较喜欢用树剖,因为树剖比较好用,也比较好写,好想,而且实际复杂度比较优。
树链剖分就是把树剖分成若干条不相交的链
显然,在链上求LCA很简单,我们只要找到链上两点深度较小的点就行了
所以在链上比在其他连接链的边上快
因此实际应用中我们一般会使用“轻重边树链剖分”
“轻重边树链剖分”指的是将树边分成两类:“重边”和“轻边”
分类方法如下:
我们定义Size(x)为以x为根结点的子树的结点个数
对于每个结点u,在它的所有子结点中寻找一个结点v
使得对于u的其他子节点w,都有Size(v)≥Size(w)
此时u有一条重边连向v,有若干条轻边连向u的其他子结点
这样一来,树上的不在重链上的边的数量便大大减少,效率就提高了
然后我们每次求LCA(x,y)的时候就可以判断两点是否在同一链上
如果两点在同一条链上我们只要找到这两点中深度较小的点输出就行了
如果两点不在同一条链上那就找到深度较大的点令它等于它所在的重链链端的父节点即为x=f[top[x]]
把树上的重链划分完以后,就可以开始求LCA了。
树剖要用到两次dfs,都是O(n)的复杂度
然后如果图是满二叉树的话是最坏情况,但此时查询也是O(logn)的
最坏情况每次一步步取f[top[x]]走,走下来是一个树的深度,也就是一个logn
时间复杂度:O(2n+mlogn)
附上代码:
#include<cstdio> #include<cstring> #include<algorithm> #include<cmath> using namespace std; int n,m,s; int idx,to[1000010],nxt[1000010],head[1000010],f[500010],top[500010],dep[500010],son[500010],siz[500010]; void addedge(int x,int y) { ++idx; to[idx]=y; nxt[idx]=head[x]; head[x]=idx; } void dfs1(int x) { dep[x]=dep[f[x]]+1; siz[x]=1; for(int i=head[x];i;i=nxt[i]) { if(f[to[i]]==0&&f[x]!=to[i]) { f[to[i]]=x; dfs1(to[i]); siz[x]+=siz[to[i]]; if(siz[son[x]]<siz[to[i]]) son[x]=to[i]; } } } void dfs2(int x) { if(x==son[f[x]]) top[x]=top[f[x]]; else top[x]=x; for(int i=head[x];i;i=nxt[i]) if(f[to[i]]==x) dfs2(to[i]); } int lca(int x,int y) { while(top[x]!=top[y]) { if(dep[top[x]]>dep[top[y]]) x=f[top[x]]; else y=f[top[y]]; } if(dep[x]<dep[y]) return x; else return y; } int main() { scanf("%d%d%d",&n,&m,&s); for(int i=1;i<n;i++) { int x,y; scanf("%d%d",&x,&y); addedge(x,y); addedge(y,x); } dfs1(s); dfs2(s); for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); printf("%d\n",lca(x,y)); } }