LCA总结

文章目录

    • LCA介绍
    • 解决方法概括:
    • 倍增法:
    • Tarjan
    • 欧拉序
    • 树剖解法:

看了很多关于LCA的文章,这里是一个总结

我们通过这个题洛谷P3379 【模板】最近公共祖先来讲LCA

LCA介绍

lca是啥?最近公共祖先
就是:两个点在这棵树上距离最近的公共祖先节点
LCA(x,y)=z,z是x与y的最近公共祖先,(换句话说z的深度要尽可能大)
来看一个经典图
在这里插入图片描述
LCA(4,5)=2
LCA(4,3)=1
LCA(2,1)=1

解决方法概括:

常用四种方法 :

  1. 用倍增法求解,预处理复杂度是 O(nlogn) ,每次询问的复杂度是 O(logn), 属于在线解法。
  2. 利用欧拉序转化为RMQ问题,用ST表求解RMQ问题,预处理复杂度 O(n+nlogn),每次询问的复杂度为 O(1),也是在线算法。
  3. 采用Tarjan算法求解,复杂度 O(α(n)+Q),属于离线算法。
  4. 利用树链剖分求解,复杂度预处理O(n),单次查询 O(logn) ,属于在线算法。

倍增法:

倍增:将两个点调到一个高度之后不断同时向上调到两点重合,即为两点的最近公共祖先
所用到的函数: grand[x][i] ,这个数组表示标号为x节点向上跳2^i步的节点
例如grand[5][0]=2(上图), 节点5向上跳20次(1次)到达节点2
grand[5][0]就是x的父节点
grand[x][1]就是x的父亲节点的父亲节点,就是grand[grand[x][0]][0]
这样就能得到一个递推式grand [ x ] [ i ] = grand [ grand [ x ] [ i-1 ] ] [ i-1 ]
先让x与y处于同一层,然后一起往上跳
跳多少呢?
比如dep[u]>dep[v]
u要向上爬h=dep[u]-dep[v],才能和v相同深度
将h进行二进制拆分,比如
h=(15)10=(1111)2
h=(5)10=(101)2
从低位开始i=0,如果是这一位是1,就grand[u][i]
任何调动次数都可以用2的指数幂之和来表示O(log n)
h = 5 = 22 + 20
h = 15 = 23 + 22 + 21 + 20
dep[]表示节点的深度
一开始跳log2dep[x]-dep[y],log我们可以打表预处理。

lg[0]=-1;
for(int i=1;i<=n;i++)lg[i]=lg[i>>1]+1;

当两点汇合时,就可以返回了
查询m组,总的复杂度应该是O(m log n)
输入:

9 1 1
1 2
1 3
2 4
2 5
4 7
4 8
3 6
6 9
5 8 
2
#include<bits/stdc++.h>
#define maxn 500005
using namespace std;
int n, m, root;
int read()
{int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();};while(isdigit(ch)) x = (x << 3) + (x << 1) + ch - '0', ch = getchar();return x * f;
}struct edge
{int to, nxt;edge() {}edge(int tt, int nn) {to = tt, nxt = nn;}
}e[maxn << 1];int head[maxn], k = 0;
void add(int u, int v)
{e[k] = edge(v, head[u]);head[u] = k++;
}int fa[maxn][25], dep[maxn], lg[maxn];
void dfs(int now, int fath)//初始化深度及祖祖辈辈
{dep[now] = dep[fath] + 1;fa[now][0] = fath;for(int i = 1; (1 << i) <= dep[now]; i++)fa[now][i] = fa[fa[now][i - 1]][i - 1];//前文的递推式for(int i = head[now]; ~i; i = e[i].nxt)if(e[i].to != fath) dfs(e[i].to, now);//继续往下遍历
}int lca(int x, int y)
{if(dep[x] < dep[y]) swap(x, y);//保证x的深度更大,跳xwhile(dep[x] > dep[y]) x = fa[x][lg[dep[x] - dep[y]]];if(x == y) return x;//特判for(int i = lg[dep[x]]; i >= 0; i--)//倍增一起往上跳if(fa[x][i] != fa[y][i]) x = fa[x][i], y = fa[y][i];return fa[x][0];
}int main()
{memset(head, -1, sizeof head);n = read(), m = read(), root = read();register int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);}dfs(root, 0);lg[0]=-1;
for(int i=1;i<=n;i++)lg[i]=lg[i>>1]+1;
//	for(int i = 1; i <= n; i++)
//		lg[i] = lg[i - 1] + (1 << lg[i - 1] == i);//log打表,后面那一坨是特判一下i是否进位了for(int i = 1; i <= m; i++){u = read(), v = read();printf("%d\n", lca(u, v));}return 0;
}

Tarjan

倍增是在线算法
Tarjan是离线算法
在这里插入图片描述
Tarjan主要用到和并查集差不多的方法
比如查询7和5
我们从根节点出发,1->2->4->7,发现另一点5还没被查询,然后回溯,再遍历,7->4->8->4->2->5,遍历到点5,我们发现另外一个点7已经访问过,就到此结束。在这过程中我们用fa[]来表示父节点,就和并查集一样,回溯时,7->4 , fa[7]=4 ;8->4 , fa[8]=4; 4->2,fa[4]=2; 5->2,fa[5]=2。
这样当发现另外一个点已经标记了,那么这个点的祖先一定是两个点的lca(可以通过路径压缩)
在访问一个点时,我们会将与这点相关的一同询问,所以tarjan是强制离线
lca用于存结果,每个询问都存了两次(因为还要查询另外一个点是否已经访问过),最后输出时 i * 2
每组答案lca[i*2](i->m)
大概时间复杂度为O(n+m)

#include<bits/stdc++.h>
#define maxn 500005
using namespace std;
int n, m, root, lca[maxn << 1];
int read()
{int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();return x * f;
}struct edge
{int to, nxt;edge(){}edge(int tt, int nn) {to = tt, nxt = nn;}
}e[maxn << 1], qe[maxn << 1];int head[maxn], k = 0;
void add(int u, int v)
{e[k] = edge(v, head[u]);head[u] = k++;
}int qhead[maxn], qk = 0;
void qadd(int u, int v)
{qe[qk] = edge(v, qhead[u]);qhead[u] = qk++;
}int fa[maxn];
int get(int x) {return fa[x] == x? x : fa[x] = get(fa[x]);}//记得路径压缩!!bool vis[maxn];
void tarjan(int u)
{register int v;vis[u] = 1;for(int i = head[u]; ~i; i = e[i].nxt)//先深优遍历下去{v = e[i].to;if(vis[v]) continue;//vis过了,就说明是父亲tarjan(v);fa[v] = u;//回溯时记录}for(int i = qhead[u]; ~i; i = qe[i].nxt)//开始扫一遍关于u的所有询问{v = qe[i].to;if(vis[v])//另一个点访问过了,可以得出答案了{lca[i] = get(v);if(i & 1) lca[i - 1] = lca[i];//这里特殊处理是因为每个询问存了两次else lca[i + 1] = lca[i];}}
}int main()
{memset(head, -1, sizeof head);memset(qhead, -1, sizeof qhead);n = read(), m = read(), root = read();register int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);fa[i] = i;//顺便初始化fa}fa[n] = n;for(int i = 1; i <= m; i++){u = read(), v = read();//存储询问qadd(u, v);qadd(v, u);}tarjan(root);//开始遍历for(int i = 0; i < m; i++)printf("%d\n", lca[i << 1]);//每个询问都存了两次,所以要*2
}

m较小时用倍增,较大时用Tarjan

欧拉序

欧拉序的定义
树在dfs过程中的节点访问顺序称为欧拉序.
那有人会问:dfs序和欧拉序啥区别?

dfs序:是指将一棵树被dfs时所经过的节点顺序(不绕回原点)。
欧拉序:就是从根结点出发,按dfs的顺序在绕回原点所经过所有点的顺序。

在这里插入图片描述
欧拉序与dfs序不同地方在于,欧拉序中每个节点可以出现多次,比如进入一次退出一次,又比如每次回溯时记录一次。

因此两个点的LCA,就是在该序列上两个点第一次出现的区间内深度最小的那个点
比如求D和E的LCA,D和E第一次出现的区间是DDCBE。这里面深度最小的就是点B,所以LCA(D,E)=B
这样就转化为区间RMQ问题,所以可以用ST表。
具体怎么做呢?
先求出欧拉序和每个节点的深度,同时用start[]记录每个节点第一次出现的位置
LCA ( T , u , v ) = RMQ ( B , start ( u ) , start ( v ) )
然后直接用ST表求RMQ就行
ST表详解

所用到数组:

a.欧拉序:图的遍历(几种存储结构写法不太一样)

cnt:序列长度(每个元素一进一出共两次,记得最大初始化为2*MAXN)
oula[]:欧拉序列,记录编号 dfs前记录一次,dfs后(回溯)再记录一次
depth[]:每个编号的深度(也可以记录每个下标的深度,见注释)

start[]:每个编号第一次出现的序列下标

b.ST表

minl[i][j] 记得第一层初始化为depth[]

pos[][]最值下标,第一层初始化为i

注意这里i是欧拉序列的下标,最终要的是编号(这里经常下标搞混!!)

欧拉序列下标=start【编号】

#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
#define N 500010  
vector<int>G[N]; 
int oula[N<<1],depth[N],start[N];//N*2
int cnt;int minl[N<<1][19],pos[N<<1][19],len[N<<1],vis[N],tmp; //开20以上就TLE,自闭
//struct node{int deep,order;}minl[N][19];int n,m,s,x,y;inline int read()
{char ch='*';while(!isdigit(ch=getchar()));int num=ch-'0';while(isdigit(ch=getchar()))num=num*10+ch-'0';return num;
}inline void dfs(int now,int fa,int deep){oula[++cnt]=now;//入 //depth[cnt]=deep;if(depth[now]==0)depth[now]=deep;if(start[now]==0)start[now]=cnt;int z=G[now].size();for(int i=0;i<z;i++){if(G[now][i]!=fa){dfs(G[now][i],now,deep+1);oula[++cnt]=now;//出 //depth[cnt]=deep;//index[now]=cnt;}}
}inline void S_table(){for(int i=1;i<2*N;++i) len[i]=(1<<(tmp+1))==i?++tmp:tmp;for(int i=1;i<=cnt;i++) minl[i][0]= depth[oula[i]],pos[i][0]=i;//depth[i]//int l = log2((double)cnt);for (int j=1;(1<<j)<=cnt;j++){for (int i=1; i + (1 << (j-1) ) - 1 <=cnt;++i){//minl[i][j] = min(minl[i][j-1], minl[i + (1 << (j-1) )][j-1]);if(minl[i][j-1]<minl[i+(1<<(j-1))][j-1])minl[i][j]=minl[i][j-1],pos[i][j]=pos[i][j-1];else minl[i][j]=minl[i + (1 << (j-1) )][j-1],pos[i][j]=pos[i + (1 << (j-1) )][j-1];}}
}inline int rmq(int l, int r){if(l>r) swap(l,r);int k=len[r-l+1];//int k = log2((double)(r-l+1));int mid=r-(1<<k)+1;if(minl[l][k]<=minl[mid][k])return pos[l][k];else return pos[mid][k];
}
int main()
{n=read();m=read();s=read(); for(int i=1;i<n;++i){x=read();y=read();G[x].push_back(y);G[y].push_back(x);}dfs(s,-1,1);//求欧拉序列S_table();//初始化st表for(int i=1;i<=m;++i){x=read();y=read();printf("%d\n",oula[rmq(start[x],start[y])]);}return 0;
}

树剖解法:

树剖比Tarjan慢,但比倍增快

树剖详讲

树剖是把一棵树按子树大小分为链。树剖基本操作中有一个是求x到y的路径的边权和,或者是所有边权进行修改。我们可以用树剖的思路来写LCA。直接看点x和y是否在一条链上,不在则深度较大者跳到链头的父亲节点处,也就是跳出这条链;在则深度较浅者为LCA。
树剖一跳就是一条链,对于n极大的情况就相当于是倍增的再一优化。

#include<bits/stdc++.h>//都是树剖模板操作,就不做多解释了。
#define maxn 500005
using namespace std;
int n, m, root;
int read()
{int x = 0, f = 1, ch = getchar();while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();return x * f;
}
struct edge
{int to, nxt;edge(){}edge(int tt, int nn){to = tt, nxt = nn;}
}e[maxn << 1];int k = 0, head[maxn];
void add(int u, int v)
{e[k] = edge(v, head[u]);head[u] = k++;
}int fa[maxn], dep[maxn], size[maxn], son[maxn];
void dfs_getson(int u)
{size[u] = 1;for(int i = head[u]; ~i; i = e[i].nxt){int v = e[i].to;if(v == fa[u]) continue;dep[v] = dep[u] + 1;fa[v] = u;dfs_getson(v);size[u] += size[v];if(size[v] > size[son[u]]) son[u] = v;}
}int top[maxn];
void dfs_rewrite(int u, int tp)
{top[u] = tp;if(son[u]) dfs_rewrite(son[u], tp);for(int i = head[u]; ~i; i = e[i].nxt){int v = e[i].to;if(v != fa[u] && v != son[u]) dfs_rewrite(v, v);}
}void ask(int x, int y)
{while(top[x] != top[y])//不同则跳{if(dep[top[x]] > dep[top[y]]) swap(x, y);y = fa[top[y]];}if(dep[x] > dep[y]) swap(x, y);printf("%d\n", x);//输出深度较小者
}int main()
{memset(head, -1, sizeof head);n = read(), m = read(), root = read();int u, v;for(int i = 1; i < n; i++){u = read(), v = read();add(u, v);add(v, u);}//树剖初始化dfs_getson(root);dfs_rewrite(root, root);for(int i = 1; i <= m; i++){u = read(), v = read();ask(u, v);}return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ASP.NET Core Web API + Ng6 实战视频 Day 2

第一天课程&#xff1a;ASP.NET Core Web API Identity Server 4 Angular 6 实战小项目视频Day 2 第一部分:Day 2 第二部分:视频专辑持续更新中....地址请点击原文链接.原文地址: http://v.qq.com/vplus/4cfb00af75c16eb8d198c58fb86eb4dc/foldervideos/8hk0029019k2fft.NET…

浮沉子

浮沉子制作及其原理 浮沉子的下沉过程&#xff1a;

.NET Core 项目指定SDK版本

一. 版本里的坑自从 .NET Core 2.1.0版本发布以后&#xff0c;近几个月微软又进行了几次小版本的发布&#xff0c;可见 .NET Core 是一门生命力非常活跃的技术。经过一段时间的实践&#xff0c;目前做 ASP.NET Core 开发时&#xff0c;使用的 Nuget 包&#xff0c;比如 Microso…

牛客网【每日一题】4月17日题目精讲 华华给月月准备礼物

文章目录题目描述题解&#xff1a;代码&#xff1a;推荐例题&#xff1a;试题链接时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C 32768K&#xff0c;其他语言65536K 64bit IO Format: %lld 题目描述 二月中旬虐狗节前夕&#xff0c;华华决定给…

Bing.com在.NET Core 2.1上运行

相关知识请参考.netCore开发团队博客 https://blogs.msdn.microsoft.com/dotnet/ Bing.com是一种云服务&#xff0c;运行在遍布全球许多数据中心的数千台服务器上。Bing服务器每秒处理来自全球消费者的数千个用户查询&#xff0c;通过他们的浏览器&#xff0c;使用Microsoft认…

c#中关于协变性和逆变性(又叫抗变)帮助理解

今天回忆了之前看的《深入理解C#》这本书中的泛型章节&#xff0c;其中对泛型的可变性的理解。泛型可变性分两种&#xff1a;协变和逆变。逆变也又称为抗变。怎么理解这两个名词的意思&#xff1a;①&#xff1a;协变即为在泛型接口类型中使用out标识的类型参数。协变的字面意思…

C# 7.0中的解构功能---Deconstruct

解构元组C#7.0新增了诸多功能&#xff0c;其中有一项是新元组(ValueTuple)&#xff0c;它允许我们可以返回多个值&#xff0c;并且配合解构能更加方便的进行工作&#xff0c;如下面例子可以看到解构元组可以写出优雅的代码&#xff0c;并且可以使用类型推断&#xff0c;但在这里…

csp初赛复习(往年真题+解析)

排序算法 前缀/后缀表达式 二进制补码、反码 最短路 图片/音频/视频文件格式 前序/中序/后序遍历 以比较作为基本运算&#xff0c;在 N 个数中找最小数的最少运算次数为&#xff08; &#xff09;。 A. NNN B. N−1N-1N−1 C. N2N^2N2 D. logNlogNlogN 正确答案&#xff1a; B …

ASP.NET Core 2.1中基于角色的授权

授权是来描述用户能够做什么的过程。例如&#xff0c;只允许管理员用户可以在电脑上进行软件的安装以及卸载。而非管理员用户只能使用软件而不能进行软件的安装以及卸载。它是独立的而又与验证配合使用&#xff0c;需要身份验证机制。对于应用程序来说&#xff0c;首先需要进行…

CF449B Jzzhu and Cities(Dijkstra)

设每个点到1的距离为dis[x]dis[x]dis[x],特殊边为(1,vi,wi)(1,v_i,w_i)(1,vi​,wi​) 1、wi>dis[vi]w_i>dis[v_i]wi​>dis[vi​]的特殊边可以删除 2、widis[vi]且num[vi]>1w_idis[v_i]且num[v_i]>1wi​dis[vi​]且num[vi​]>1,特殊边可以删掉 思路上的偏差&…

分布式事务一致性解决方案

一、从数据一致性谈起↑一致性问题&#xff0c;“万恶之源”是数据冗余和分布并通过网络交互网络异常是常态。1、数据一致性的情形主库、从库和缓存数据一致性&#xff0c;相同数据冗余&#xff0c;关系数据库&#xff0c;为保证关据库的高可用和高性能&#xff0c;一般会采用主…

.net core 生成二维码

其实生成二维码的组件有很多种&#xff0c;如&#xff1a;QrcodeNet&#xff0c;ZKWeb.Fork.QRCoder&#xff0c;QRCoder等我选QRCoder&#xff0c;是因为小而易用、支持大并发生成请求、不依赖任何库和网络服务。既然是.net core 那当然要用依赖注入&#xff0c;通过构造函数注…

小雨坐地铁

链接&#xff1a; 时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C 524288K&#xff0c;其他语言1048576K 64bit IO Format:%lld 题目描述 小雨所在的城市一共有 m 条地铁线&#xff0c;分别标号为 1 号线&#xff0c;2 号线&#xff0c;……&…

计蒜客 91 地铁 HDU 5263 平衡大师(二分+网络流)

先说PPT的思路 PPT的思路源于这句话&#xff1a; 对每条边 (u, v)&#xff0c;连一条 (u, v) 容量为 1&#xff0c;费用为 1 的边。如果 流了表示删去这条边。 流过原图上的边表示删去这条边意味着什么呢&#xff1f; 令dif[u]u的出度-入度 如图&#xff0c;灰边表示原图上的…

asp.net core 外部认证多站点模式实现

PS&#xff1a;之前因为需要扩展了微信和QQ的认证&#xff0c;使得网站是可以使用QQ和微信直接登录。github 传送门 。然后有小伙伴问&#xff0c;能否让这个配置信息&#xff08;appid&#xff0c; appsecret&#xff09;按需改变&#xff0c;而不是在 ConfigureServices 里面…

如何让敏捷软着陆?

背景当前&#xff0c;敏捷已经成为了2018的热词&#xff0c;执行敏捷研发模式的项目多数都是从瀑布模型转型过来的&#xff0c;瀑布模型是一套根深蒂固的传统流程&#xff0c;如果硬着陆的话&#xff0c;很容易折翼。笔者在各类项目敏捷实施过程中&#xff0c;总结了一些接地气…

牛客网【每日一题】4月24日 子序列

链接&#xff1a; 时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C 262144K&#xff0c;其他语言524288K 64bit IO Format:%lld 题目描述 小美有一个由n个元素组成的序列{a1,a2,a3,…,an}&#xff0c;她想知道其中有多少个子序列{ap1,ap2,…,apm}…

[AGC031E] Snuke the Phantom Thief(网络流)

考虑枚举偷的珠宝的个数k&#xff0c;且假设它们按照坐标大小排好了序&#xff08;x坐标排一次&#xff0c;y坐标排一次&#xff09;。 那么可以将条件转化一下&#xff0c; 在珠宝按x坐标排好序时&#xff0c; x坐标大于等于aia_iai​的最多取bib_ibi​个可以转化为取的前k−…

牛妹的游戏

链接&#xff1a; 时间限制&#xff1a;C/C 1秒&#xff0c;其他语言2秒 空间限制&#xff1a;C/C 262144K&#xff0c;其他语言524288K 64bit IO Format: %lld题目描述 UPD:数据保证不会有两条控制链控制的据点完全相同&#xff0c;也保证不会有某条控制链两端控制的据点相同…

.NET Core使用NLog通过Kafka实现日志收集

一、前言NET Core越来越受欢迎&#xff0c;因为它具有在多个平台上运行的原始.NET Framework的强大功能。Kafka正迅速成为软件行业的标准消息传递技术。这篇文章简单介绍了如何使用.NET(Core)和Kafka实现NLog的Target。在日常项目开发过程中&#xff0c;Java体系下Spring Boot …