26.LCA问题

一、问题简介

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。为了方便,我们记某点集 S = { v 1 , v 2 , … , v n } S=\{v_1,v_2,\ldots,v_n\} S={v1,v2,,vn}的最近公共祖先为 L C A ( S ) LCA(S) LCA(S) L C A ( v 1 , v 2 , . . . , v n ) LCA(v_1,v2,...,v_n) LCA(v1,v2,...,vn)

除了暴力沿着树根的方向一步步往上以外,常见的求解 LCA 问题的算法有三种,分别是倍增算法、离线tarjan算法、树链剖分算法。它们各有优缺点,需要在不同的时候选择恰当的算法进行使用。

二、倍增求LCA

1.倍增算法

倍增算法,顾名思义就是翻倍。它是一种和二分法思路相反的高效算法。二分法是每次缩小一半,从而以 O ( log ⁡ n ) O(\log n) O(logn)的速度定位到问题的解。而倍增算法是每次扩大一倍,以 O ( 2 n ) O(2^n) O(2n)的速度逼近问题的解。它能够使线性的处理转化为对数级的处理,大大地优化时间复杂度。

这个方法在很多算法中均有应用,其中最常用的有两大类:

  • ST 表求解 RMQ 问题、后缀数组
  • LCA 问题、快速幂

2.求解LCA

  • 预处理出 p a r e n t i , l e v e l parent_{i,level} parenti,level数组,表示点 i i i 的第 2 l e v e l 2^{level} 2level 个祖先。初始值可以通过 dfs 确立父子关系,随后通过倍增的方式求出祖先关系。通过这个数组,我们可以在树上快速移动,大幅减少了跳转次数。我们将在树上跳转的过程分为两个阶段来进行。
  • 在跳转的第一阶段中,我们要将 x , y x,y x,y 两点跳转到同一深度。我们可以计算出 x , y x,y x,y 两点的深度之差,设其为 Δ d \Delta d Δd。通过将 Δ d \Delta d Δd 进行二进制拆分,我们将 Δ d \Delta d Δd 次游标跳转优化为 Δ d \Delta d Δd 的二进制表示中,所含 1 1 1 的个数次跳转。
  • 在第二阶段中,我们从最大的 j j j 开始循环尝试,一直尝试到 0,如果 p a r e n t x , j ≠ p a r e n t y , j parent_{x,j}\not=parent_{y,j} parentx,j=parenty,j,则 x ← p a r e n t x , j , y ← p a r e n t y , j x\leftarrow parent_{x,j},y\leftarrow parent_{y,j} xparentx,j,yparenty,j。等到 j = 0 j=0 j=0时,最后的 LCA 为 p a r e n t x , 0 parent_{x,0} parentx,0
  • 预处理时间复杂度为 O ( n log ⁡ n ) O(n\log n) O(nlogn),单次查询时间复杂度为 O ( log ⁡ n ) O(\log n) O(logn)

3.模板代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>

using namespace std;
typedef int ll;
const ll maxn=500010;
struct node
{
	ll v;
	ll next;
}e[2*maxn];
ll p[maxn],t=0;
ll n,m,s;
ll parent[maxn][30],d[maxn];
void insert(ll u,ll v)
{
	e[t].v=v;
	e[t].next=p[u];
	p[u]=t++;
}
void dfs(ll u,ll pre)
{
	for(ll i=p[u];i!=-1;i=e[i].next)
	{
		ll v=e[i].v;
		if(d[v]==-1)
		{
			parent[v][0]=u;
			d[v]=d[u]+1;
			dfs(v,u);
		}
	}
}
ll LCA(ll x,ll y)
{
	ll i;
	if(d[x]<d[y])
		swap(x,y);
	for(i=0;(1<<i)<=d[x];i++)
		;
	i--;
	for(ll j=i;j>=0;j--)
		if(d[x]-(1<<j)>=d[y])
			x=parent[x][j];
	if(x==y)
		return x;
	for(ll j=i;j>=0;j--)
	{
		if(parent[x][j]!=parent[y][j])
        {
            x=parent[x][j];
            y=parent[y][j];
        }
	}
	return parent[x][0];
}
int main()
{
	memset(d,-1,sizeof(d));
	memset(p,-1,sizeof(p));
	scanf("%d%d%d",&n,&m,&s);
	for(ll i=1;i<=n-1;i++)
	{
		ll u,v;
		scanf("%d%d",&u,&v);
		insert(u,v);
		insert(v,u);
	}
	d[s]=0;
	dfs(s,-1);
	for(ll level=1;(1<<level)<=n;level++)
		for(ll i=1;i<=n;i++)
			parent[i][level]=parent[parent[i][level-1]][level-1];
	for(ll i=1;i<=m;i++)
	{
		ll x,y;
		scanf("%d%d",&x,&y);
		ll lca=LCA(x,y);
		printf("%d\n",lca);
	}
	
	return 0;
}

三、离线tarjan算法

四、树链剖分算法

1.算法简介

有时候因为题目特性,倍增求 LCA 可能会比较慢。而由于一些强制在线的原因, tarjan 离线的方法求 LCA 的方法也不再适用,这时候就需要下面讲到的树链剖分算法进行求解了。主要思想是利用轻重链的划分来快速在树上进行跳转。

2.算法分析

  • 按照深度优先遍历一遍整棵树,对于每个结点,选取它的子节点中以这个子节点为根的子树 s i z e size size 最大的那一个,将这个子节点和他的父节点连接起来,表示他们在同一条重链上。
  • 在查询两个节点的LCA时,如果当前两个节点不在同一重链上,则将深度更大的结点跳转到重链的顶端。
  • 重复上面操作 2 2 2直到两个节点处于同一重链,这时候因为两点在同一条链上,则返回深度更低的那一个,即两个点的LCA。

3.模板程序

#include <cstdio>
#include <iostream>
#include <cstring>

using namespace std;
typedef long long ll;
const ll maxn=500010;
struct node
{
    ll v;
	ll next;
}e[maxn*2];
ll p[maxn],t=0;
ll d[maxn],size[maxn],son[maxn],top[maxn],fa[maxn];
ll n,m,S;
void insert(ll u,ll v)
{  
	e[t].v=v;
	e[t].next=p[u]; 
    p[u]=t++;  
}  
void dfs1(ll u)
{  
    size[u]=1;
    for(ll i=p[u];i!=-1;i=e[i].next)  
    {
        ll v=e[i].v;  
        if(fa[u]!=v)
		{
            fa[v]=u;  
            d[v]=d[u]+1;  
            dfs1(v);  
            size[u]+=size[v];  
            if(size[son[u]]<size[v])//son[u]表示结点u的儿子中以其为根的子树size最大的那个(用来构造重链)
		        son[u]=v;
        }  
    }  
}  
void dfs2(ll u)
{  
    if(u==son[fa[u]])//如果u是fa[u]的儿子中拥有最大size的那个
        top[u]=top[fa[u]];//把top[u]连向top[fa[u]](形成重链)   
    else
		top[u]=u;//否则指向它自己   
    for(ll i=p[u];i!=-1;i=e[i].next)
        if(e[i].v!=fa[u])
          dfs2(e[i].v);
}
ll LCA(ll x,ll y)
{  
    while(top[x]!=top[y])//x,y不在同一条重链上时
	{
		if(d[top[x]]>d[top[y]])//将深度大的上提
        	x=fa[top[x]];
		else
			y=fa[top[y]];
    }
    if(d[x]<d[y])//返回x,y中在较上方的那个
    	return x;
    return y;
}
int main()
{
    memset(p,-1,sizeof(p));
    scanf("%lld%lld%lld",&n,&m,&S);
    for(ll i=1;i<n;i++)
	{
        ll u,v;
        scanf("%lld%lld",&u,&v);
        insert(u,v);
        insert(v,u); 
    }
    d[S]=1;
    dfs1(S);//预处理d和fa
    dfs2(S);//预处理top
    for(ll i=1;i<=m;i++)
	{
        ll u,v;
        scanf("%lld%lld",&u,&v);
        printf("%lld\n",LCA(u,v));
    }
    
    return 0;
}

四、习题

P3379 【模板】最近公共祖先(LCA)

P2420 让我们异或吧

P5836 [USACO19DEC]Milk Visits S

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值