最近公共祖先(朴素法和两种倍增法)

最近公共祖先(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 2xMAXDEEP(最深的节点深度)的 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][j1]][j1]j=0j>1

解释:

第一个, j = 0 j=0 j=0就是根节点。

第二个,从 a n c anc anc的意义出发:节点 i i i向上走 2 j − 1 2^{j-1} 2j1步后,再走 2 j − 1 2^{j-1} 2j1步,加起来就是 2 j 2^j 2j次步。

(根据定义:2式表示,节点 a n c [ i ] [ j − 1 ] anc[i][j-1] anc[i][j1] 向上走了 2 j − 1 2^{j-1} 2j1,而 a n c [ i ] [ j − 1 ] anc[i][j-1] anc[i][j1] 表示 i i i 向上走 2 j − 1 2^{j-1} 2j1 ,所以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 L1步,是不同的节点。

我们可以从大到小的枚举 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];

测试网址:P3379 【模板】最近公共祖先(LCA)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值