倍增求解最近公共祖先(LCA)问题

介绍

对于一棵树,常常有这样的问题,如下图,求出6和8节点的最近公共祖先节点

  • 祖先节点就是指当前节点沿着链向上经过的节点,最近公共祖先节点就是两个询问节点沿着树链往上走直至相遇的第一个节点

在这里插入图片描述

简要分析和朴素思路

看完问题介绍直接能想到的做法就是沿着节点往上走,直到两个节点相遇,第一个位置就是LCA,为了验证这个想法,还是要找道题试一试
模板题

  • 首先要建图,刚开始学习算法,主要掌握算法原理,除此之外越方便越好,那么最方便的建图方法是用vector数组,所以考虑使用vector,dfs建图,对一个点使用vis数组向上遍历记录所有祖先节点
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
vector<int> son[MAXN];
int fa[MAXN];
int vis[MAXN];
void dfs(int u){
    
    
    int len = son[u].size();
    for(int i=0;i<len;i++){
    
    
        int v = son[u][i];
        if(v == fa[u]) continue;//因为建图方式,可能出现u的父节点为v的情况
        fa[v] = u;
        dfs(v);
    }
}
int LCA(int x, int y){
    
    
    memset(vis, 0, sizeof vis);
    while(x){
    
    
        vis[x] = 1;
        x = fa[x];
    }while(!vis[y]){
    
    
        y = fa[y];
    }
    return y;
}
int main(){
    
    
    int n, m, s, x, y;
    scanf("%d%d%d", &n, &m, &s);
    for(int i=1;i<n;i++){
    
    
        scanf("%d%d", &x, &y);
        son[x].emplace_back(y);
        son[y].emplace_back(x);
    }
    dfs(s);
    for(int i=0;i<m;i++){
    
    
        scanf("%d%d", &x, &y);
        printf("%d\n", LCA(x, y));
    }
    return 0;
}
  • 这是一个思路,交了之后TLE三个点,考虑memset也是比较费时间的,那么可以考虑另外一种思路就是dfs过程中记录所有节点的深度,根据深度,先将两个节点提到一个高度,然后一起向上寻找祖先节点,直到祖先节点相同,那么就是两个节点的最近公共祖先
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
vector<int> son[MAXN];
int fa[MAXN];
int depth[MAXN];
void dfs(int u, int d){
    
    
    int len = son[u].size();
    depth[u] = d;
    for(int i=0;i<len;i++){
    
    
        int v = son[u][i];
        if(v == fa[u]) continue;
        fa[v] = u;
        dfs(v, d + 1);
    }
}
int LCA(int x, int y){
    
    
    if(depth[x] < depth[y]) swap(x, y);
    while(depth[x] != depth[y]) x = fa[x];
    if(x == y) return x;
    while(fa[x] != fa[y]){
    
    
        x = fa[x];
        y = fa[y];
    }
    return fa[x];
}
int main(){
    
    
    int n, m, s, x, y;
    scanf("%d%d%d", &n, &m, &s);
    for(int i=1;i<n;i++){
    
    
        scanf("%d%d", &x, &y);
        son[x].emplace_back(y);
        son[y].emplace_back(x);
    }
    dfs(s, 0);
    for(int i=0;i<m;i++){
    
    
        scanf("%d%d", &x, &y);
        printf("%d\n", LCA(x, y));
    }
    return 0;
}

在这里插入图片描述

  • 居然水过去了,那么换一道题
    POJ1330
  • 也是一道模板,不同在于没给根节点,需要先找根节点,for一圈就行了,一看数据范围10的4次方,根本卡不住,算了

倍增算法

  • 其实看起来第二种朴素算法也不很费时间,将x提到与y同高之后,再往上一起提升,总的时间复杂度是O(max(depth[x],depth[y])),线性时间复杂度,也很不错,这个算法往上走的过程中是一个一个的,除非链长达到几百万,时间问题才会体现出来
  • 但是可以考虑倍增思想,在向上走的过程中是一个一个的,能不能跨越式的向上呢?那么我们就需要记录每个节点的一部分祖先节点,也就是跨越上去的那些
  • 使用一个二维数组f第一维表示当前子节点,第二维表示距离他是2的多少次幂的祖先节点,,记录子结点到达和他距离为2i的祖先节点的标号,线性递推求出每个子结点的那些祖先节点,思路是使用两圈for循环,第一圈找到所有父亲节点,第二圈找到爷爷,第三圈找爷爷的爷爷,第四圈找爷爷的爷爷的爷爷的爷爷,每一圈都是前一圈的两倍,所以显然n应该放在内圈,这样能一直找到上2的22次方代,如果需要,可以加长
    for(int i=1;i<=22;i++){
    
    
        for(int j=1;j<=n;j++){
    
    
            f[j][i] = f[f[j][i - 1]][i - 1];
        }
    }
  • 接下来就可以利用f数组起到加速的作用,根据x和y深度差, 因为任何数都能够拆成若干个2的幂次方之和的形式(若干个1相加),所以考虑把这个深度差均分成2的i次方,这样可以实现大跨步的往上走,也就是倍增
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
using namespace std;
typedef long long ll;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e6 + 100;
vector<int> son[MAXN];
int depth[MAXN];
int f[MAXN][30];
void dfs(int u, int p, int d){
    
    
    depth[u] = d;
    f[u][0] = p;
    int len = son[u].size();
    for(int i=0;i<len;i++){
    
    
        int v = son[u][i];
        if(v == f[u][0]) continue;
        f[v][0] = u;
        dfs(v, u, d + 1);
    }
}
int LCA(int x, int y){
    
    
    if(depth[x] < depth[y]) swap(x, y);
    for(int i=log2(depth[x] - depth[y]);i>=0;i--){
    
    
        if((1 << i) <= depth[x] - depth[y]) x = f[x][i];
    }
    if(x == y) return x;
    for(int i=log2(depth[x]);i>=0;i--){
    
    
        if(f[x][i] != f[y][i]){
    
    
            x = f[x][i];
            y = f[y][i];
        }
    }
    return f[x][0];
}
int main(){
    
    
    int n, m, s, x, y;
    scanf("%d%d%d", &n, &m, &s);
    for(int i=1;i<n;i++){
    
    
        scanf("%d%d", &x, &y);
        son[x].emplace_back(y);
        son[y].emplace_back(x);
    }
    dfs(s, s, 0);
    for(int i=1;i<=22;i++){
    
    
        for(int j=1;j<=n;j++){
    
    
            f[j][i] = f[f[j][i - 1]][i - 1];
        }
    }
    while(m--){
    
    
        scanf("%d%d", &x, &y);
        printf("%d\n", LCA(x, y));
    }
    return 0;
}
  • 这个模板记住先把x和y提到一个高度,然后在一起往上移直到到一块,在第一步之后有个特判因为可能y就是x的祖先,理解之后记忆不难
    在这里插入图片描述
  • 可以看到原来的一秒多变成了500多毫秒,优化幅度很大,再压缩就需要优化存储结构了,因为push_back有些费时间,倍增算法可以告一段落

猜你喜欢

转载自blog.csdn.net/roadtohacker/article/details/113784602
今日推荐