P3379 【模板】最近公共祖先(LCA)【在线算法详解】

一、倍增

对于结点u和v,不妨设deep[u]>deep[v]

(1)、先向上跳u使得它们处于同一深度:deep[v];

(2)、然后再结点u和结点b一起跳,跳它们最近公共祖先。

  • 这里所谓“跳”是指往上跳2的次幂的高度。设f[ u ][ i ]表示距结点u高度差为 2^i 的祖先。
  • f[ u ][ i ] = f[ f[ u ][ i - 1 ] ][i - 1],其中f[ u ][ 0 ] = fa[ u ].
  • 只需要对有根树跑一遍dfs就可以得到f[ u ][ 0 ],然后再根据方程一层层推出其余的次幂祖先。

相关理论

  1. 我们知道任何一个正整数都可以表示成二进制。根据正整数拆分理论,不管深度差是多少,二者最后一定能处于同一深度。【这个保证了上面所说的第一“跳”一定可以实现】
  2. 如果u 和 v的最近公共祖先是root,那么root的祖先全部都是u和v的公共祖先,只是不能保证是最近的。【所以第二跳的时候,我们每次往上跳的高度要保证使得f[ u ][ i ] != f[ v ][ i ],这样到最后得到的u和v一定就是它们LCA的两个儿子结点】

参考博客(他写的有点啰嗦,但很细,好理解)

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

using namespace std;
const int maxN = 500005;

inline int read()
{
    int x = 0, f = 1; char c = getchar();
    while(c < '0' || c > '9') { if(c == '-') f = -f; c = getchar(); }
    while(c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
    return x * f;
}

struct EDGE{
    int adj, to;
    EDGE(int a = -1, int b = 0): adj(a), to(b) {}
}edge[maxN << 1];
int head[maxN], cnt;

void init()
{
    memset(head, -1, sizeof(head));
    cnt = 0;
}

void add_edge(int u, int v)
{
    edge[cnt] = EDGE(head[u], v);
    head[u] = cnt ++ ;
}

int deep[maxN], f[maxN][21], n, m, root;

void dfs(int u, int fa)
{
    deep[u] = deep[fa] + 1;
    f[u][0] = fa;
    for(int i = head[u]; ~i; i = edge[i].adj)
    {
        if(edge[i].to == fa)
            continue;
        dfs(edge[i].to, u);
    }
}

void LCA_pre()
{
    deep[0] = 0;
    dfs(root, 0);
    for(int i = 1; i <= 20; ++ i )
        for(int u = 1; u <= n; ++ u ) //放在内层循环,保证每一层的结点都更新完才更新下一层
            f[u][i] = f[f[u][i - 1]][i - 1];
}

int LCA(int u, int v)
{
    if(deep[u] < deep[v]) swap(u, v); //保证deep[u] > deep[v]
    for(int i = 20; i >= 0; -- i )
    {
        if(deep[f[u][i]] >= deep[v])
            u = f[u][i];
    }
    if(u == v) return u;
    for(int i = 20; i >= 0; -- i )
    {
        if(f[u][i] != f[v][i])
        {
            u = f[u][i];
            v = f[v][i];
        }
    }
    return f[u][0];
}

int main()
{
    init();
    n = read(); m = read(); root = read();
    for(int i = 0; i < n - 1; ++ i )
    {
        int u, v; u = read(); v = read();
        add_edge(u, v);
        add_edge(v, u);
    }
    LCA_pre();
    for(int i = 0; i < m; ++ i )
    {
        int u, v; u = read(); v = read();
        printf("%d\n", LCA(u, v));
    }
    return 0;
}

二、欧拉序+ST算法

对于一颗有根树,我们求出它的欧拉序vt[ u ],记录下每个结点第一次在欧拉序中出现的位置first[ u ]。

我们可以很容易得到欧拉序结点对应的深度,那么对于结点u和v,它们的LCA就是[ first[u], first[v] ]区间中的最小深度对应的结点。

不懂看下边的栗子啦~

我们知道LCA(D, C) = A。那么通过这个算法的实现求解LCA(D,C)的过程就是:

(1)、first[D] = 3, first[C] = 8.

(2)、确定要查找的欧拉序列:vt [3, 8] = {3, 2, 3, 2, 1, 2}

(3)、找到最小的深度:1

(4)、找到该深度对应的下标:7

(5)、成功找到LCA:vt[7] = A

注意:欧拉序的长度:结点数*2.

因为每个结点出现的次数为:进入该结点的1次+回溯的次数(儿子结点的个数)

我们可以将整体看作进入和回溯各一次,因为如果是叶子结点的回溯就直接是父亲结点的回溯啦~

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>

using namespace std;
typedef long long ll;

const int maxN = 500005;
const int maxM = 1000006;

inline int read()
{
    int x = 0, f = 1; char c = getchar();
    while(c < '0' || c > '9') { if(c == '-') f = -f; c = getchar(); }
    while(c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar(); }
    return x * f;
}

struct EDGE{
    int adj, to;
    EDGE(int a = -1, int b = 0) : adj(a), to(b) {}
}edge[maxN << 1];
int head[maxN], cnt;

int n, m, root;
int deep[maxN], first[maxN];
int vt[maxM], _min[maxM][25], id[maxM][25];
int tot;

void init()
{
    memset(head, -1, sizeof(head));
    cnt = 0;
    tot = 0;
    deep[0] = 0;
}

void add_edge(int u, int v)
{
    edge[cnt] = EDGE(head[u], v);
    head[u] = cnt ++;
}

void dfs(int u, int fa)
{
    deep[u] = deep[fa] + 1;
    vt[++ tot] = deep[u];
    _min[tot][0] = deep[u];
    id[tot][0] = u;
    first[u] = tot;
    for(int i = head[u]; ~i; i = edge[i].adj)
    {
        if(edge[i].to == fa) continue;
        dfs(edge[i].to, u);
    }
    vt[++ tot] = deep[fa];
    _min[tot][0] = deep[fa];
    id[tot][0] = fa;
}

void LCA_pre()
{
    for(int j = 1; (1 << j) < tot; ++ j )
    {
        for(int i = 1; i + (1 << j - 1) < tot; ++ i )
        {
            if(_min[i][j - 1] <= _min[i + (1 << j - 1)][j - 1])
            {
                _min[i][j] = _min[i][j - 1];
                id[i][j] = id[i][j - 1];
            }
            else
            {
                _min[i][j] = _min[i + (1 << j - 1)][j - 1];
                id[i][j] = id[i + (1 << j - 1)][j - 1];
            }
        }
    }
}

int LCA(int u, int v)
{
    int l = min(first[u], first[v]);
    int r = max(first[u], first[v]);
    int k = log2(r - l + 1);
    if(_min[l][k] <= _min[r - (1 << k) + 1][k])
        return id[l][k];
    else
        return id[r - (1 << k) + 1][k];
}

int main()
{
    n = read(); m = read(); root = read();
    init();
    for(int i = 0; i < n - 1; ++ i )
    {
        int u, v; u = read(); v = read();
        add_edge(u, v);
        add_edge(v, u);
    }
    dfs(root, 0);
    LCA_pre();
    for(int i = 0; i < m; ++ i )
    {
        int u, v; u = read(); v = read();
        printf("%d\n", LCA(u, v));
    }
    return 0;
}
发布了242 篇原创文章 · 获赞 68 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_44049850/article/details/104302137