LCA (最近公共祖先问题)

摘自kiana810学长的讲课内容

一.概述

LCA(Least Common Ancestors),即最近公共祖先,是指在有根树中,找出某两个结点u和v最近的公共祖先(另一种说法,离树根最远的公共祖先)。

二.方法

1.树上的路径:

众所周知,两点在树上的路径是唯一的,但是如何快速维护路径的相关信息(路径权值和、路径边权最大/最小值等)?
在解决这些问题之前,首先考虑一个问题:如何快速找到两点在树上的路径?
假设两个节点为x和y,若x是y的祖先或y是x的祖先,我们只需直接从深度较大的节点向上访问即可找到路径。
若没有这种祖先关系,则我们只要找到x和y的最近公共祖先,将两个节点到他们最近公共祖先的路径连接起来即可。

2.稀疏表与祖先:

与稀疏表类似(不知道稀疏表的自己查),我们不必保存每个节点的所有祖先信息,而是只保存其所有2^k倍祖先(倍增),换句话说,用fa[x][k]表示x的2^k倍祖先。

fa[x][0]:x的1倍祖先,即x的父节点。

fa[x][1]:x的2倍祖先,即x父节点的父节点

• fa[x][2]:x的4倍祖先,即x父节点的父节点的父节点的父节点。

• ………………

• 更新操作也很简单:fa[x][k+1]=fa[fa[x][k]][k]。

3.祖先的查询:

• 如何查询一个节点的k倍祖先?

• 方法:每次找到k的二进制表示中,从低到高第一个1所在的位置,假设是第x位,则先访问该节点的第2x倍祖先,以此类推

• 可能会用到lowbit(不知道的自己查!)lowbit(x)=x&(-x);

4.最近公共祖先:

• 如何查询两个节点的最近公共祖先?
• 直接做很困难,因为两个节点的一部分祖先之间没有联系,很难定位到他们的最近公共祖先,
• 想法:先将两个节点的深度变到相同,使得它们同时向上再走相同的深度后可以到达最近公共祖先节点,而我们只需要确定后者的深度即可。

5.最近公共祖先的实现

• 遵循以下步骤来找到两个节点x和y的最近公共祖先:
• ①不妨设x和y的深度分别为dep[x]和dep[y],且dep[x]<dep[y],则我们首先找到y的
dep[y]-dep[x]倍祖先,并将y移动到该位置,更新y的值,若此时y与x已经相等,则表
明x就是两者的最近公共祖先,不再执行下面的过程
• ②取k为满足下述条件的最大非负数:x和y的2
^k倍祖先不相等,然后将x和y都移动到
其2^k倍祖先的位置并更新他们的值,显然k可以枚举也可以二分。
• ③假设不存在满足条件的k,则说明此时x和y的父节点一定是同一个点,而这个点就
是两点的最近公共祖先。

三.例题

1.模板
#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
inline int read() 
{
    int x = 0, f = 1; char ch = getchar();
    while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
    while(ch >= '0' && ch <= '9') {x = x * 10 + ch - '0'; ch = getchar();}
    return x * f;
}
int n, m, root, cnt;
int depth[500010], head[500010], fa[500010][20];
bool vis[500010];
struct data 
{ 
	int to, next; 
}edge[500010 << 1];
inline void addedge(int u, int v) 
{
    edge[++cnt].to = v; edge[cnt].next = head[u];
    head[u] = cnt;
}
void dfs(int x) 
{
    vis[x] = true;
    for(int i = 1; i <= 18; ++i) {
        if(depth[x] < (1 << i)) break;
        fa[x][i] = fa[fa[x][i - 1]][i - 1];
    }
    for(int i = head[x]; i; i = edge[i].next) {
        if(vis[edge[i].to]) continue;
        depth[edge[i].to] = depth[x] + 1;
        fa[edge[i].to][0] = x;
        dfs(edge[i].to);
    }
}
inline int lca(int x, int y)
{
    if(depth[x] < depth[y]) swap(x, y);
    int d = depth[x] - depth[y];
    for(int i = 0; i <= 18; ++i)
        if((1 << i) & d) x = fa[x][i];
    for(int i = 18; i >= 0; --i)
        if(fa[x][i] != fa[y][i])
		{
            x = fa[x][i];
            y = fa[y][i];
        }
    if(x == y) return x;
    else return fa[x][0];
}
int main() {
    n = read(), m = read(), root = read();
    for(int i = 1, u, v; i < n; ++i) 
	{
        u = read(), v = read();
        addedge(u, v);
        addedge(v, u);
    }
    dfs(root);
    for(int i = 1, x, y; i <= m; ++i) 
	{
        x = read(), y = read();
        printf("%d\n", lca(x, y));
    }
    return 0;
}
2.bzoj 1787 [Ahoi2008] Meet 紧急集合(之前发过)
http://blog.csdn.net/paperback_writer/article/details/76631198
谢谢观看~
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值