LCA(倍增 + tarjin + RMQ)

最近公共祖先简称 LCA(Lowest Common Ancestor)。两个节点的最近公共祖先,就是这两个点的公共祖先里面,离根最远的那个。

一般来讲有以下几种求解的方法。

向上标记法

首先是最朴素的算法——向上标记法。
算法思路
如果我们需要查询u,v的最近公共祖先,那么我们会先让u向上遍历,边遍历边标记,直到遍历到根节点。接着让v向上遍历,如果第一次遇到了被标记过的点,那么就说明这个点是u和v的最近公共祖先。
但是这样做每次询问的复杂度都是树高,随机数据的情况下复杂度是 O ( l o g n ) O(logn) O(logn),而如果是一条链的话复杂度就会变成 O ( n ) O(n) O(n)

倍增算法

倍增算法是最经典的 LCA 求法,他是朴素算法的改进算法。倍增算法通过预处理 f a fa fa数组,大幅度提高了跳转的时间, f a i , j fa_{i,j} fai,j代表从i这个节点开始向根节点跳跃 2 j 2^j 2j步之后所到达的节点。
预处理复杂度 O ( n l o g n ) O(nlogn) O(nlogn), 查询复杂度 O ( l o g n ) O(logn) O(logn)
倍增的算法思路:
首先通过bfs或者dfs预处理出fa数组和depth数组(深度),然后如果要查询的两个节点是u和v,那么我们先找到深度更大的那个节点,让此节点开始往上跳,直到跳到和另外一个节点同一层。
(注意跳的时候是从大的步数枚举到小的步数,因为我们每个步数只使用一次,所以一定选大的更优,例如:如果可以跳8,那么就不会去跳1 + 2 + 4。根据二进制分解的原理可知我们一定可以跳到同一层。)
跳到同一层之后,再让两个节点同时向上跳,直到跳到他们的最近公共祖先的下一层,此时两个点的最近公共祖先就是 f a u , 0 fa_{u,0} fau,0

void bfs(int x) { // 放入根节点
    queue<int> Q;
    memset(depth, 0x3f, sizeof depth);
    depth[0] = 0, depth[x] = 1; //0号点是哨兵
    Q.push(x);
    while(Q.size()) {
        int head = Q.front();
        Q.pop();

        for(int i = h[head]; ~i; i = ne[i]) {
            int to = e[i];
            if(depth[to] > depth[head] + 1) { //说明没有遍历过
                depth[to] = depth[head] + 1;
                Q.push(to);
                f[to][0] = head;
                for(int j = 1; j <= 16; ++ j)
                    f[to][j] = f[f[to][j - 1]][j - 1];
            }
        }
    }
}

int lca(int a, int b) {
    if(depth[a] < depth[b]) swap(a, b); //只跳最下面的点
    for(int k = 16; k >= 0; -- k) 
        if(depth[f[a][k]] >= depth[b]) a = f[a][k];
        //哨兵可以解决跳出树了的问题
        //跳到同一层
    if(a == b) return b; //如果是同一个点,说明b就是a的最近公共祖先。
    for(int k = 16; k >= 0; -- k)
        if(f[a][k] != f[b][k]) a = f[a][k], b = f[b][k]; 
    //一起跳直到跳到他们的最近公共祖先的下一层
    return f[a][0]; //a或者b向上走一层就是他们的最近公共祖先
}

Tarjan算法

是向上标记法的优化
将所有的点分成三大类:

  • 1.已经遍历过的点,即回溯过的点
  • 2.正在遍历的点,即当前路径中的点
  • 3.还没遍历过的点

算法思路:按dfs序遍历所有节点,每遍历完一条路径,就将这一条路径上的所有点都放到与路径的根节点相同的集合中。
复杂度 O ( n + m ) O(n + m) O(n+m)

#include<bits/stdc++.h>
using namespace std;

const int N = 2e4 + 500, M = 2 * N;  //双向边

typedef pair<int, int> PII;
int h[N], e[M], ne[M], w[M], idx;
int dist[N], st[N], p[N], ans[N]; //分别是 每个点和1号点的距离数组  标记数组  并查集数组  和答案数组
int n, m;
vector<PII> query[N];
// query[i][first][second] first存查询距离i的另外一个点j,second存查询编号idx

void init() {
    for(int i = 1; i <= n; ++ i) p[i] = i;
    memset(h, -1, sizeof h);
}

int find(int x) {
    if(p[x] != x) p[x] = find(p[x]);
    return p[x];
}

void add(int a, int b, int c) {
    w[idx] = c, e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void dfs(int u, int fa) {
    for(int i = h[u]; ~i; i = ne[i]) {
        int to = e[i];
        if(to == fa) continue;
        dist[to] = dist[u] + w[i];    //更新dist数组
        dfs(to, u);
    }
}

void tarjan(int u) {
    st[u] = 1;  //当前路径点标记为1,即正在遍历的点
     // u这条路上的根节点的左下的点用并查集合并到根节点
    for(int i = h[u]; ~i; i = ne[i]) {
        int to = e[i];
        if(!st[to])   //等于0说明没有遍历过
            tarjan(to), p[to] = u;  //要注意,是在回溯的时候才开始合并的!
    }
    
    //回溯的同时开始搜索当前点u有关系的询问,尝试求解
    for(auto item : query[u]) {  //统计答案
        int to = item.first, id = item.second;
        if(st[to] == 2) {    //等于2说明已经回溯过了
            int ancy = find(to);  //找到最近公共祖先
            ans[id] = dist[to] + dist[u] - 2 * dist[ancy];  
            //u到to的距离 = d[u]+d[to] - 2*d[lca]
        }
    }
    st[u] = 2;   //回溯的标记
}

void solve() { 
    scanf("%d%d", &n, &m);
    init();
    for(int i = 1; i < n; ++ i) {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
        add(b, a, c);
    }
    for(int i = 1; i <= m; ++ i) {
        int l, r;
        scanf("%d%d", &l, &r);
        if(l != r) {
            query[l].push_back({r, i});
            query[r].push_back({l, i});
        }
    }
    dfs(1, -1);
    tarjan(1);
    for(int i = 1; i <= m; ++ i) printf("%d\n", ans[i]);
}

int main() {
    solve();
    return 0;
}

dfs序 + RMQ

这种做法不是很常见,举个例子:
在这里插入图片描述
现在有这样的一棵树,我们可以得到一个dfs序列:

12484942521363731 12484942521363731 12484942521363731

如果我们现在想要找两个节点的最近公共祖先,例如:
9和5的话,我们只需要找到序列中任意一对 9 9 9 5 5 5,然后,对于这个序列的话就是 9425 9425 9425,找到一段这样的子串,然后这个子串中最小的节点 2 2 2就是他们的最近公共祖先。
再比如我们要找4和7的最近公共祖先的话,随便找一段例如: 425213637 425213637 425213637,那最近公共祖先就是1。

这个区间最值问题可以用ST表来解决。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值