图论:倍增求解最近公共祖先LCA

说明:CSDN和公众号文章同步发布,需要第一时间收到最新内容,请关注公众号【比特正传】。

最近公共祖先LCA是NOI大纲中指定的提高组的图论部分的知识点,难度系数为6,提高组考察难度为5~8。

引入

树是一种特殊的图,有没有想过一个问题,给你一棵多叉树,随机给出两个节点,求解两个节点的最近公共祖先?

模板题目链接:

https://www.luogu.com.cn/problem/P3379

基本概念

树的深度:给节点分层,根节点为第一层,即深度为1,依次类推,如图:

节点深度通过depth数组记录,如上图的depth数组如下:

朴素算法

Step1:先从根节点深搜整棵树,搜索过程中,填充深度数组depth和父节点数组fa,代码很简单,如下:

// u表示当前节点,father表示u的父节点
void dfs(int u, int father) {
    depth[u] = depth[father]+1;  // 当前节点深度等于父节点深度+1
    fa[u] = father;   // 记录u父节点
    for(int ne : g[u])  {  // 遍历所有孩子节点,并递归填充depth和fa数组
        if(ne != father) dfs(ne, u); // 除了自己的父节点,其他节点都递归搜索
    }
}

Step2:需要查询两个节点的最近公共祖先,最朴素的想法是,找出两个节点中,深度最大的那个节点,让其向上移动,直到两个节点的深度一样(即在同一层),此时停止,然后两个节点一起向上移动,直到两个节点的父节点为同一个节点即可得到最近公共祖先,代码如下:

// 寻找u和v的LCA
int lcaBaoli(int u, int v) {
    if(depth[u] < depth[v]) swap(u, v);  // 让u永远指向最深的节点
    while(depth[u] > depth[v]) u = fa[u];  // 只要u的深度大于v,那就让u一直往上爬,直到两者在同一层停止就行
    if(u == v) return u;  // 在同一层了,先判断是不是同一个节点,如果是直接返回
    while(fa[u] != fa[v]) {  // u和v的父节点不是同一节点时
        u = fa[u], v = fa[v]; // 让u和v一起向上爬,此时u和v一定处于同一层中
    }
    return fa[u];  // 此时u的父节点和v的父节点是同一个节点,因此直接返回即可
}

完整代码如下:

#include "bits/stdc++.h"
using namespace std;
const int N = 500000+9;
int n, m, s, x, y;
vector<int> g[N];
int depth[N];  // depth[u] 表示节点u的深度
int fa[N];  
// 寻找u和v的LCA
int lcaBaoli(int u, int v) {
    if(depth[u] < depth[v]) swap(u, v);  // 让u永远指向最深的节点
    while(depth[u] > depth[v]) u = fa[u];  // 只要u的深度大于v,那就让u一直往上爬,直到两者在同一层停止就行
    if(u == v) return u;  // 在同一层了,先判断是不是同一个节点,如果是直接返回
    while(fa[u] != fa[v]) {  // u和v的父节点不是同一节点时
        u = fa[u], v = fa[v]; // 让u和v一起向上爬,此时u和v一定处于同一层中
    }
    return fa[u];  // 此时u的父节点和v的父节点是同一个节点,因此直接返回即可
}
// u表示当前节点,father表示u的父节点
void dfs(int u, int father) {
    depth[u] = depth[father]+1;  // 当前节点深度等于父节点深度+1
    fa[u] = father;   // 记录u父节点为father
    for(int ne : g[u])  {  // 遍历所有孩子节点,并递归填充depth和fa数组
        if(ne != father) dfs(ne, u); // 除了自己的父节点,其他节点都递归搜索
    }
}
int main() {
    scanf("%d%d%d", &n, &m, &s);
    for(int i=1; i<n; i++) {
        scanf("%d%d", &x, &y);
        g[x].push_back(y); // 建成无向图
        g[y].push_back(x);
    }
    dfs(s, 0); // 通过深搜填充depth数组和p数组
    while(m--) {
        scanf("%d%d", &x, &y);
        printf("%d\n", lcaBaoli(x, y));
    }
    return 0;
}

结果如下:

以上思想有什么问题吗?哪里还可以优化呢?

还记得ST表吗?还记得并查集路径压缩吗?能不能把这些思想揉一揉,结合到一起呢?

RMQ问题之ST表_st表预处理时间复杂度-CSDN博客

算法思想

倍增思想

上面的代码问题是:向上找父亲,一层一层走太慢了,并查集如果不做路径压缩,也是一层一层找父亲,层数过大会导致效率贼低,因此并查集做了路径压缩。ST呢?f[i][j]表示起点为i,区间长度为2^j的区间,也就是区间[i, i+2^j -1] 的最值,那么递推式为:

f[i][j] = max(f[i][j-1], f[i+2^(j-1)][j-1])

是否可以借助该倍增思想求祖先,而不是一个一个往上跳。

注意:大于0的任何一个正整数,都可以用2的次幂求和表示出来(二进制),这句话非常重要

记录和保存下来每个节点的不同层级的祖先,按照距离进行划分,距离为1,表示上一级祖先(爸爸);距离为2,表示上两级祖先(即爷爷);距离为4,表示上四级祖先(即爷爷的爷爷)。

上面的想法可以通过fa数组进行保存,fa[u][i]表示u的【上2^i 级祖先】,上面的图对应的fa数组如下:

首先通过dfs深搜这棵树,搜索过程中填充depth和fa数组,代码如下,结合代码理解:

// u表示当前节点,father表示u的父节点
void dfs(int u, int father) {
    depth[u] = depth[father]+1;  // 当前节点深度等于父节点深度+1
    fa[u][0] = father;   // 当前节点向上2^0=1个祖先为当前的父节点
    for(int i=1; i<=19; i++) {  //向上找到1,2,4,8,....,2^19长度的祖先并填充
        fa[u][i] = fa[fa[u][i-1]][i-1];  // 该递推式是核心
    }
    for(int ne : g[u])  {  // 遍历所有孩子节点,并递归填充depth和p数组
        if(ne != father) dfs(ne, u); // 除了自己的父节点,其他节点都递归搜索
    }
}

上面的代码结合注释理解应该没什么问题。

下面看找最近公共祖先的核心代码,倍增算法:

// lcaMultiply 函数返回u和v的父节点
int lcaMultiply(int u, int v) {  // 倍增
    if(depth[u] < depth[v]) swap(u, v);  // 让u指向深度最大的节点
    for(int i=19; i>=0; i--) { // 向上找u的祖先,通过数学可以推出来,最终u和v的深度一定会相等,因为两者深度差为一个大于等于0的数,如果等于0什么都不做,对于大于0的数,一定可以通过1,2,4,8,16,32,...,凑出来
        if(depth[fa[u][i]] >= depth[v]) {  
            u = fa[u][i];
        }
    }
    if(u == v) return u;  // 如果此时u和v刚好相等,那一定是最近的公共祖先,直接返回即可,
    for(int i=19; i>=0; i--) {  // 两个一起往上找祖先,
        if(fa[u][i] != fa[v][i]) {    // 如果两个祖先不一样,就继续往上走
            u = fa[u][i];  // 向上走到祖先
            v = fa[v][i];
        }
    }
    return fa[u][0];
}

完整代码如下:

#include "bits/stdc++.h"
using namespace std;
const int N = 500000+9;
int n, m, s, x, y;
vector<int> g[N];
int depth[N];  // depth[u] 表示节点u的深度
int fa[N][20];  // fa[u][i]表示节点u向上的2^i个父节点,比如000->111->222->333->444,则fa[444][0]为333,fa[444][1]为222, fa[444][2]为000
// lcaMultiply 函数返回u和v的父节点
int lcaMultiply(int u, int v) {  // 倍增
    if(depth[u] < depth[v]) swap(u, v);  // 让u指向深度最大的节点
    for(int i=19; i>=0; i--) { // 向上找u的祖先,通过数学可以推出来,最终u和v的深度一定会相等,因为两者深度差为一个大于等于0的数,如果等于0什么都不做,对于大于0的数,一定可以通过1,2,4,8,16,32,...,凑出来
        if(depth[fa[u][i]] >= depth[v]) {  
            u = fa[u][i];
        }
    }
    if(u == v) return u;  // 如果此时u和v刚好相等,那一定是最近的公共祖先,直接返回即可,
    for(int i=19; i>=0; i--) {  // 两个一起往上找祖先,
        if(fa[u][i] != fa[v][i]) {    // 如果两个祖先不一样,就继续往上走
            u = fa[u][i];  // 向上走到祖先
            v = fa[v][i];
        }
    }
    return fa[u][0];
}
// u表示当前节点,father表示u的父节点
void dfs(int u, int father) {
    depth[u] = depth[father]+1;  // 当前节点深度等于父节点深度+1
    fa[u][0] = father;   // 当前节点向上2^0=1个祖先为当前的父节点
    for(int i=1; i<=19; i++) {  //向上找到1,2,4,8,....,2^19长度的祖先并填充
        fa[u][i] = fa[fa[u][i-1]][i-1];  // 该递推式是核心
    }
    for(int ne : g[u])  {  // 遍历所有孩子节点,并递归填充depth和p数组
        if(ne != father) dfs(ne, u); // 除了自己的父节点,其他节点都递归搜索
    }
}
int main() {
    scanf("%d%d%d", &n, &m, &s);
    for(int i=1; i<n; i++) {
        scanf("%d%d", &x, &y);
        g[x].push_back(y); // 建成无向图
        g[y].push_back(x);
    }
    dfs(s, 0); // 通过深搜填充depth数组和p数组
    while(m--) {
        scanf("%d%d", &x, &y);
        printf("%d\n", lca(x, y));
    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/678517.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

6 -力扣高频 SQL 50 题(基础版)

6. 使用唯一标识码替换员工ID -- MySQL中的left join是一种连接方式 -- 它以左表为基准&#xff0c;返回左表中所有的行&#xff0c;同时返回右表中与左表匹配的行。 -- 如果右表中没有匹配的行&#xff0c;则用NULL填充。 select unique_id,name from Employees e left join …

告别繁琐,Xinstall一键解决App代理结算难题!

在移动互联网的浪潮中&#xff0c;App的推广和运营成为了众多企业和开发者关注的焦点。然而&#xff0c;随着App市场的日益竞争&#xff0c;代理结算的复杂性和繁琐性成为了许多推广者头疼的问题。为了解决这个问题&#xff0c;Xinstall凭借其专业的技术和丰富的服务经验&#…

项目更换服务器时间少8小时

时区错误 输入 date 查看当前的linux系统时间 hwclock --show 查看当前linux硬件时间 如果发现系统时间和硬件时间不同步&#xff0c;而且硬件时间是正确的&#xff0c;可以用以下命令&#xff1a;hwclock --hctosys 把硬件时间同步到系统时间 mysql时区错误可以参考这位大…

java环境部署

jdk下载 如果你电脑已经下载了 jdk &#xff0c;那你可以跳过这一步了 jdk 的下载路径:https://www.oracle.com/java/technologies/downloads 我这里下载的是java21 你们可以选择自己需要下载的 游览进去的页面是这样子的&#xff08;相比以前这个页面发生很大变化了&#x…

《额尔古纳河右岸》有感

看完了迟子建老师的《额尔古纳河右岸》&#xff0c;老规矩&#xff0c;写点东西吧。最近一段时间确实挺迷茫的&#xff0c;所以给自己找了点事儿&#xff0c;看看书。期初并不能认真看进去&#xff0c;慢慢的看见去之后&#xff0c;就愈发想知道故事的后来。 书里有太多关于死亡…

用增之Firebase

目录 简介 开发准备&#xff1a; 1、在Firebase平台创建项目 2、将项目关联到应用 3、项目配置 简介 前面讲了google ddl部分&#xff0c;本篇为Firebase的事件上报部分&#xff0c;包括在FireBase平台创建应用 &#xff0c; 如果有用到ddl…

如何判断ubuntu是桌面版(destop版)还是服务版(server版)?(systemctl status display-manager)

文章目录 用命令systemctl status display-manager 用命令systemctl status display-manager systemctl status display-manager如果是ubuntu desktop&#xff0c;将显示服务正在运行&#xff0c;如&#xff1a; 如果是ubuntu server&#xff0c;将不会显示服务&#xff0c;提…

《缺失MRI模态下的脑肿瘤分割的潜在相关表示学习》| 文献速递-深度学习肿瘤自动分割

Title 题目 Latent Correlation Representation Learning for Brain Tumor Segmentation With Missing MRI Modalities 《缺失MRI模态下的脑肿瘤分割的潜在相关表示学习》 01 文献速递介绍 脑肿瘤是世界上最具侵略性的癌症之一&#xff0c;脑肿瘤的早期诊断在临床评估和治…

【Javascript系统学习】

语法与数据类型 语法 var\let\const 用 var 或 let 语句声明的变量&#xff0c;如果没有赋初始值&#xff0c;则其值为 undefined。如果访问一个未声明的变量会导致抛出 ReferenceError 异常&#xff1a; undefined 值在布尔类型环境中会被当作 false数值类型环境中 undefin…

APP分发移动应用分发未来:内容驱动

APP分发移动应用分发未来&#xff1a;内容驱动 一、引言 随着移动互联网技术的不断进步和用户需求的日益多样化&#xff0c;移动应用分发行业正面临着前所未有的机遇与挑战。在这个过程中&#xff0c;内容驱动成为了一个重要的趋势&#xff0c;它不仅可以提升用户体验&#x…

香橙派OrangePi AIpro上手笔记——之USB摄像头目标检测方案测试(三)

整期笔记索引 香橙派OrangePi AIpro上手笔记——之USB摄像头目标检测方案测试&#xff08;一&#xff09; 香橙派OrangePi AIpro上手笔记——之USB摄像头目标检测方案测试&#xff08;二&#xff09; 香橙派OrangePi AIpro上手笔记——之USB摄像头目标检测方案测试&#xff08;…

前端之HTML语言之基础标签(持续更新)(基础部分更新结束)

前端之HTML语言 学习完后端的各种层之后&#xff0c;今天开始学习前端&#xff0c;前端和后端都是一个项目的组成部分。 前端对应得到语言是HTML&#xff0c;HTML最重要的有三块&#xff0c;行为&#xff0c;样式&#xff0c;J结构。行为就是交互&#xff0c;理解为鼠标的点击…

Python数据分析案例45——基于融合模型(Stack)的电商用户购买行为预测

案例背景 最近618快到了&#xff0c;上电商购买的人很多&#xff0c;正好我手上还有这个用户购买行为的数据&#xff0c;就做了一个机器学习模型流程&#xff0c;然后也使用的都是常见的机器学习模型&#xff0c;但是加了一点创新吧&#xff0c;使用了stacking融合模型。简单来…

【python】成功解决“NameError: name ‘X’ is not defined”错误的全面指南

成功解决“NameError: name ‘X’ is not defined”错误的全面指南 一、引言 在Python编程中&#xff0c;NameError: name X is not defined是一个常见的错误。这个错误通常意味着我们试图使用一个未定义的变量名X。本文将详细解析这一错误的原因&#xff0c;并提供一系列实用…

正版软件 | Fences 最新版本 V5 - 组织工作流程的最佳方式

『Fences 简介』 Stardock Fences 是一款由 Stardock 公司开发的桌面组织工具&#xff0c;旨在帮助用户管理桌面上的图标和文件。以下是对 Stardock Fences 软件的概述&#xff1a; Stardock Fences 概述 开发商: Stardock 功能: 桌面图标管理: Fences 允许用户将桌面上的…

【Python机器学习】无监督学习——不同类型的预处理

之前学习过&#xff0c;一些算法&#xff08;比如神经网络和SVM&#xff09;对数据缩放非常敏感。因此&#xff0c;通常的做法是对特征进行调节&#xff0c;使数据更适合于这些算法。通常来说&#xff0c;这是对数据的一种简单的按照特征的缩放和移动。举例&#xff1a; impor…

MPLAB--读写MCU数据

空工程 Read –Programmer\Read –File\Export, –确定后选择文件位置 & 文件名 Program –File\Import…,选择烧录的文件*.hex –Programmer\Program

在Vue3中使用WebHQChart实现K线图的沙盘推演

本文由ScriptEcho平台提供技术支持 项目地址&#xff1a;传送门 Vue.js K线沙盘推演代码 应用场景介绍 本代码演示了一个使用 Vue.js 框架开发的 K 线沙盘推演工具&#xff0c;它允许用户加载历史 K 线数据并对其进行编辑和修改&#xff0c;从而模拟和分析不同的市场走势。…

[原型资源分享]经典产品饿了么UI模版部件库

​部件库预览链接&#xff1a;https://f13gm0.axshare.com 支持版本: Axrure RP 8 文件大小: 3MB 文档内容介绍 基本部件&#xff1a;表单样式&#xff1a;12款、数据样式&#xff1a;10款、服务样式&#xff1a;6款、导航&#xff1a;5款、业务组件&#xff1a;7款、 模板…

关于无法通过脚本启动Kafka集群的解决办法

启动Kafka集群时&#xff0c;需要在每台个节点上启动启动服务&#xff0c;比较麻烦&#xff0c;通过写了以下脚本来进行启停&#xff1b;发现能正常使用停止功能&#xff0c;不能正常启动Kafka&#xff1b; Kafka启停脚本&#xff1a; ## 以防不能通过shell脚本启动Kafka服务…