c++基础树上问题——求lca(最近公共祖先)的几种方法,算法必看哟!

c++基础树上问题——求lca(最近公共祖先)的几种方法,算法必看哟!

目录

 一,lca简介

核心定义

应用场景

关键特性

二,求lca的方法

方法一:朴素求lca(倍增求lca)

代码:

例题:

问题描述

输入格式

输出格式

样例输入

样例输出

样例说明

测评数据规模

代码详解:

方法二:tarjan求lca

1. 时间复杂度极低,适合大规模查询

2. 利用并查集实现 “路径压缩”,合并与查询高效

3. 一次遍历处理所有查询,减少树的访问次数

4. 适用于静态树的批量查询场景

5. 实现逻辑直观,依托 DFS 与并查集的天然契合

总结

代码:

例题:

注:本文题目均来自蓝桥杯官网公开题目,仅用于技术讨论和算法学习。

 一,lca简介

        LCA(least common ancesters 最近公共祖先)是树结构中的核心概念,指两个节点所有公共祖先中距离它们最近(深度最大)的节点。

核心定义

(1)祖先:树中从当前节点到根节点路径上的所有节点,自身也可视为自己的祖先。

(2)公共祖先:同时是两个节点祖先的节点。

(3)最近公共祖先:在所有公共祖先中,深度最深的那个节点(距离两个节点路径长度之和最小)。

应用场景

(1)树结构查询:如二叉树中两个节点的路径长度计算(路径长度 = 节点 A 到 LCA 的距离 + 节点 B 到 LCA 的距离)。

(2)图论与算法:处理树形结构的拓扑关系,比如网络路由优化、家谱关系查询、XML/HTML 文档结构解析。

(3)工程实践:编译器语法树分析、数据库索引树(如 B 树)的节点关联查询。

关键特性

(1)唯一性:在一棵有根树中,任意两个节点的 LCA 有且仅有一个。

(2)路径特性:两个节点的最短路径必然经过它们的 LCA,且 LCA 是路径上的中间关键点。

(3)根节点关联:若一个节点是另一个节点的祖先,则该节点就是两者的 LCA。

二,求lca的方法

方法一:朴素求lca(倍增求lca)

         要求lca,就应先求出每个点的深度(用dfs),用朴素的求lca的方法是:
         不妨设x为深度更深的点,不断让x往上爬,直到dep[x]==dep[y],如果x==y就返回,如果不等于就让x和y 一起往上跳,直到x==y再返回,思路简单,复杂度高。所以就有了倍增法求lca:本质上是dp,类似st表,fa[i][j]表示i号节点,往上走2^j所到的节点,当dep[i]-2^j>=1时fa[i][j]有效(设根节点深度为1)
 例:

 1 | 2 | 3 | 4 \ 5


 fa[5][0]=4(从5向上走2^0),fa[5][1]=fa[fa[5][0]][0]=3
 由此得出:

代码:

void dfs(int x, int p)//p:父节点 { dep[x] = dep[p] + 1; fa[x][0] = p; for (int i = 1; i <= 20; i++) { fa[x][i] = fa[fa[x][i - 1]][i - 1]; } for (const auto& y : g[x]) { if (y == p)continue; dfs(y, x); } } int lca(int x, int y) { if (dep[x] < dep[y])swap(x, y); //贪心,x向上跳,直到与y同层 for (int i = 20; i >= 0; i--) { if (dep[fa[x][i]] >= dep[y])x = fa[x][i]; } if (x == y)return x;//二者相同,直接返回 //与y同层之后,二者同时向上跳,但要保持x!=y for (int i = 20; i >= 0; i--) { if (fa[x][i] != fa[y][i])x = fa[x][i], y = fa[y][i]; } return fa[x][0];// x的父节点就是LCA(此时x和y的父节点相同) }

例题:

蓝桥杯官网——最近公共祖先lca查询(模板题)

问题描述

给定一棵有 N 个节点的树,每个节点有一个唯一的编号,从 1 到 N。树的根节点是 1 号节点。接下来,你会得到 Q 个查询。对于每个查询,你将得到两个节点的编号,你的任务是找到这两个节点的最低公共祖先。

输入格式

第一行包含一个整数 N,表示树的节点数。

接下来的 N−1 行,每行包含两个整数 U 和 V,表示节点 U 和节点 V 之间有一条边。

下一行包含一个整数 Q,表示查询的数量。

接下来的 Q 行,每行包含两个整数 A 和 B,表示你需要找到节点 A 和节点 B 的最低公共祖先。

输出格式

对于每个查询,输出一行,该行包含一个整数,表示两个节点的最近公共祖先。

样例输入

5 1 2 1 3 2 4 2 5 3 4 5 3 4 3 5 

样例输出

2 1 1 

样例说明

对于第一个查询,4 和 5 的最低公共祖先是 2。

对于第二个查询,3 和 4 的最低公共祖先是 1。

对于第三个查询,3 和 5 的最低公共祖先是 1。

测评数据规模

2≤N≤10^5,1≤Q≤10^4,1≤U,V,A,B≤N,题目保证输入的边形成一棵树。

代码详解:

#include <iostream> #include<vector> using namespace std; const int N=1e5+9; int fa[N][24],dep[N]; vector<int>g[N]; void dfs(int x,int p) { dep[x]=dep[p]+1; fa[x][0]=p; for(int i=1;i<=20;i++) { fa[x][i]=fa[fa[x][i-1]][i-1]; } for(const auto&y:g[x]) { if(y==p) continue; dfs(y,x); } } int lca(int x,int y) { //先判断深度要深的向上跳,默认x if(dep[x]<dep[y]) swap(x,y); //贪心,先使得x,y同层 for(int i=20;i>=0;i--) { if(dep[fa[x][i]]>=dep[y]) x=fa[x][i]; } //到达同一层,又要分两种情况:1,xy此时相遇 if(x==y) return x; //2,xy不相遇:二者同时向上跳,但要保持x!=y for(int i=20;i>=0;i--) { if(fa[x][i]!=fa[y][i]) x=fa[x][i],y=fa[y][i]; } return fa[x][0];//此时返回二者任意节点的父节点,就是lca! } int main() { ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); int n;cin>>n; for(int i=1;i<n;i++) { int u,v;cin>>u>>v; g[u].push_back(v),g[v].push_back(u); } dfs(1,0); int q;cin>>q; while(q--) { int x,y;cin>>x>>y; cout<<lca(x,y)<<'\n'; } return 0; }

方法二:tarjan求lca

Tarjan 算法求 LCA(基于并查集的离线算法)的核心优势在于高效处理多组查询,尤其适合需要一次性解决大量 LCA 查询的场景。以下是其具体优势的详细解释:

1. 时间复杂度极低,适合大规模查询

(1)Tarjan 算法是离线算法(需先收集所有查询,再一次性处理),其时间复杂度为 O(n + q),其中 n 是树的节点数,q 是查询次数。

(2)对比倍增算法(在线算法,单次查询 O (logn)):当查询次数 q 很大(如 q = 1e5 甚至 q = 1e6)时,Tarjan 算法的总效率更高(n + q 远小于 q * logn)。

2. 利用并查集实现 “路径压缩”,合并与查询高效

(1)算法核心依赖并查集(Union-Find)的数据结构,通过 root(x) 快速查找节点所在集合的根,并用路径压缩优化查询效率。

(2)在遍历树的过程中,通过 “合并子树到父节点” 的操作,天然维护了 “节点与其祖先的连通性”,避免了重复计算。

3. 一次遍历处理所有查询,减少树的访问次数

(1)算法通过深度优先搜索(DFS) 遍历树一次,在回溯时完成并查集的合并,并同步处理所有与当前节点相关的查询。

(2)相比在线算法(如倍增)需要对每个查询单独遍历树的部分路径,Tarjan 算法只需遍历树一次,大幅减少了对树结构的重复访问。

4. 适用于静态树的批量查询场景

  • 若问题中所有查询在处理前已知(离线场景),Tarjan 算法是最优选择之一。例如:
    • 批量查询树上多对节点的 LCA;
    • 基于 LCA 计算多对节点的路径长度、距离等衍生问题。
  • 典型应用:家谱树中批量查询亲属关系、网络拓扑中批量计算节点间最短路径等。

5. 实现逻辑直观,依托 DFS 与并查集的天然契合

(1)算法利用 DFS 的回溯特性:当遍历完一个节点的所有子树后,该节点与其子树的所有节点已被合并到同一集合,且集合的根为该节点(或更高祖先)。

(2)此时若查询中存在与该节点相关的配对(且另一节点已被访问),则两节点的 LCA 就是 “已访问节点所在集合的根”,逻辑清晰且易于理解。

总结

Tarjan 算法求 LCA 的核心优势是离线处理下的高效性,尤其在查询数量庞大时,其 O (n + q) 的时间复杂度远优于在线算法。但需注意其局限性:必须提前知晓所有查询(无法处理动态新增的查询),且依赖并查集和 DFS 的配合。

对比而言:

(1)若需要动态查询(查询逐步到来),优先选倍增算法;

(2)若查询一次性给出且数量大,Tarjan 算法是更优解。

代码:

int root(int x) { return pre[x] = (pre[x] == x ? x : root(pre[x])); } vector<int>vec[N]; // 记录需要做lca的点对,就是1到m的查询点 bitset<N>vis; // 标记节点是否被访问过 map<int, int>lca[N]; // 存储点对的lca结果 /*vis 是一个 bitset,用于标记节点是否已完成所有子树的遍历(即「回溯完成」)。 只有当节点的所有子节点都处理完毕后,才会标记为 true。*/ void tarjan(int x, int father)// Tarjan算法求LCA { fa[x] = father; //这一步是在遍历x的所有子节点,及子节点的子节点 for (const auto& y : g[x]) { if (y == fa[x])continue; tarjan(y, x); pre[root(y)] = root(x); /*当子节点 y 的所有子树都处理完毕后,执行 pre[root(y)] = root(x): 将 y 所在集合的根节点(root(y))合并到 x 所在集合的根节点(root(x))中。 这一步的作用是:标记 y 及其子树已处理完毕,并将它们归入 x 的集合,为后续计算 LCA 做准备。*/ //y是x的子节点,y的根有可能就是x,所以可将y集合并入x集合中 } vis[x] = true;//说明x已经查询过了 for (const auto& y : vec[x])//处理与x有关的查询,比如我要查询3,5是否有公共祖先,y就是5,x就是3 { if (vis[y])lca[x][y] = lca[y][x] = root(y);//若y也被查询过,说明在之前遍历x及其子树的过程中 //y出现过了 //则y与x必然联通,二者必然有lca! /*此时 root(y) 之所以是 LCA,原因如下: y 已处理完毕,意味着 y 的所有祖先(包括 LCA)都已完成合并, root(y) 指向 y 所在分支中最深的已合并祖先。 x 正在处理中(尚未合并到父节点),而 y 已处理,说明 x 和 y 的公共祖先中, 最深的那个就是 root(y)(因为 y 已合并到该祖先的集合中,而 x 还未继续向上合并)。*/ } }

例题:

就是上面那道:

#include<utility> #include<iostream> #include<vector> #include<queue> #include<algorithm> #include<bitset> using namespace std; const int N=2e5+9; int pre[N],ans[N]; vector<int>g[N]; //需要一个结构体,来存储每个查询点以及对应的另一个查询点和编号 //因为最终要输出一个答案数组,所以要有编号 struct Q { int x,id; }; vector<Q>q[N];//存储查询点对和对应的编号 int root(int x) { return pre[x]=(pre[x]==x?x:root(pre[x])); } bitset<N>vis; void tarjan(int x,int fa) { vis[x]=true; for(const auto&y:g[x]) { if(y==fa) continue; tarjan(y,x); pre[y]=x;//相当于pre[root(y)]=root(x); } for(const auto& t:q[x]) { int y=t.x,id=t.id; if(vis[y]) ans[id]=root(y); } } int main() { ios::sync_with_stdio(0),cin.tie(0),cout.tie(0); int n;cin>>n; for(int i=1;i<n;i++) { int u,v;cin>>u>>v; g[u].push_back(v),g[v].push_back(u); } //不要忘了初始根!!! for(int i=1;i<=n;i++) pre[i]=i; int m;cin>>m; for(int i=1;i<=m;i++) { int x,y;cin>>x>>y; q[x].push_back({y,i}); q[y].push_back({x,i}); } tarjan(1,0); for(int i=1;i<=m;i++) cout<<ans[i]<<'\n'; }

okkkkkkk了,如果觉得不错,就麻烦点赞+关注+收藏吧!

Read more

【Spring】原理解析:Spring Boot 自动配置

【Spring】原理解析:Spring Boot 自动配置

目录 1.前言 插播一条消息~ 2.正文 2.1加载bean到容器中 2.1.1 @ComponentScan:主动扫描发现Bean 2.1.2 @Import:灵活导入Bean的“万能钥匙” 2.1.3 自定义注解:封装配置的“快捷方式” 2.2Spring Boot原理分析 2.2.1 @SpringBootApplication组合注解 2.2.2 @EnableAutoConfiguration:自动配置的"总开关" 3.小结 1.前言 作为Java开发者,你是否曾在传统Spring项目中反复编写@ComponentScan注解来指定Bean扫描路径?或者在集成第三方组件时,不得不手动通过@Import导入十几个配置类?

By Ne0inhk
Tomcat下载安装以及配置(详细教程)

Tomcat下载安装以及配置(详细教程)

本文讲的是Java环境 文章目录 * 前言 * 下载及安装Tomcat * 启动Tomcat * 测试Tomcat * 配置Tomcat 环境变量 * IDEA中配置Tomcat * Eclipse中配置Tomcat 前言 提示:这里可以添加本文要记录的大概内容: 今天晚上查看自己原来项目的时候,突然发现运行不了,仔细查看发现是tomcat没配置,但是tomcat在电脑里已经下载过了,只是还没有配置,这篇文章就讲tomcat在电脑与idea中的配置 提示:以下是本篇文章正文内容,下面案例可供参考 下载及安装Tomcat 进入tomcat官网,Tomcat官网 选择需要下载的版本,点击下载 下载路径一定要记住,并且路径中尽量不要有中文 下载后是压缩包 .zip,解压后 tomcat系统各个文件夹目录是什么意义: bin:放置的是Tomcat一些相关的命令,启动的命令(startup)和关闭的命令(shutdown)等等 conf:(configure)配置文件 lib:(library)库,依赖的 jar包 logs:Tomca

By Ne0inhk
Go语言×Kingbase数据库极速打通:Gokb驱动三步实操,让国产数据库连接效率嘎嘎提升!

Go语言×Kingbase数据库极速打通:Gokb驱动三步实操,让国产数据库连接效率嘎嘎提升!

引言 Kingbase 作为国产数据库代表,本文将介绍Go语言通过Gokb驱动连接KingbaseES 数据库的全流程,包含环境配置、连接验证、SQL执行及常见问题处理,从基础连接到高级操作全面掌握这一技术栈。 KingbaseES 数据库【系列篇章】: No.文章地址(点击进入)1电科金仓KingbaseES数据库解析:国产数据库的崛起与技术创新2KingBase数据库迁移利器:KDTS工具深度解析与实战指南3KingBase数据库迁移利器:KDTS工具 MySQL数据迁移到KingbaseES实战4电科金仓KingbaseES V9数据库:国产数据库的自主创新与行业实践深度解析5KingbaseES客户端工具Ksql使用全指南:从安装到高级操作6Spring JDBC与KingbaseES深度集成:构建高性能国产数据库应用实战7深度解析:基于 ODBC连接 KingbaseES 数据库的完整操作与实践8Python驱动Ksycopg2连接和使用Kingbase:国产数据库实战指南 一、环境准备 已安装与驱动对应版本的数据库,且数据库连接可用。 1.1 下载Go

By Ne0inhk
PostgreSQL - 事务的提交、回滚与保存点操作

PostgreSQL - 事务的提交、回滚与保存点操作

👋 大家好,欢迎来到我的技术博客! 📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。 🎯 本文将围绕PostgreSQL这个话题展开,希望能为你带来一些启发或实用的参考。 🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获! 文章目录 * PostgreSQL - 事务的提交、回滚与保存点操作 * 什么是数据库事务? * PostgreSQL 事务的基本语法 * 1. 开启事务 * 2. 提交事务(COMMIT) * 3. 回滚事务(ROLLBACK) * 4. 保存点(SAVEPOINT) * 事务生命周期图解 * Java 中如何操作 PostgreSQL 事务? * 准备工作 * 关闭自动提交模式 * 基本事务操作示例 * 保存点(SAVEPOINT)的 Java 实现 * 场景:批量插入订单项 * 事务隔离级别详解 * Java 设置隔离级别示例 * 事务与锁机制 *

By Ne0inhk