【图论·树】最近公共祖先LCA
8.最近公共祖先LCA
一棵有根树,若节点z即是节点x的祖先,又是y的祖先,则称z是x、y的公共祖先,其中深度最大的一个被称为x、y的最近公共祖先。
理解LCA:
向上标记法:从点x向上走到根节点,并标记所有经过的节点。然后从点y向上走到根节点,遇到的第一个已标记的节点就是LCA(x,y)。单次复杂度\(O(N)\)。
可以每次找深度比较大的那个点,让它向上跳。显然在树上,这两个点最后一定会相遇,相遇的位置就是想要求的 LCA。 (或者先向上调整深度较大的点,令他们深度相同,然后再共同向上跳转,最后也一定会相遇。)这种方法可以求出从点u到点v的简单路径上的点。单次复杂度\(O(dis(u,v))\)。
LCA具有交换律和结合律。
8.1.重链剖分在线求LCA\(O(N+Q\log N)-O(N)\)
时间复杂度:\(O(N+Q\log N)\)。空间复杂度:\(O(N)\)。
int dep[N],siz[N],fa[N],son[N];
int top[N];
void dfs1(int u)
{
dep[u]=dep[fa[u]]+1,siz[u]=1;
for(int i=h[u];i!=0;i=ne[i])
{
int v=e[i];
if(v==fa[u]) continue;
fa[v]=u;
dfs1(v);
siz[u]+=siz[v];
if(siz[son[u]]<siz[v]) son[u]=v;
}
return ;
}
void dfs2(int u,int t)
{
top[u]=t;
if(!son[u]) return ;
dfs2(son[u],t);
for(int i=h[u];i;i=ne[i])
{
int v=e[i];
if(v==fa[u] || v==son[u]) continue;
dfs2(v,v);
}
return ;
}
int lca(int u,int v)
{
while(top[u]!=top[v])
{
if(dep[top[u]]<dep[top[v]]) swap(u,v);
u=fa[top[u]];
}
return dep[u]<dep[v] ? u : v;
}
// fa[rt]=0; //注意初始化fa
dfs1(rt);
dfs2(rt,rt);
cin>>u>>v;
cout<<lca(u,v)<<endl;
8.2.dfs序+ST表在线求LCA\(O(N\log N+Q)-O(N\log N)\)
当u=v时,lca(u,v)是u。当u≠v时,lca(u,v)是dfs序上下标在\([dfn_u+1,dfn_v]\)中的深度最小的任意节点的父亲。
一种避免记录每个结点的父亲和深度的方法是直接在 ST 表的最底层记录父亲,比较条件是取时间戳较小的结点。
时间复杂度:\(O(N\log N+Q)\)。空间复杂度:\(O(N\log N)\)。
int dfn[N],num;
int lg2[N],st[N][20];
void dfs(int u,int fa)
{
dfn[u]=++num;
st[dfn[u]][0]=fa;
for(int i=h[u];i;i=ne[i])
{
int v=e[i];
if(v==fa) continue;
dfs(v,u);
}
return ;
}
void st_lca()
{
for(int i=2;i<=n;i++) lg2[i]=lg2[i>>1]+1;
for(int k=1;1+(1<<k)-1<=n;k++)
for(int l=1;l+(1<<k)-1<=n;l++)
{
if(dfn[st[l][k-1]]<dfn[st[l+(1<<(k-1))][k-1]]) st[l][k]=st[l][k-1];
else st[l][k]=st[l+(1<<(k-1))][k-1];
}
return ;
}
int lca(int u,int v)
{
if(u==v) return u;
if(dfn[u]>dfn[v]) swap(u,v);
int k=lg2[dfn[v]-(dfn[u]+1)+1];
if(dfn[st[dfn[u]+1][k]]<dfn[st[dfn[v]-(1<<k)+1][k]]) return st[dfn[u]+1][k];
else return st[dfn[v]-(1<<k)+1][k];
}
dfs(rt,0);
st_lca();
cin>>u>>v;
cout<<lca(u,v)<<endl;
8.3.Tarjan算法离线求LCA\(O(\max\{N,Q\alpha(N+Q,N)\})-O(\max\{N,Q\})\)
本质上是并查集优化的向上标记法。
使用路径压缩/路径压缩和按秩合并优化的并查集:时间复杂度:\(O(\max\{N,Q\alpha(N+Q,N)\})\)。空间复杂度:\(O(\max\{N,Q\})\)。
int n,m,root;
int h[N],e[M],ne[M],idx;
int p[N]; //并查集
bool vis[N]; //是否遍历完成即将回溯
int ans[N];
vector<PII> q[N]; //离线储存询问
void tarjan(int u,int fa)
{
for(int i=h[u];i!=0;i=ne[i])
{
int v=e[i];
if(v==fa) continue;
tarjan(v,u);
p[v]=u;
}
vis[u]=true; //注意这行代码要在下行的上方,否则将不会处理询问LCA(u,u)
for(auto it : q[u]) if(vis[it.x]) ans[it.y]=find(it.x);
return ;
}
scanf("%d%d%d",&n,&m,&root);
for(int i=1;i<n;i++)
{
int u,v;
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
q[x].push_back({y,i}),q[y].push_back({x,i});
}
for(int i=1;i<=n;i++) p[i]=i;
tarjan(root,-1);
for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
8.4.其他方法求LCA
-
树上倍增法在线求LCA\(O(N\log N+Q\log N)-O(N\log N)\)
优点:可以与其他倍增数组结合,解决树上倍增问题。前置知识简单。
时间复杂度:\(O(N\log N+Q\log N)\)。空间复杂度:\(O(N\log N)\)。
int n,m,root;
int h[N],e[M],ne[M],idx;
int depth[N],fa[N][20];
void init_lca(int u,int father)
{
for(int i=h[u];i!=0;i=ne[i])
{
int v=e[i];
if(v==father) continue;
depth[v]=depth[u]+1;
fa[v][0]=u;
for(int k=1;k<=17;k++)
fa[v][k]=fa[fa[v][k-1]][k-1];
init_lca(v,u);
}
return ;
}
int lca(int a,int b){
if(depth[a]<depth[b]) swap(a,b);
for(int i=17;i>=0;i--)
if(depth[fa[a][i]]>=depth[b])
a=fa[a][i];
if(a==b) return a; //这里不要忘记!!!
for(int i=17;i>=0;i--)
if(fa[a][i]!=fa[b][i]){
a=fa[a][i];
b=fa[b][i];
}
return fa[a][0];
}
depth[root]=1;//这里不要忘记!!!
init_lca(root,-1);
int x,y;
cin>>x>>y;
int p=lca(x,y);//此时p就是x和y的最近公共祖先
-
欧拉序+ST表在线求LCA\(O(N\log N+Q)-O(N\log N)\)
优点:可以与其他欧拉序数组结合,解决树上欧拉序问题。
使用欧拉序把树上问题转化为线性问题,两个点的最近公共祖先就是两个点的欧拉序区间深度最小的节点,将问题转化为RMQ问题。
注意欧拉序列的长度是2n-1。
时间复杂度:\(O(N\log N+Q)\)。空间复杂度:\(O(N\log N)\)。
int euler[N],seq[N*2],sidx;
int dep[N],lg2[N*2],st[N*2][20];
void dfs(int u,int fa)
{
++sidx;
seq[sidx]=u,euler[u]=sidx;
for(int i=h[u];i!=0;i=ne[i])
{
int v=e[i];
if(v==fa) continue;
dep[v]=dep[u]+1;
dfs(v,u);
seq[++sidx]=u;
}
return ;
}
void st_lca()
{
for(int i=2;i<=2*n-1;i++) lg2[i]=lg2[i>>1]+1;
for(int i=1;i<=2*n-1;i++) st[i][0]=seq[i];
for(int k=1;1+(1<<k)-1<=2*n-1;k++)
for(int l=1;l+(1<<k)-1<=2*n-1;l++)
{
if(dep[st[l][k-1]]<=dep[st[l+(1<<(k-1))][k-1]]) st[l][k]=st[l][k-1];
else st[l][k]=st[l+(1<<(k-1))][k-1];
}
return ;
}
int lca(int x,int y)
{
if(euler[x]>euler[y]) swap(x,y);
int k=lg2[euler[y]-euler[x]+1];
if(dep[st[euler[x]][k]]<=dep[st[euler[y]-(1<<k)+1][k]]) return st[euler[x]][k];
else return st[euler[y]-(1<<k)+1][k];
}
dfs(s,-1);
st_lca();
cin>>u>>v;
cout<<lca(u,v)<<endl;
8.5.拓展应用
-
点集LCA
给定一棵 \(n\) 个点的以点 \(rt\) 为根的树,请回答下面 \(q\) 次询问:给定点集中的点的 \(\text{LCA}\) 是谁?
以点 \(rt\) 为根预处理 \(\text{dfs}\) 序。
“给定点集中的点的 \(\text{LCA}\)”是“给定点集中的 \(\text{dfs}\) 序最小的和最大的两个点的 \(\text{LCA}\)”。
-
换根LCA
给定一棵 \(n\) 个点的无根树,请回答下面 \(q\) 次询问:当以点 \(rt_i\) 为根时,点 \(u_i,v_i\) 的 \(\text{LCA}\) 是谁?
选定一个点为根(下文选定点 \(1\) 为根)预处理 \(\text{LCA}\)。
“当以点 \(rt_i\) 为根时,点 \(u_i,v_i\) 的 \(\text{LCA}\)”是“当以点 \(1\) 为根时,在点 \(rt_i,u_i\) 的 \(\text{LCA}\)、点 \(rt_i,v_i\) 的 \(\text{LCA}\) 和点 \(u_i,v_i\) 的 \(\text{LCA}\) 中深度最大的点”。
-
换根区间LCA
给定一棵 \(n\) 个点的无根树,请回答下面 \(q\) 次询问:当以点 \(rt_i\) 为根时,编号在 \([l_i,r_i]\) 中的点的 \(\text{LCA}\) 是谁?
选定一个点为根(下文选定点 \(1\) 为根)预处理 \(\text{dfs}\) 序和 \(\text{LCA}\)。
在以 \(\text{dfs}\) 序为下标的点的编号的序列上,在以 \(rt_i\) 为根的子树对应的区间和它的两个补区间(即在该序列上,下标为 \([1,\text{dfs}_{rt_i}-1],[\text{dfs}_{rt_i},\text{dfs}_{rt_i}+\text{size}_{rt_i}-1],[\text{dfs}_{rt_i}+\text{size}_{rt_i},n]\) 的三个区间,其中 \(\text{size}_{rt_i}\) 表示以 \(rt_i\) 为根的子树的点数)中各自找到最靠近区间左端点和右端点的编号在 \([l_i,r_i]\) 中的点,下文把这六个点称为关键点。
“当以点 \(rt_i\) 为根时,编号在 \([l_i,r_i]\) 中的点的 \(\text{LCA}\)”是“当以点 \(rt_i\) 为根时,这六个关键点的 \(\text{LCA}\)”。
-
LCA点集
给定一个点集V,求满足\(\forall u,v\in V,\text{LCA}(u,v)\in V'\)的点集V'。
按照时间戳将V中的点排序,排序后将相邻两个节点(包括首尾)的lca加入V',最后对V'去重。
性质:
- \(\forall u,v\in V\cup V',\text{LCA}(u,v)\in V'\)。
- V'的大小是\(O(|V|)\)。
- 《8.5.拓展应用5.》。
-
求树上包含给定点集V的连通块的边集的总长度最小值
按照时间戳将V中的点排序,排序后累加相邻两个节点(包括首尾)之间的路径长度为res,答案=res/2。
-
树上倍增法
例题:次小生成树
-
求三个点的“集合点”,要求从三个点走到“集合点”的路径不重合:“集合点”是三点的两两间深度最大的lca处。
-
公式:对于已按入栈序排列的若干个点\(a_1,a_2,\cdots,a_n\)在树上构成的最小连通块,这些点在树上构成的最小连通块大小\(=\sum\limits_{i=1}^{n}dep[a_i]-\sum\limits_{i=2}^{n}dep[\text{LCA}(a_{i-1},a_{i})]-dep[\text{LCA}(a_1,a_2,\cdots,a_n)]+1\)。
Bonus:如何用线段树维护?在入栈序上建立线段树,线段树上第i个叶子节点代表入栈序中第i个原图节点的信息。上面式子的第一部分很好维护。对于第二、三部分,线段树新建mi、ma信息储存当前区间入栈序最小、大的原图节点编号,在pushup中,\(tr[u].w_2=tr[tr[u].lson].w_2+dep[lca(tr[tr[u].lson].ma,tr[tr[u].rson].mi)]+tr[tr[u].rson].w_2,tr[u].w_3=dep[lca(tr[u].mi,tr[u].ma)]\)。把关键点依次插入即可。

浙公网安备 33010602011771号