填坑行动8-最近公共祖先LCA(树上倍增) 学习笔记
板子题
题目描述
如题,给定一棵有根多叉树,请求出指定两个点直接最近的公共祖先。
输入格式
第一行包含三个正整数\(N,M,S\)分别表示树的结点个数、询问的个数和树根结点的序号。
接下来\(N−1\)行每行包含两个正整数\(x, y\)表示\(x\)结点和\(y\)结点之间有一条直接连接的边(数据保证可以构成树)。
接下来\(M\)行每行包含两个正整数\(a,b\),表示询问\(a\)结点和\(b\)结点的最近公共祖先。
输出格式
输出包含\(M\)行,每行包含一个正整数,依次为每一个询问的结果。
输入输出样例
输入 #1
5 5 4
3 1
2 4
5 1
1 4
2 4
3 2
3 5
1 2
4 5
输出 #1复制
4
4
1
4
4
说明/提示
对于\(30\%\)的数据,\(N\leq10,M\leq 10\)。
对于\(70\%\)的数据,\(N\leq 10000,M\leq 10000\)。
对于\(100\%\)的数据,\(N\leq 500000,M\leq 500000\)。
样例说明:
该树结构如下:
第一次询问:\(2,4\)的最近公共祖先,故为\(4\)。
第二次询问:\(3,2\)的最近公共祖先,故为\(4\)。
第三次询问:\(3,5\)的最近公共祖先,故为\(1\)。
第四次询问:\(1,2\)的最近公共祖先,故为\(4\)。
第五次询问:\(4,5\)的最近公共祖先,故为\(4\)。
故输出依次为\(4,4,1,4,4\)。
算法解析
首先大家都可以想到,这道题目可以使用\(\operatorname{dfs}\)来解决这个问题,但是,我们很容易得出这个算法并不能A掉这道题目,\(\operatorname{dfs}\)的算法复杂度是\(\Theta\left(NM\right)\),超时。
我们来分析一下\(\operatorname{dfs}\)的步骤:
- 将较低的节点提升,使两个节点在同一高度。
- 将两个节点提升,直到重合。
我们发现在上述过程中,都需要将两个节点向上提,最坏情况下提\(N\)次,那么怎样才能吧节点提起更快呢?也就是一次不仅仅提一个。
倍增就可以做到。我们学过RMQ问题的同学都知道,RMQ使在一条链上进行倍增,而倍增版本的LCA也是相同的原理。令\(f_{i,j}\)为节点\(i\)向上提\(2^j\)个节点是多少,不难得出递推公式:$$\large f_{i,j}=f_{f_{i,j-1},j-1}$$
写在代码中就是:f[i][j]=f[ f[i][j-1 ][j-1]
解读:相当于节点\(i\)向上提\(2^j\)次就相当于先提\(2^{j-1}\)次,在提\(2^{j-1}\)次即可。
预处理代码如下:
void dfs(int num,int pre){
f[num][0]=pre;
deep[num]=deep[pre]+1;
for(int i=1;i<=t;i++)
f[num][i]=f[f[num][i-1]][i-1];
for(int i=head[num];i;i=nex[i])
if(to[i]!=pre)
dfs(to[i],num);
}
得到了\(f_{i,j}\),我们就可以得出\(\operatorname{LCA}\left(a,b\right)\)了,具体做法和dfs一样,但是需要注意两点:
- 在提节点的时候要注意从大到小计算。
- 当两个节点位于同一高度时,要特判两个点是否重合。
- 注意在向上提两个点的时候,需要让两个点不相等,不然会提过头。
然后就是LCA的代码了:
int lca(int x,int y){
if(deep[x]<deep[y]){
x^=y;
y^=x;
x^=y;
}//交换
if(deep[x]>deep[y]){
for(i=t;i>=0;i--)
if(deep[f[x][i]]>=deep[y]&&f[x][i]!=0)
x=f[x][i];
//if(deep[x]!=deep[y]) printf("*"); //翻车标记
}
if(x==y) return x;
for(i=t;i>=0;i--)
if(f[x][i]!=f[y][i]&&f[x][i]!=0&&f[y][i]!=0)
x=f[x][i],y=f[y][i];
//if(f[x][0]!=f[y][0]) printf("*"); //翻车标记
return f[x][0];
}
最后是完整的AC代码:
#include<cstdio>
#include<cmath>
#define maxn 500039
using namespace std;
int head[maxn],nex[maxn<<1],to[maxn<<1],k;
#define add(x,y) nex[++k]=head[x];\
head[x]=k;\
to[k]=y;
int u,v,f[maxn][20],deep[maxn];
int root,n,m,i,j,T,t;
void dfs(int num,int pre){
f[num][0]=pre;
deep[num]=deep[pre]+1;
for(int i=1;i<=t;i++)
f[num][i]=f[f[num][i-1]][i-1];
for(int i=head[num];i;i=nex[i])
if(to[i]!=pre)
dfs(to[i],num);
}
int lca(int x,int y){
if(deep[x]<deep[y]){
x^=y;
y^=x;
x^=y;
}//交换
if(deep[x]>deep[y]){
for(i=t;i>=0;i--)
if(deep[f[x][i]]>=deep[y]&&f[x][i]!=0)
x=f[x][i];
//if(deep[x]!=deep[y]) printf("*"); //翻车标记
}
if(x==y) return x;
for(i=t;i>=0;i--)
if(f[x][i]!=f[y][i]&&f[x][i]!=0&&f[y][i]!=0)
x=f[x][i],y=f[y][i];
//if(f[x][0]!=f[y][0]) printf("*"); //翻车标记
return f[x][0];
}
int main(){
scanf("%d%d%d",&n,&T,&root);
for(i=1;i<n;i++){
scanf("%d%d",&u,&v);
add(u,v);
add(v,u);
}
deep[0]=-1;
t=(int)log2(n)+1;
dfs(root,0);
int x=0,y=1;
while(T--){
scanf("%d%d",&u,&v);
printf("%d\n",lca(u,v));
}
return 0;
}
都0202年了应该没人用邻接矩阵的吧。
关于LCA的拓展
LCA不仅仅能处理最近公共祖先,而且还可以处理两个点到最短公共祖先的距离、最短路、最大最小点权、最大最小边……这里就不一一叙述了。