最近公共祖先(Lowest Common Ancestor)
定义:
两个节点的祖先相同,则叫该节点的公共祖先。
距离这两个节点最近的公共祖先,称为最近公共祖先。
朴素
采用两点逐渐向根移动的方法,求出LCA。
具体步骤:
1.求出每个节点的深度
2.询问两个节点是否重合,若重合,则LCA已经求出。否则,选择两个点中深度较大一个,移动它的父亲。
重复第二步,直到求出结果。
时间复杂度 O ( n ) O(n) O(n),查询 q q q次的话,复杂度 O ( q n ) O(qn) O(qn)
优点:简单,该算法允许树动态改变。
代码如下:
struct EDGE{int to,nxt;}e[MAXN];
void addedge(int u,int v){e[++cnt].to=v; e[cnt].nxt=adj[u]; adj[u]=cnt;}//邻接表
//建树的过程
//同时求出每个节点的深度,与每个节点的父亲
void dfs(int u,int father)
{
fat[u]=father; deep[u]=deep[father]+1;
for(int i=adj[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v!=father) dfs(v,u);
}
return ;
}
//相当于模拟移动
int LCA(int u,int v)
{
while(u!=v)
{
if(deep[u]>=deep[v]) u=fat[u];
else v=fat[v];
}
return u;
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=n-1;++i)
{
int u,v;scanf("%d%d",&u,&v);
addedge(u,v); addedge(v,u);
}
dfs(s,s);
for(int i=1;i<=m;++i)
{
int a,b; scanf("%d%d",&a,&b);
printf("%d\n",LCA(a,b));
}
return 0;
}
用处:实现简单,可以用于程序对拍。
倍增LCA
第一步:初始化(建树)
求出倍增数组 a n c anc anc。定义, a n c [ i ] [ j ] anc[i][j] anc[i][j]为 i i i 节点向上走 2 j 2^j 2j 后能走到的点。
(和一开始建树的思路一样)
其中, M A X L O G MAXLOG MAXLOG 是最小的满足 2 x ≤ M A X D E E P 2^x \leq MAXDEEP 2x≤MAXDEEP(最深的节点深度)的 x x x。
( M A X L O G MAXLOG MAXLOG 是自己定义的常数, M A X D E E P MAXDEEP MAXDEEP 是根据题目要求来的。)
我们规定根节点的父亲就是它自己,这样根节点往上走还是根节点。
对于 a n c anc anc 而言,有如下的递推关系:
a n c [ i ] [ j ] = { a n c [ i ] [ j ] j = 0 a n c [ a n c [ i ] [ j − 1 ] ] [ j − 1 ] j > 1 anc[i][j]=\left\{\begin{matrix} anc[i][j] & j=0 \\ anc[ anc[i][j-1] ][j-1]& j > 1 \end{matrix}\right. anc[i][j]={anc[i][j]anc[anc[i][j−1]][j−1]j=0j>1
解释:
第一个, j = 0 j=0 j=0就是根节点。
第二个,从 a n c anc anc的意义出发:节点 i i i向上走 2 j − 1 2^{j-1} 2j−1步后,再走 2 j − 1 2^{j-1} 2j−1步,加起来就是 2 j 2^j 2j次步。
(根据定义:2式表示,节点 a n c [ i ] [ j − 1 ] anc[i][j-1] anc[i][j−1] 向上走了 2 j − 1 2^{j-1} 2j−1,而 a n c [ i ] [ j − 1 ] anc[i][j-1] anc[i][j−1] 表示 i i i 向上走 2 j − 1 2^{j-1} 2j−1 ,所以i节点一共向上走了 2 j 2^j 2j ,完成递推。)
第二步:把两个节点移动到同一深度
假设 L C A ( x , y ) LCA(x,y) LCA(x,y)不失一般性,不妨令 d e e p [ x ] ≥ d e e p [ y ] deep[x] \geq deep[y] deep[x]≥deep[y] 。(否则就交换 x x x, y y y 。)
先让 x x x 往上走 d e e p [ x ] − d e e p [ y ] deep[x]-deep[y] deep[x]−deep[y]步。
我们将这个差值转化为二进制,然后,通过通过倍增数组往上走
2
2
2的幂次步。
(即:对于二进制为
1
1
1的第
i
i
i位,往上走
2
i
2^i
2i步,这里调用之前的
a
n
c
anc
anc 数组)
(这里运用了结论:所以的整数,都可以写成2的幂次和。(二进制))
那么,就可以在 Θ ( l o g 2 n ) \Theta (log_2n) Θ(log2n)的时间复杂度内到达目标深度。
或者,还有一种方法:
从大到小扫描 i i i,如果 a n c [ x ] [ i ] anc[x][i] anc[x][i]的深度不小于 y y y,就跳 x x x。
第三步: 求LCA
在第二步的处理后,假设 x , y x,y x,y往上走最小的 L L L步后是同一节点
也就意味着, x , y x,y x,y向上走最大的 L − 1 L-1 L−1步,是不同的节点。
我们可以从大到小的枚举
2
i
2^i
2i步,如果当前
x
,
y
x,y
x,y向上走
2
i
2^i
2i步后为同一点,则停止。否则就一起往上走。
(由于倍增数组的特性,该过程可以看成二分)
直到不能走为止,再往上走一步就是 L C A LCA LCA。(PS: 2 0 = 1 2^0=1 20=1)
时间复杂度: O ( ( n + q ) l o g 2 ( n ) ) O((n+q)log_2(n)) O((n+q)log2(n))
优点:在线查询,支持动态树。
代码如下:
#include <bits/stdc++.h>
#define MAXN 10000005
#define MAXLOG 32
using namespace std;
int n,m,s;
int deep[MAXN],anc[MAXN][MAXLOG];
int cnt=0,adj[MAXN];
struct EDGE{int to,nxt;} e[MAXN];
void addedge(int u,int v){e[++cnt].to=v; e[cnt].nxt=adj[u]; adj[u]=cnt;}
void dfs(int u,int father)
{
anc[u][0]=father; deep[u]=deep[father]+1;
for(int i=1;i<MAXLOG;++i) anc[u][i]=anc[ anc[u][i-1] ][i-1];
for(int i=adj[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v!=father) dfs(v,u);
}
return ;
}
int LCA(int u,int v)
{
if(deep[u]<deep[v])
swap(u,v);
int h=deep[u]-deep[v];
for(int i=0;i<MAXLOG;++i) //上文的方法一 (判断分别是2的几次幂,详情看位运算)
{
if( h&(1<<i) )
{
u=anc[u][i];
}
}
if(u==v) return u;
for(int i=MAXLOG-1;i>=0;--i) // -1!!!
{
if(anc[u][i]!=anc[v][i])
{
u=anc[u][i];
v=anc[v][i];
}
}
return anc[u][0];
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=n-1;++i)
{
int u,v;scanf("%d%d",&u,&v);
addedge(u,v); addedge(v,u);
}
dfs(s,s);
for(int i=1;i<=m;++i)
{
int a,b; scanf("%d%d",&a,&b);
printf("%d\n",LCA(a,b));
}
return 0;
}
方法2:变的部分只有标出的方法1循环的部分
for(int i=MAXLOG-1;i>=0;--i)
if(deep[ anc[u][i] ] >= deep[v] )
u=anc[u][i];