图论(8)LCA

2023/6/5 21:09:39

一、概念

 给定一棵有根树,若节点z既是x的祖先,又是y的祖先,则称z是x和y的公共祖先。

在所有公共祖先中,深度最大的一个为最近公共祖先。

求最近公共祖先的方法有:

(1):

        1.x向上走,标记所有路过节点

        2.y向上走,走到第一个被标记节点

(2):树上倍增法

个人认为是动态规划和快速幂的结合。

设f(x,k)是x的2^k辈祖先,即x向上走2^k步到达的结点。如果该节点不存在,则为0.

f(x,0)显然是x的父节点,f(x,k)则可由之前计算的一步步推得,2^k=2^(k-1)+2^(k-1)即x向上走2^(k-1)步到达的结点再向上走2^(k-1)步。:

f(x,k)=f(f(x,k-1),k-1)

求lca前先预处理这个f数组。

我们可以用bfs,按照层次顺序,在入队之前,计算它的f数组。时间复杂度nlogn。

基于f数组计算LCA,又有一下几步:

1.设d(x)为x的深度,如果y比x深,则交换x,y

2.用快速幂思想(二进制拆分思想),把x调整到和y一个深度。

3.如果x==y 则已经找到。如果不等于,就把x,y同时向上调整,保持深度相同且二者不相会。

5.调整后,x,y必定只差一步相会,输出他们的父节点,就是LCA

具体看代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;

const int N =40010,M=N*2;

int root;
int n,m;
int h[N],e[M],ne[M],idx;
int depth[N];
int fa[N][16];

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

void bfs()
{
    memset(depth,0x3f,sizeof depth);

    depth[0]=0;depth[root]=1;
    queue<int> q;
    q.push(root);

    while(q.size())
    {
        int t=q.front();
        q.pop();
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(depth[j]>depth[t]+1)
            {
                depth[j]=depth[t]+1;//这一步保证了不会更新已经更新过的节点
                q.push(j);
                fa[j][0]=t;
                for(int k=1;k<=15;k++)
                    fa[j][k]=fa[fa[j][k-1]][k-1];
            }
        }
    }
}

int lca(int a,int b)
{
    //先跳到同一层
    if(depth[a]<depth[b]) swap(a,b);
    for(int k=15;k>=0;k--)
        if(depth[fa[a][k]]>=depth[b])
            a=fa[a][k];
    if(a==b) return a;
    //跳到lca的下一层
    for(int k=15;k>=0;k--)
        if(fa[a][k]!=fa[b][k])
        {
            a=fa[a][k];
            b=fa[b][k];
        }
    return fa[a][0];
}



int main()
{
    scanf("%d",&n);

    memset(h,-1,sizeof h);

    for(int i=0;i<n;i++)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        if(b==-1) root=a;
        else add(a,b),add(b,a);
    }

    bfs();
    cin>>m;
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        int p=lca(a,b);
        if(a==p) puts("1");
        else if(p==b) puts("2");
        else puts("0");
    }
    return 0;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4562419/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 二、LCA的tarjan算法

在线做法:读一个询问,处理一个,输出一个
离线做法:读完全部询问,再全部处理完,再全部输出

LCA的tarjan算法时离线算法,本质上是使用并查集对“向上标记法”的优化。把m个询问统一读入,最后统一输出。

在深度优先遍历的任意时刻,树上的点分为三类:
1.已经访问完毕并且回溯的点,标记为2

2.已经开始递归,但是没有回溯的点,这些结点就是正在访问的结点x以及x的祖先。标记为1

3.尚未开始访问的点,没有标记。

对于正在访问的结点x,他到根节点的路径已经标记为1。如果y是已经回溯的点,则lca(x,y)就是y向上走到根,第一个遇到标记为1的点。

 用并查集优化,当一个节点获得2标记时,把它所在的集合合并到它父节点所在的集合中(合并时父节点标记一定是1,且单独构成一个集合)

这相当于每个已经回溯过的点都指向它的父节点,即向上走遇到的第一个开始递归但没有回溯的点,也就是x所在的支路上深度比x小的某一个点,也就是LCA(x,y)。

本题中最短距离是dist(x)+dist(y)-2*dist(LCA(x,y))

#include<iostream>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;

typedef pair<int,int> PII;

const int N =10010,M=N*2;

int h[N],e[M],ne[M],w[M],idx;
int n,m;
int dist[N];
int res[M];//每个询问的结果
int st[N];
int p[N];
vector<PII> query[M];


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

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

void dfs(int u,int fa)
{
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(j==fa) continue;
        dist[j]=dist[u]+w[i];
        dfs(j,u);
    }
}

void tarjan(int u)
{
    st[u]=1;//正在递归的结点
    for(int i=h[u];~i;i=ne[i])
    {
        int j=e[i];
        if(!st[j])
        {
            tarjan(j);
            p[j]=u;
        }
    }

    for(auto item:query[u])
    {
        int y=item.first,id=item.second;
        if(st[y]==2)
        {
            int anc=find(y);
            res[id]=dist[u]+dist[y]-2*dist[anc];
        }
    }
    st[u]=2;
}

int main()
{
    cin>>n>>m;

    memset(h,-1,sizeof h);
    for(int i=1;i<=n;i++) p[i]=i;
    for(int i=0;i<n-1;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        add(a,b,c),add(b,a,c);
    }
    for(int i=0;i<m;i++)
    {
        int a,b;
        cin>>a>>b;
        if(a!=b)
        {
            query[a].push_back({b,i});
            query[b].push_back({a,i});
        }
    }

    dfs(1,-1);
    tarjan(1);
    for(int i=0;i<m;i++) cout<<res[i]<<endl;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4570518/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

三、例题

 AcWing 356. 次小生成树 

 在之前介绍过严格次小生成树。它与最小生成树只差一条边,构造方法是枚举所有非树边,设该边连接的是a,b。则用它替代a到b上的最大边或者是次大边。

枚举中边权和最大的为答案。

关键是如何求出任意两个点的最大边和次大边。我们仿照动态规划求LCA的思想。用d1[i][k]记录j点向上跳2^k次方步的最大边,d2为次大边。

状态转移:在一棵树中,a到b的简单路为a到LCA(a,b)到b。因此借助LCA更新。具体方式不赘述。

#include<iostream>
#include<algorithm>
#include<queue>
#include<cstring>
using namespace std;

typedef  long long LL;
const int N =100010,M=300010,INF=0x3f3f3f3f;

int n,m;
struct Edge{
    int a,b,w;
    bool used;
    bool operator< (const Edge& t)const
    {
        return w<t.w;
    }
}edge[M];

int p[N];
int h[N],e[M],ne[M],w[M],idx;
int depth[N],fa[N][17],d1[N][17],d2[N][17];

void add(int a,int b,int c)
{
    e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int find(int x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
LL krusal()//预处理树边
{
    LL res=0;
    for(int i=0;i<=n;i++)  p[i]=i;
    sort(edge,edge+m);
    for(int i=0;i<m;i++)
    {
        int a=find(edge[i].a),b=find(edge[i].b),w=edge[i].w;
        if(a!=b)
        {
            edge[i].used=true;
            res+=w;
            p[a]=b;
        }
    }
    return res;
}

void build()
{
    memset(h,-1,sizeof h);
    for(int i=0;i<m;i++)
        if(edge[i].used)
        {
            int a=edge[i].a,b=edge[i].b,w=edge[i].w;
            add(a,b,w),add(b,a,w);
        }
}

void bfs()
{
    queue<int> q;
    memset(depth,0x3f,sizeof depth);
    depth[0]=0;depth[1]=1;
    
    q.push(1);
    
    while(q.size())
    {
        int t=q.front();
        q.pop();
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(depth[j]>depth[t]+1)
            {
                depth[j]=depth[t]+1;
                q.push(j);
                fa[j][0]=t;
                d1[j][0]=w[i];d2[j][0]=-INF;
                for(int k=1;k<=16;k++)
                {
                    int anc=fa[j][k-1];
                    fa[j][k]=fa[anc][k-1];
                    int distance[4]={d1[j][k-1],d1[anc][k-1],d2[j][k-1],d2[anc][k-1]};
                    for(int u=0;u<4;u++)
                    {
                        int d=distance[u];
                        if(d>d1[j][k]) d2[j][k]=d1[j][k],d1[j][k]=d;
                        else if(d!=d1[j][k]&&d>d2[j][k]) d2[j][k]=d;
                    }
                }
            }
        }
    }
    
}

int lca(int a,int b,int w)
{
    static int distance[N*2];
    int cnt=0;
    if(depth[a]<depth[b])
        swap(a,b);
        
    for(int k=16;k>=0;k--)
        if(depth[fa[a][k]]>=depth[b])
        {
            distance[cnt++]=d1[a][k];
            distance[cnt++]=d2[a][k];
            a=fa[a][k];//最后才跳
        }
    if(a!=b)
    {
        for(int k=16;k>=0;k--)
        {
            if(fa[a][k]!=fa[b][k])
            {
                distance[cnt++]=d1[a][k];
                distance[cnt++]=d2[a][k];
                distance[cnt++]=d1[b][k];
                distance[cnt++]=d2[b][k];
                a=fa[a][k],b=fa[b][k];
            }
        }
        distance[cnt++]=d1[a][0];
        distance[cnt++]=d1[b][0];
    }
    int dist1=-INF,dist2=-INF;
    for(int i=0;i<cnt;i++)
    {
        int d = distance[i];
        if (d > dist1) dist2 = dist1, dist1 = d;
        else if (d != dist1 && d > dist2) dist2 = d;
    }
    if(w>dist1) return w-dist1;
    if(w>dist2) return w-dist2;
    
    return INF;
}

int main()
{
    cin>>n>>m;
    for(int i=0;i<m;i++)
    {
        int a,b,w;
        cin>>a>>b>>w;
        edge[i]={a,b,w};
    }
    LL sum=krusal();
    build();
    bfs();
    LL res=1e18;
    for(int i=0;i<m;i++)
        if(!edge[i].used)
        {
            int a=edge[i].a,b=edge[i].b,w=edge[i].w;
            res=min(res,sum+lca(a,b,w));
        }
    cout<<res;
}

AcWing 352. 闇の連鎖 (树上差分)

 树上差分:区间操作对应路径操作,前缀和对应子树和。

根据题意,主要边构成一棵树,附加边是非树边。把一条附加边添加到主要边构成的树中,会形成一个环。因此如果第一步切断x,y之间路径的某条边,那么第二步就必须切断附加边(x,y)。

因此我们设每条附加边把(x,y)之间路径上的每条边“覆盖了一次”。我们只需要统计每条主要边被覆盖了多少次。如果第一次把覆盖0次的主要边切断,第二步可以切断任意一条附加边。

如果第一次把覆盖1次的主要边切断,第二步唯一

如果第一次把覆盖2次以上的主要边切断,第二步无解。

树上差分:对每条非树边,令节点x+1,节点y+1,节点LCA(x,y)-2。最后做一次dfs,求出以F数组,代表以x为根的子树中各节点的权值之和。表示,x节点与他的父节点之间的树边被覆盖的次数。

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;

const int N =100010,M=2*N;

int n,m;
int h[N],e[M],ne[M],idx;
int d[N];
int depth[N];
int fa[N][17];
int ans;

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

void bfs()
{
    memset(depth,0x3f,sizeof depth);
    depth[0]=0,depth[1]=1;
    queue<int> q;
    q.push(1);

    while(q.size())
    {
        int t=q.front();
        q.pop();
        for(int i=h[t];i!=-1;i=ne[i])
        {
            int j=e[i];
            if(depth[j]>depth[t]+1)
            {
                depth[j]=depth[t]+1;
                q.push(j);
                fa[j][0]=t;
                for(int k=1;k<=16;k++)
                    fa[j][k]=fa[fa[j][k-1]][k-1];
            }
        }
    }
}

int lca(int a,int b)
{
    if(depth[a]<depth[b]) swap(a,b);

    for(int k=16;k>=0;k--)
        if(depth[fa[a][k]]>=depth[b])
            a=fa[a][k];

    if(a==b) return a;
    for(int k=16;k>=0;k--)
        if(fa[a][k]!=fa[b][k])
        {
            a=fa[a][k];
            b=fa[b][k];
        }
    return fa[a][0];
}

int dfs(int u,int father)
{
    int res=d[u];
    for(int i=h[u];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(j==father) continue;
        int s=dfs(j,u);
        if(s==0) ans+=m;
        else if(s==1) ans++;
        res+=s;
    }
    return res;
}

int main()
{
    cin>>n>>m;
    memset(h,-1,sizeof h);
    for(int i=0;i<n-1;i++)
    {
        int a,b;
        cin>>a>>b;
        add(a,b),add(b,a);
    }
    bfs();

    for(int i=0;i<m;i++)
    {
        int a,b;
        cin>>a>>b;
        int p=lca(a,b);
        d[a]++,d[b]++,d[p]-=2;
    }
    dfs(1,-1);
    cout<<ans;
}

作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4587939/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


http://www.jnnr.cn/a/273020.html

相关文章

人工智能轨道交通行业周刊-第32期(2023.1.30-2.5)

本期关键词&#xff1a;智能装车系统、南昌地铁巡检机器人、中国铁道学会科学技术奖、AIGC报告、智慧城市 1 整理涉及公众号名单 1.1 行业类 RT轨道交通中关村轨道交通产业服务平台人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交…

配置安全的linux-apache服务器(5)

实验简介 实验所属系列&#xff1a;Linux网络服务配置与安全 实验对象&#xff1a; 本科/专科信息安全专业、网络工程 相关课程及专业&#xff1a;系统安全配置、服务器配置、计算机网络 实验时数&#xff08;学分&#xff09;&#xff1a;2学时 实验类别&#xff1a;实践实验…

删除文件还有救吗?9个最好数据恢复软件你值得尝试

数据恢复软件是一种程序&#xff0c;可帮助从意外删除的存储设备中恢复数据。在这篇博文中&#xff0c;我们彻底研究并生成了适用于 Windows PC 的最佳数据恢复软件列表。我们推荐 奇客数据恢复 作为适用于 Windows 的最佳数据恢复软件。因为它带有不同的扫描模式和选项&#x…

axios中params和data的区别

在开发项目的过程中我们往往忽略了一点&#xff0c;请求接口的传参方式&#xff0c;习惯了post请求就用data,get请求就用params。 params是添加到url的请求字符串中的&#xff0c;用于get请求。服务器并不会读取http body里面的数据,这样我们传递的就是Params里的请求的参数了。…

面试官:Exception和Error有什么区别?

回答思路&#xff1a; 相同点和不同点 异常的分类 异常处理关键字 异常处理的原则 回答总结&#xff1a; 首先相同点是Exception和Error都继承了Throwable类&#xff0c;而不同的是Exception 和 Error 体现了不同异常情况的分类。可以说Error是天灾&#xff0c;出现了也恢复不…

C++基础(4) - 运算符

文章目录输入输出浅谈1、cout 进行 C 输出1.1 控制符 endl1.2 使用 cout 进行拼接2、cin 获取键盘输入常用运算符分类算术运算符1、除法&#xff08;/&#xff09;2、求模&#xff08;%&#xff09;3、自增和自减赋值运算符关系运算符逻辑运算符三元运算符位运算符1、原码、反码…

Java开发学习(四十二)----MyBatisPlus查询语句之条件查询

一、条件查询的类 MyBatisPlus将书写复杂的SQL查询条件进行了封装&#xff0c;使用编程的形式完成查询条件的组合。 这个我们在前面都有见过&#xff0c;比如查询所有和分页查询的时候&#xff0c;都有看到过一个Wrapper类&#xff0c;这个类就是用来构建查询条件的&#xff0…

springboot,vue教务管理系统

开发工具&#xff1a;IDEA服务器&#xff1a;Tomcat9.0&#xff0c; jdk1.8项目构建&#xff1a;maven数据库&#xff1a;mysql5.7前端技术&#xff1a;vue elementUI服务端技术&#xff1a;springbootmybatis本系统拥有三种角色&#xff1a;管理员、教师和学生&#xff0c;项目…

RL笔记:动态规划(1): 策略估计和策略提升

目录 0. 前言 (4.1)策略估计&#xff0c;Policy Evaluation(Prediction) Example 4.1 (python代码) Exercise 4.1 Exercise 4.2 Exercise 4.3 (4.2)Policy Improvement 0. 前言 Sutton-book第4章&#xff08;动态规划&#xff09;学习笔记。本文是关于其中4.1节&#xf…

fpga图像处理(sobel算子)

【声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 关于sobel算子,前面已经讲过计算方法了。一种是上下的sobel算子,一种是左右的sobel算子,两者都相当于prewitt算子的进一步拓展。当然,之前的实现方法都是基于python和opencv实现…

Cadence PCB仿真 使用 Allegro PCB SI 为电源网络分配电压并选择仿真的电源网络的方法图文教程

🏡《总目录》   🏡《分目录》 目录 1,概述2,分配电压3,选择仿真网络4,总结1,概述 进行电源分配网络PDN的仿真前,需要进行一些准备工作。首先需要为电源网络分配适合的电压,并选择需要进行PDN分析的网络。本文介绍其具体方法。 2,分配电压 第1步:执行Analyze→P…

TensorFlow Serving模型部署

7.7 TensorFlow Serving模型部署 学习目标 目标 无应用 应用TensorFlow Serving完成模型服务运行 7.7.1 TensorFlow Serving TensorFlow Serving是一种灵活的高性能服务系统&#xff0c;适用于机器学习模型&#xff0c;专为生产环境而设计。TensorFlow Serving可以轻松部署新…

OpenGL | OpenGL 绘制其他图形

一、绘制点GL_POINTSOpenGL默认绘制的点大小是1px&#xff0c;可以使用glPointSize()来改变点的大小&#xff0c;但是要注意&#xff1a;glPointSize()不能放在glBegin()和glEnd()之间&#xff0c;要放在glBegin()之前。1.代码void Draw() {glClearColor(1, 1, 1, 1.0f); //白…

大数据之HBase基础

文章目录前言一、HBase基础简介&#xff08;一&#xff09;基础介绍&#xff08;二&#xff09;应用场景&#xff08;三&#xff09;特点二、数据模型&#xff08;一&#xff09;行键&#xff08;row key&#xff09;&#xff08;二&#xff09;列&#xff08;三&#xff09;列…

(深度学习快速入门)第四章第三节:卷积层详解1

文章目录一&#xff1a;什么是卷积运算&#xff08;了解&#xff09;二&#xff1a;从全连接层到卷积层&#xff08;1&#xff09;解决空间不变性&#xff08;2&#xff09;解决参数爆炸-稀疏连接和权值共享三&#xff1a;CNN中的图像卷积卷积层&#xff1a;卷积层是CNN中的核心…

道路病害识别监测系统 CNN网络

道路病害识别监测系统通过CNN网络深度学习算法&#xff0c;道路病害识别监测对巡检车上实时监控道路影像数据进行分析&#xff0c;输出道路病害裂缝巡检报告并落图展示。在CNN出现之前&#xff0c;对于图像的处理一直都是一个很大的问题&#xff0c;一方面因为图像处理的数据量…

mybatis plus 更新时间 创建时间自动填充失效的情况和解决方案

问题描述&#xff1a; 调用mybatisplus的IService接口中的update(Wrapper updateWrapper)&#xff0c;update_time没有自动填充。 spuService.update(updateWrapper); 解决方案&#xff1a; 不使用update(wrapper xxx)方法&#xff0c;使用以下方法替代&#xff1a; 1.boole…

Traffic Signs Recognition with 95% Accuracy using CNNKeras

Traffic Signs Recognition 导读 本文采用CNN模型和Keras库 使用GTSRB数据集 构建模型可以分为四部分&#xff1a;1、先探索数据集 2、构建CNN模型 3、训练和验证模型 4、使用测试数据测试模型 保存模型并使用Python自带包tkinter实现GUI 一、数据集 下载完数据后&#xff0c;可…

使用redisson实现分布式锁

在微服务项目中使用redisson实现一个分布式锁 一、引入依赖 spring-boot版本2.3.12.RELEASE <dependencies><dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.6.5</version></dep…

【HTML】HTML 标签 ② ( 排版标签 | 标题标签 | 段落标签 | 水平线标签 | 换行标签 | div 标签 | span 标签 )

文章目录一、排版标签1、标题标签2、段落标签3、水平线标签4、换行标签5、div 标签 和 span 标签HTML 常用的标签有如下类型 : 排版标签文本格式化标签图像标签链接标签 , 其中 链接涉及到 相对路径 与 绝对路径问题 ; 一、排版标签 排版标签 是 网页布局 中 , 最常用的标签 …