LCA+倍增浅析

介绍:

LCA即为求在一棵树上的两点a,b的最近公共祖先。

最近公共祖先是什么?

顾名思义,如果有一个点x是a和b的祖先,且x到a和b的距离最短,那么x就是a,b的最近公共祖先。


应用:

LCA多用于求树上两点间最短路径(即为不重复路径)上的权值和、最值问题。

为什么?

因为最短路径(不重复路径)说明了这条路径要经过它们的最近公共祖先,也可以理解为从它们的最近公共祖先出发分别走到它们自己。


LCA实现:

考虑搜索记录下a,b两点的深度。

如果a的深度>b的深度,则不断把a上升到它父亲,直到a的深度=b的深度。

反之,如果b的深度>a的深度,则不断把b上升到它父亲,直到b的深度=a的深度。


好了,我们现在已经使a和b处在同一深度。

我们现在要做的就是,把a和b每次各自上升到它们的父亲,直到它们相同。

那么a,b相同之后最近公共祖先就是a(或者说是b)了。


但是这样一个个往上升会很慢,怎么办呢?

我们可以考虑使用倍增。


倍增的思想及自创的伪倍增:

既然一个个往上升会很慢,那么我们如果能几个几个甚至几百几千地往上升,就会快很多了。

——这便是倍增的思想。


但是,如果我们想一次往上升x个位置,就需要知道每个点的第x个祖先是多少。

怎么去记录呢?


在还没系统地学倍增之前,我是这样想的:

比如说树的深度是100000,

我们先记录每个点的第10000个祖先是什么,第1000个祖先是什么,第100个祖先是什么,第10个祖先是什么,以及第1个祖先(它的父亲)是什么。


至于如何去记录,可以在用dfs建树的过程中开个数组,记录从根节点到当前节点所经过的点(也就是当前点的所有祖先),然后每次把当前点的第10000个祖先、第1000个祖先、第100个祖先、第10个祖先、第1个祖先记录下来。


然后我们往上升的时候就可以先一万个一万个地升,当它离它的目标深度差小于一万时再一千个一千个地升,以此类推,最后当它离它的目标深度小于10时一个一个地升,用几个while循环去实现。


这样做LCA的时候,我们就可以在n*log10(n)内求出它的最近公共祖先。

至于这样做LCA的具体操作实现,因为与下文的真正倍增大体相同,在这里就不讲了,留到下文再讲。


但是,通常LCA的应用不会让你只去求两点的最近公共祖先,还可能会让你求树上两点间最短路径的权值和、最小最大权值。


而这种倍增只能通过记录从根节点到每个节点的前缀和求出路径中的权值和,却无法求出最小最大权值。

所以,我管这种不能记录最值的方法叫伪倍增。

那么,如何去解决求树上两点间最短路径的最小最大权值呢?

就要用到真正的倍增了。


真正的倍增:

真正的倍增的思想类似于RMQ。

我们设f[i][j]表示点i的第2^j个祖先。

因为2^(j-1)*2=2^j,

如图,所以对于点i来说,它的第2^j个祖先就等于它的第2^(j-1)个祖先的第2^(j-1)个祖先。


所以我们就可以得到转移方程f[i][j]=f[f[i][j-1]][j-1]。

因为转移方程中f[i][j-1]>i,所以按j来分层,转移的时候先从小到大枚举j,再从小到大枚举i。


如果我们要求出路径中的最小值(最大值同理),

那么也一样可以类似地设g[i][j]表示从第i个点到第i个点的第2^j个祖先这段路径的最小值。

转移方程同理:g[i][j]=min(g[i][j-1],g[f[i][j-1]][j-1]);

就是说把它分成两半,再从这两半的答案中取个最小值。


如果说要求出路径中的权值和也同理,

g[i][j]=g[i][j-1]+g[f[i][j-1]][j-1];


好了,到现在为止我们已经预处理得到了f数组和g数组,

那么,如何通过倍增做LCA呢?


LCA+倍增的实现:

前面我们已经知道了普通LCA的做法:

1、先调深度,把深度调至相同;

2、两点同时上升,直到它们相同。


那么,先看调深度,我们如何用倍增实现调深度呢?

我们假设两点x,y中较深的那个点是x,它们的深度差为t。

那就相当于我们要把点x上升到它的第t个祖先。

我们设一个k,使得2^k<=t,每次x=f[x][k];  t-=2^k; k--; 

可以用while循环去实现。

打一下核心代码:

k=30;
while (t>0)
{
    if ((1<<k)<=t) 
    {
         t-=1<<k;
         x=f[x][k];
    }
    k--;
}

如果有求和求最值操作的,在里面把那个g数组加上去就好了。


好了,现在深度已经相同了,我们转入操作2:两点同时上升,直到它们相同。

我们还是设一个k,使得深度deep>(1<<k)。

如果f[x][k]=f[y][k]就表示当前如果往上跳的话可能会跳过它们的最近公共祖先,所以就不跳,k--;

否则如果f[x][k]!=f[y][k]就跳,因为这样肯定不会超过它的最近公共祖先。

还是打一个伪代码:

k=30;
while (k>=0)
{
     if (deep>(1<<k) && f[x][k]!=f[y][k])
     {
          deep-=(1<<k);
          x=f[x][k];
          y=f[y][k];     
     }
     k--;
}

这样做完以后,x,y都成了它们最近公共祖先的直系儿子,所以最近公共祖先就是现在x的父亲(或者说是y的父亲)。


所以呢,LCA和倍增就讲完了,

如果讲解内容或伪代码有任何问题,欢迎留言提醒,

如果觉得写得好,欢迎点赞和转发,

谢谢!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值