【算法详解】LCA(最近公共祖先)

定义:

Lca(最近公共祖先) 指在一棵有根树中任意 2 2 2个节点 u , v u,v u,v最近的公共祖先。
如下图:
【算法详解】LCA(最近公共祖先)_第1张图片
如右图,结点 4 , 6 4,6 46的公共祖先有 1 、 2 1、2 12,但最近的公共祖先是 2 2 2,即 L c a ( 4 , 6 ) = 2 Lca(4,6) = 2 Lca(4,6)=2
如何求得 u , v u,v u,v的最近公共祖先呢?

算法一:暴力

现在有一个最朴素的算法,暴力。

  • 1 1 1.u,v中深度大的往上走,直到 u , v u,v uv深度相同。若此时 u = = v u==v u==v,则已找到,输出 u u u
  • 2 2 2.如果深度相同 u , v u,v u,v却没有相等,就令 u , v u,v uv一起往上走,直到走到同一个结点,输出 u u u
算法时间复杂度为 O ( n ) O(n) O(n)

代码如下:

int Lca(int u,int v)
{
	//fa 表示每个节点的父亲
	//dep 表示每个节点深度 
	//dep,fa数组都可以预处理出来 
	if (fep[u]>dep[v]) swap(u,v);
	while (dep[v]>dep[u]) v=fa[v];
	//令v,u在同一深度 
	if (u==v) return u;
	while (u!=v)
	{
		u=fa[u];
		v=fa[v];	
	} 
	return u;
}

算法二:倍增法

思想:注意到 u , v u,v uv向上走到最近公共祖先 w w w之前, u , v u,v uv所在的结点不相同。而到达最近公共祖先 w w w后,再往上走仍是 u , v u,v uv的公共祖先,即 u , v u,v uv走到同一个结点。这具有二分性质。于是我们可以预处理出一个 2 k 2^{k} 2k的表, f a [ k ] [ u ] fa[k][u] fa[k][u]表示u往上走 2 k 2^k 2k步走到的结点,令根结点深度为 0 0 0,则 2 k > d e p [ u ] 2^k>dep[u] 2k>dep[u]时,令 f a [ k ] [ u ] = − 1 fa[k][u]=-1 fa[k][u]=1(不合法情况的处理)。

详解:不妨假设 d e p [ u ] < d e p [ v ] dep[u] < dep[v] dep[u]<dep[v]

  • 那么 v v v往上走 d = d e p [ v ] − d e p [ u ] d = dep[v] - dep[u] d=dep[v]dep[u]步,此时 u , v u,v uv所在结点深度相同。
  • 该过程可用二进制优化。由于d是确定值,将d看成2的次方的和值, d = 2 k 1 + 2 k 2 + . . . + 2 k m d = 2^{k1} + 2^{k2} + ... + 2^{km} d=2k1+2k2+...+2km,利用 f a fa fa数组,如 v = f a [ k 1 ] [ v ] v = fa[k1][v] v=fa[k1][v] v = f a [ k 2 ] [ v ] v = fa[k2][v] v=fa[k2][v]加速向上移动。
  • 若此时 u = = v u == v u==v,说明 L c a ( u , v ) Lca(u,v) Lca(u,v)已找到。
  • 利用 f a fa fa数组加速 u , v u,v uv一起往上走到最近公共祖先 w w w的过程。令 d = d e p t h [ u ] − d e p t h [ w ] d = depth[u] - depth[w] d=depth[u]depth[w],虽然 d d d是个未知值,但依然可以看成2的次方的和。从高位到低位枚举 d d d的二进制位,设最低位为第0位,若枚举到第k位,有 f a [ k ] [ u ] ! = f a [ k ] [ v ] fa[k][u] != fa[k][v] fa[k][u]!=fa[k][v],则令 u = f a [ k ] [ u ] , v = f a [ k ] [ v ] u = fa[k][u],v = fa[k][v] u=fa[k][u]v=fa[k][v]。最后最近公共祖先 w = f a [ 0 ] [ u ] = f a [ 0 ] [ v ] w = fa[0][u] = fa[0][v] w=fa[0][u]=fa[0][v],即 u u u v v v的父亲。

注意

  • 1.因为根节点没有父亲,因此到了根节点不能再通过 f a fa fa数组向上移动,所以令 f a [ 0 ] [ r o o t ] = − 1 fa[0][root]=-1 fa[0][root]=1.
  • 2.如果当前节点通过 f a fa fa数组移动后,超过了根节点(只可能到达根节点的父亲),那么这也是一种不合法的移动,需标记为 − 1 -1 1
时间复杂度: 预处理为 O ( n l o g n ) O(n log n) O(nlogn),每次查询为 O ( l o g n ) O(log n) O(logn).

代码如下:

void dfs(int x,int y)
{
	dep[x]=y;
	for (int i=head[x];i;i=Next[i])
	{
		int l=ver[i];
		if (l==fa[0][x]) continue;
		fa[0][l]=x;
		dfs(l,y+1);
	}
}
void init()
{
	fa[0][1]=-1;
	dfs(1,0);
	for (int i=1;(1<<i)<n;i++)
		for (int j=1;j<=n;j++)
			if (fa[i-1][j]<0) fa[i][j]=-1; 
				else fa[i][j]=fa[i-1][fa[i-1][j]];
}
inline int lca(int u,int v)
{
	if (dep[u]>dep[v]) swap(u,v);
	for (int i=0,d=dep[v]-dep[u];d;i++,d>>=1)
		if (d&1) v=fa[i][v];
	if (u==v) return u;
	for (int i=25;i>=0;i--)
	if (fa[i][v]!=fa[i][u])
	{
		v=fa[i][v];
		u=fa[i][u];
	}
	return fa[0][u];
}

算法三:tarjan离线算法

思想

  • 离线算法就是先把所有询问存起来,一次处理完,最后输出。而在线算法就是即询问即计算,前面两个算法都是在线算法。
  • 离线Tarjan算法基于这样一个事实。

详解

  • 要找 w = L c a ( u , v ) w=Lca(u,v) w=Lca(u,v),在 d f s dfs dfs遍历完 u u u到遍历完 v v v的过程中,遍历到 v v v时, u u u w w w路径上除 w w w外结点的子树都遍历过了, w w w的子树还未遍历完。如果对于结点 u u u,访问完它的子树后就把u在并查集中的父亲设为它在树中的父亲,那么访问到 v v v u u u在并查集中的父亲就是 L c a ( u , v ) Lca(u,v) Lca(u,v)

举个栗子






以上图片来自Spana。

代码如下:

void Tarjian(int x)
{
    fa[x]=x;
    vis[x]=1;
    for(int i=link1[x];i;i=a[i].next)
    {
        if(!vis[a[i].y])
        {
            Tarjian(a[i].y);
            fa[a[i].y]=x;
       }
    }
    for(int i=link2[x];i;i=b[i].next)
    if(vis[b[i].y])
    ans[b[i].id]=getfa(b[i].y);
}
离线tarjan算法复杂度为 O ( n + q ) O(n+q) O(n+q) n n n为结点个数, q q q为询问个数.

还有一个基于Rmq和欧拉序的算法,但是我不会,请自找dalao博客学习。

习题:

  • [POJ 1330]Nearest Common Ancestors
  • [HDU 2586]How far away ?(模板)
  • [BZOJ 1787][AHOI 2008]紧急集合
  • [LightOJ 1128]Greatest Parent
  • [BZOJ 2144]跳跳棋(难)
  • [NOIP 2013]火车运输
  • [UVA 11354]Bond
注:

对于有些题,可参考我的博客:https://blog.csdn.net/zhuchangcheng1003
当然,对于另一些题,我没写博客(懒,难),请请教其他dalao的博客。
如:
https://blog.csdn.net/huang_ke_hai
https://blog.csdn.net/hzk_cpp?utm_source=feed

你可能感兴趣的:(算法详解)