首页 > 编程语言 >树上问题/简单算法 LCA【最近公共祖先】

树上问题/简单算法 LCA【最近公共祖先】

时间:2024-07-16 09:40:19浏览次数:10  
标签:dep int fa 算法 LCA 树上 root 节点

概念引入

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

在下面的说明中,我们设两个节点分别为 \(x\),\(y\),节点 \(x\),\(y\) 的深度分别表示为 \(dep_x\),\(dep_y\),将树称为 \(T\)

算法详解:

朴素算法:

先将节点 \(x\) 的深度提升到与 \(y\) 节点深度相同的位置,然后两个一起一个一个往上跳,直到相遇。

那么易知时间复杂度为 \(O(n)\),因为过于朴素,这里就不给出代码了。

倍增算法:

对于朴素算法中的:

“然后两个一起一个一个往上跳”

我们可以用倍增的方式完成跳跃.

那么如何实现呢?

我们知道,任何一个非负整数都可以进行二进制拆分.例如:

  • \(7 = 2 ^ 0 + 2 ^ 1 + 2 ^ 2\)
  • \(14 = 2 ^ 1 + 2 ^ 2 + 2 ^ 3\)

那么如果需要向上跳的次数为 \(n\),则朴素算法的时间复杂度为 \(O(n)\),而倍增算法的复杂度就可以达到\(O(log_2 n)\).

( 对于将 \(x\),\(y\) 提升到同一高度的过程,同样可以使用倍增的算法来实现,这里不多加说明 )

接下来考虑如何实现

我们设 \(fa_{x,i}\) 为 \(x\) 节点的 \(2 ^ i\) 级的祖先,即 \(x\) 节点向上走 \(2 ^ i\) 步所到达的节点

  1. 预处理 \(fa\) 数组:

我们知道:\(2 ^ n = 2 ^ {n-1} * 2 ^ {n - 1}\)

那么我们可以将向上走 \(2 ^ i\) 步看作先走 \(2 ^ {i - 1}\) 再走 \(2 ^ {i - 1}\) 步

则有: \(fa_{x,i} = fa_{fa_{x,i - 1},i - 1}\) 这可以说是非常关键的一步

因为 \(\forall x \in T\) , \(fa_{x,0}\) (即 \(x\) 的父节点)都可以通过一遍 \(dfs\) 解决

for(int i = 1;i <= log2(n);i++){
    for(int j = 1;j <= n;j++){
        fa[j][i] = fa[fa[j][i - 1]][i - 1];
    }
}
  1. 将 \(x\),\(y\) 提升到同一高度:

主要思想是将 \(x\),\(y\) 的深度差值进行二进制拆分

if(dep[x] < dep[y]) swap(x,y);//保证x的深度大于y,方便后面的计算
int delta = dep[x] - dep[y];
for(int i = 0;i <= log2(n);i++){
    if((1 << i) & delta) x = fa[x][i];//向上跳,本质是对delta进行二进制拆分
}
  1. \(x\),\(y\) 同时向上跳

这里有一个细节,即是将 \(i\) 从 \(log_2 n\) 往 \(0\) 枚举,这样可以避免拆分的重复或漏

for(int i = log2(n);i >= 0;i--){
    if(fa[x][i] != fa[y][i]){
        //Leap!
        x = fa[x][i];
        y = fa[y][i];
    }
}

我们可以将第二步和第三步封装成一个函数:

int lca(int x,int y){
    if(dep[x] < dep[y]) swap(x,y);
    int delta = dep[x] - dep[y];
    for(int i = 0;i <= log2(n);i++){
        if((1 << i) & delta) x = fa[x][i];
    }
    if(x == y){
        return x;
    }
    for(int i = log2(n);i >= 0;i--){
        if(fa[x][i] != fa[y][i]){//这里最终跳跃到的是lca(x,y)的子节点
            x = fa[x][i];
            y = fa[y][i];
        }
    }
    return fa[x][0];
}

那么我们的倍增算法就好了,可知其时间复杂度为 \(O(log_2 n)\)

这里附上完整代码:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 7;
const int LOG = 30;
int n,m,r;
int dep[MAXN];
int fa[MAXN][LOG];
bool vis[MAXN];
vector<int> tree[MAXN];
void dfs(int root){
	vis[root] = true;
	if(tree[root].size() == 0){
		return;	
	}
	for(int to : tree[root]){
		if(!vis[to]){
			dep[to] = dep[root] + 1;
			fa[to][0] = root;
			dfs(to);	
		}		
	}
}
int lca(int x,int y){
	//Leap to a same depth:
	int dx = dep[x],dy = dep[y];
	if(dep[x] != dep[y]){
		if(dx < dy){
			swap(dx,dy);
			swap(x,y);
		}
		int delta = dx - dy;
		for(int i = 0;i <= LOG - 2;i++){
			if((1 << i) & delta) x = fa[x][i];
		}
	}
	if(x == y){
		return x;
	}
	//Leap to the child node of lca(x,y)
	for(int i = LOG - 2;i >= 0;i--){
		if(fa[x][i] != fa[y][i]) {
            x = fa[x][i];
            y = fa[y][i];
        }
	}
	return fa[x][0];
}
int main(){
	scanf("%d%d%d", &n, &m, &r);
	for(int i = 1;i < n;i++){
		int x,y;
		scanf("%d%d", &x, &y);
		tree[x].push_back(y);
		tree[y].push_back(x);
	}
	dep[r] = 1;
	fa[r][0] = 0;
	//Pre-Processing
	dfs(r);
	for(int i = 1;i <= LOG - 2;i++){
		for(int j = 1;j <= n;j++){
			fa[j][i] = fa[fa[j][i - 1]][i - 1];
		}
	}
	for(int i = 1;i <= m;i++){
		int x,y;
		scanf("%d%d", &x, &y);
		printf("%d\n", lca(x,y));
	}
	return 0;
}

标签:dep,int,fa,算法,LCA,树上,root,节点
From: https://www.cnblogs.com/wyl123ly/p/18304549

相关文章

  • 算法——1.绪论
    绪论数据结构基本概念➢数据:所有能输入到计算机中并能被计算机程序识别和处理的符号集合。数值数据:整数、实数等非数值数据:图形、图象、声音、文字等➢数据元素:数据的基本单位,在计算机程序中通常作为一个整体进行考虑和处理数据、数据元素、数据项之间的关系包含关......
  • 前端开发中的二分查找算法
    在前端开发中,处理和搜索大量数据时,效率是一个关键因素。二分查找算法是一种高效的查找算法,适用于在有序数组中快速找到目标值。本文将详细介绍二分查找算法的基本原理、实现方法及其在前端开发中的应用。什么是二分查找?二分查找(BinarySearch)是一种在有序数组中查找目标值的算法......
  • 算法思想总结:字符串
    一、最长公共前缀.-力扣(LeetCode)思路1:两两比较 时间复杂度mn 实现findcomon返回两两比较后的公共前缀classSolution{public:stringlongestCommonPrefix(vector<string>&strs){//两两比较stringret=strs[0];size_tn=strs......
  • 数据结构与算法 —— Transformers之Pipeline
    Transformers之Pipeline是HuggingFaceTransformers库中提供的一种使用预训练模型进行推理的极简方式。这些Pipeline对象从库中抽象出大部分复杂代码,为多项任务(如命名实体识别、情感分析、特征提取和问答等)提供了简单的API。以下是对Transformers之Pipeline的详细介绍:一、......
  • 数据结构与算法 —— DFS的实现方法(递归与迭代)
    在讨论文件系统(FileSystem,简称FS)的实现方法时,特别是关注于递归与迭代这两种编程范式,我们实际上是在探讨如何在编程层面上对文件系统进行操作,如遍历目录、创建多级目录等。虽然文件系统的底层实现(如FAT32、NTFS、ext4等)复杂且通常不由应用开发者直接操作,但我们可以从应用层......
  • 算法金 | 秒懂 AI - 深度学习五大模型:RNN、CNN、Transformer、BERT、GPT 简介
    1.RNN(RecurrentNeuralNetwork)时间轴1986年,RNN模型首次由DavidRumelhart等人提出,旨在处理序列数据。关键技术循环结构序列处理长短时记忆网络(LSTM)和门控循环单元(GRU)核心原理RNN通过循环结构让网络记住以前的输入信息,使其能够处理序列数据。每个节点不仅接收当前......
  • 算法金 | 最难的来了:超参数网格搜索、贝叶斯优化、遗传算法、模型特异化、Hyperopt、O
    ​大侠幸会,在下全网同名「算法金」0基础转AI上岸,多个算法赛Top「日更万日,让更多人享受智能乐趣」今日215/10000抱个拳,送个礼为模型找到最好的超参数是机器学习实践中最困难的部分之一1.超参数调优的基本概念机器学习模型中的参数通常分为两类:模型参数和超......
  • 算法金 | 秒懂 AI - 深度学习五大模型:RNN、CNN、Transformer、BERT、GPT 简介
    1.RNN(RecurrentNeuralNetwork)时间轴1986年,RNN模型首次由DavidRumelhart等人提出,旨在处理序列数据。关键技术循环结构序列处理长短时记忆网络(LSTM)和门控循环单元(GRU)核心原理RNN通过循环结构让网络记住以前的输入信息,使其能够处理序列数据。每个节点不仅接收当......
  • 代码随想录算法训练营第十三天 | 144.二叉树的前序遍历、94、二叉树的中序遍历、145、
    144.二叉树的前序遍历题目:.-力扣(LeetCode)思路:有递归法和使用栈来模拟递归的迭代法。代码:1.递归/***Definitionforabinarytreenode.*structTreeNode{*intval;*TreeNode*left;*TreeNode*right;*TreeNode():val(0),left(nu......
  • KMP算法
    KMP算法KMP算法是一个字符串算法,通常用于匹配字符串。KMP算法的原理如果我们暴力枚举下标\(i,j\),\(i\)是文本串的下标,\(j\)是模式串(你要在文本串中匹配的字符串)的下标,时间复杂度\(O(NM)\),其中\(N,M\)分别为文本串和模式串的长度。我们看一下匹配过程:(gif动图请耐心观看)......