转载来自:https://blog.andrewei.info/2015/10/08/e6-9c-80-e8-bf-91-e5-85-ac-e5-85-b1-e7-a5-96-e5-85-88lca-e7-9a-84-e4-b8-89-e7-a7-8d-e6-b1-82-e8-a7-a3-e6-96-b9-e6-b3-95/

简述

LCA(Least Common Ancestors),即最近公共祖先,是指这样一个问题:在有根树中,找出某两个结点 u 和 v 最近的公共祖先(另一种说法,离树根最远的公共祖先)。

算法

RMQ_ST
在线算法

简介

RMQ(Range Minimum/Maximum Query), 即区间最值查询, 是指这样一个问题: 对于长度为 n 的数列 A,回答若干询问 RMQ(A, i, j)(i, j <= n), 返回数列A中下标在i,j之间的最小/大值。所谓在线算法,是指用户每输入一个查询便马上处理一个查询。该算法一般用较长的时间做预处理,待信息充足以后便可以用较少的时间回答每个查询。ST(Sparse Table)算法是一个非常有名的在线处理RMQ问题的算法,它可以在 O(nlogn) 时间内进行预处理,然后在
O(1) 时间内回答每个查询

算法过程

首先是预处理,用动态规划(DP)解决。设 A[i] 是要求区间最值的数列, dp[i, j] 表示从第 i 个数起连续 2^j 个数中的最大值。例如数列 3 2 4 5 6 8 1 2 9 7 ,dp[1,0] 表示第 1 个数起,长度为 2 ^ 0 = 1 的最大值,其实就是 3 这个数。 dp[1, 2] = 5, dp[1, 3] = 8, dp[2, 0] = 2 , dp[2, 1] = 4 ……从这里可以看出 dp[i, 0] 其实就等于 A[i] 。这样,DP 的状态、初值都已经有了,剩下的就是状态转移方程。我们把
dp[i, j] 平均分成两段(因为 dp[i, j] 一定是偶数个数字), 从 i 到i+2 j−1−1为一段,i+2 j−1 到 i+2 j−1 为一段(长度都为 2 j−1)
。用上例说明,当 i = 1,j = 3 时就是 3,2,4,5 和 6,8,1,2 这两段。dp[i,j] 就是这两段的最大值中的最大值。于是我们得到了动态规划方程

 
    dp[i,j]=max(dp[i,j−1],dp[i+2 j−1,j−1])

然后是查询。取 k=[log2(r−l+1)]

则有

 
     RMQ(A,l,r)=min(dp[l,k],dp[r−2k+1,k])

举例说明,要求区间 [2,8] 的最大值,就要把它分成 [2, 5] 和 [5, 8] 两个区间,因为这两个区间的最大值我们可以直接由 dp[2, 2] 和 dp[5, 2] 得到。

参考代码:

#include <cstdio>
const int MAXN = 100010;
int n, q;
int num[MAXN];
int dp[MAXN][20];
void ST(){
for(int i = 1; i <= n; i++)
dp[i][0] = num[i];
for(int j = 1; j < 20; j++)
for(int i = 1; i + (1 << j) - 1 <= n; i++)
dp[i][j] = max(dp[i][j - 1], dp[i + (1 << (j - 1))][j - 1]);
}
int RMQ(int l, int r){
if(l > r) return 0;
int k = log((double)(r - l + 1)) / log(2.0);
return max(dp[l][k], dp[r - (1 << k) + 1][k]);
}
int main(){
while(scanf("%d%d", &n, &q) != EOF){
for(int i = 1; i <= n; i++)
scanf("%d", &num[i]);
ST();
int l, r;
for(int i = 1; i <= q; i ++){
scanf("%d%d", &l, &r);
printf("%d\n", RMQ(l, r));
}
}
return 0;
}

利用 RMQ_ST 算法求解 LCA 问题

思想是: 将树看成一个无向图,u 和 v 的公共祖先一定在 u 与 v 之间的最短路径上:

  1. DFS: 从树 T 的根开始, 进行深度优先遍历(将树 T 看成一个无向图), 并记录下每次到达的顶点, 以及这个点的深度, 第一个的结点是 root(T), 每经过一条边都记录它的端点。由于每条边恰好经过 2 次,因此一共记录了 2n - 1 个结点,用 dfn[1, … , 2n-1] 来表示。
  2. 计算first: 用 first[i] 表示 dfn 数组中第一个值为 i 的元素下标,即如果 first[u] < first[v] 时,DFS 访问的顺序是 dfn[first[u],first[u]+1,…,first[v]]。虽然其中包含
    u 的后代,但深度最小的还是 u 与 v 的公共祖先。
  3. RMQ: 当 first[u]>=first[v] 时,LCA[T,u,v]=RMQ(L,first[v],first[u]);
  4. 否则 LCA[T,u,v]=RMQ(L,first[u],first[v]),
    计算 RMQ。

    由于 RMQ 中使用的ST算法是在线算法,所以这个算法也是在线算法。

参考代码:

int pcnt = 0;               // 用来计算遍历序
int first[MAXN];
int dfn[2 * MAXN]; // 注意数组大小
int deepth[2 * MAXN];
int dp[2 * MAXN][20];
void dfs(int u, int fa, int dep){
dfn[++pcnt] = u;
first[u] = pcnt;
deepth[pcnt] = dep;
for(int i = head[u]; i + 1; i = edge[i].nxt){
int v = edge[i].v;
if(v == fa) continue;
dfs(v, u, dep + 1);
dfn[++pcnt] = u;
deepth[pcnt] = dep;
}
}
void ST(){
for(int i = 1; i <= pcnt; i++)
dp[i][0] = i;
for(int j = 1; j < 20; j++)
for(int i = 1; i + (1 << j) - 1 <= pcnt; i++){
int a = dp[i][j - 1], b = dp[i + (1 << (j - 1))][j - 1];
if(deepth[a] < deepth[b]) dp[i][j] = a;
else dp[i][j] = b;
}
}
int RMQ(int l, int r){
int k = log((double)(r - l + 1)) / log(2.0);
int a = dp[l][k], b = dp[r - (1 << k) + 1][k];
if(deepth[a] < deepth[b]) return a;
else return b;
}
int LCA(int L, int R){
int l = first[L];
int r = first[R];
if(l > r) swap(l, r);
int pos = RMQ(l, r);
return dfn[pos];
}

离线 TarJan 算法

简述

Tarjan算法(以发现者Robert Tarjan命名)是一个在图中寻找强连通分量的算法。算法的基本思想为:任选一结点开始进行深度优先搜索dfs(若深度优先搜索结束后仍有未访问的结点,则再从中任选一点再次进行)。搜索过程中已访问的结点不再访问。搜索树的若干子树构成了图的强连通分量。

应用到要解决的LCA问题上,则是:对于新搜索到的一个结点 u, 先创建由 u 构成的集合,再对 u 的每颗子树进行搜索,每搜索完一棵子树,这时候子树中所有的结点的最近公共祖先就是 u 了。

算法思想

Tarjan算法基于dfs的框架,对于新搜到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每个子树进行搜索;

每搜索完一棵子树,则可确定子树内的LCA询问都已解决,其他的LCA询问的结果必然在这个子树之外;

这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先;

之后继续搜索下一棵子树,直到当前结点的所有子树搜完;

这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问;

如果有一个从当前结点到结点v的询问,且v已经被检查过;

则由于进行的是dfs,当前结点与v的最近公共祖先一定还没有被检查;

而这个最近公共祖先的包含v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先;

算法步骤

对于每一个结点:

  1. 建立以u为代表元素的集合;
  2. 遍历与u相连的结点v,如果没有被访问过,对于v使用Tarjan_LCA算法,结束后将v的集合并入u的集合;
  3. 对于与u有关的询问(u,v),如果v被访问过,则结果就是v所在集合的代表元素;

参考代码:

const int MAXN = 300100;
struct Edge{
int v, nxt;
int id;
};
bool vis[MAXN];
int n, q, ecnt, qcnt;
Edge tedge[MAXN * 2], qedge[MAXN * 2];
int thead[MAXN], qhead[MAXN], res[MAXN], par[MAXN];
void init(){
ecnt = qcnt = 0;
memset(vis, 0, sizeof(vis));
memset(tedge, 0, sizeof(tedge));
memset(qedge, 0, sizeof(qedge));
memset(thead, -1, sizeof(thead));
memset(qhead, -1, sizeof(qhead));
}
void addEdge(int *head, Edge *edge, int &cnt, int u, int v, int id){
edge[cnt].v = v;
edge[cnt].id = id;
edge[cnt].nxt = head[u];
head[u] = cnt++;
}
int Find(int x){
if(par[x] != x) return par[x] = Find(par[x]);
return par[x];
}
void Union(int u, int v){
int fu = Find(u);
int fv = Find(v);
par[fu] = fv;
}
void Tarjan(int u){
par[u] = u;
vis[u] = true;
for(int i = thead[u]; i + 1; i = tedge[i].nxt){
int v = tedge[i].v;
if(vis[v]) continue;
Tarjan(v);
Union(v, u);
}
for(int i = qhead[u]; i + 1; i = qedge[i].nxt){
int v = qedge[i].v;
int id = qedge[i].id;
if(!vis[v]) continue;
res[id] = Find(v);
}
}

(拓展)运用Tarjan算法求图上两点间最大边

参考代码:

const int MAXN = 50050;
struct Edge{
int v, nxt;
int index;
};
bool vis[MAXN];
int Min[MAXN], Max[MAXN];
int ecnt, qcnt, acnt, n, q;
int uu[MAXN], vv[MAXN], val[MAXN];
Edge tedge[2 * MAXN], qedge[2 * MAXN], aedge[2 * MAXN];
int thead[MAXN], qhead[MAXN], ahead[MAXN], par[MAXN], res[MAXN];
void init(){
ecnt = qcnt = acnt = 0;
memset(res, 0, sizeof(res));
memset(val, 0, sizeof(val));
memset(vis, 0, sizeof(vis));
memset(tedge, 0, sizeof(tedge));
memset(qedge, 0, sizeof(qedge));
memset(aedge, 0, sizeof(aedge));
memset(thead, -1, sizeof(thead));
memset(qhead, -1, sizeof(qhead));
memset(ahead, -1, sizeof(ahead));
}
void addEdge(int *head, Edge *edge, int u, int v, int index, int &cnt){
edge[cnt].v = v;
edge[cnt].index = index;
edge[cnt].nxt = head[u];
head[u] = cnt++;
}
int Find(int x){
if(x == par[x]) return par[x];
int temp = par[x];
par[x] = Find(par[x]);
Max[x] = max(Max[x], Max[temp]);
Min[x] = min(Min[x], Min[temp]);
return par[x];
}
void Tarjan(int u){
vis[u] = true;
par[u] = u;
for(int i = qhead[u]; i + 1; i = qedge[i].nxt){
int v = qedge[i].v, index = qedge[i].index;
if(!vis[v]) continue;
int lca = Find(v);
addEdge(ahead, aedge, lca, v, index, acnt);
}
for(int i = thead[u]; i + 1; i = tedge[i].nxt){
int v = tedge[i].v;
if(vis[v]) continue;
Tarjan(v);
par[v] = u;
}
for(int i = ahead[u]; i + 1; i = aedge[i].nxt){
int index = aedge[i].index;
Find(uu[index]);
Find(vv[index]);
res[index] = max(Max[
res[index] = max(max(Up[uu[index]], Down[vv[index]]), Max[vv[index]] - Min[uu[index]]);
}
}

简介倍增
LCA

倍增法LCA也是一个求最近公共祖先的在线算法,他利用了二分搜索的思想降低每次寻找最近公共祖先的复杂度,预处理的复杂度为 O(nlog(n)), 每次查询的复杂度为 O(log(n))。

算法流程

  1. 初始化所有点的深度和第 2^0, 2^1, 2^2, … 2^n 个祖先;
  2. 从深度大的节点上升至深度小的节点同层,如果此时两节点相同直接返回此节点,即lca,否则,利用倍增法找到最小深度的p[a][j]!=p[b][j],此时他们的父亲p[a][0]即lca。

参考代码:

const int MAXN = 300010;
const int DEG = 20; struct Edge{
int v, nxt;
}; int ecnt, n, m;
Edge edge[MAXN * 2];
int fa[MAXN][20];
int head[MAXN], depth[MAXN]; void init(){
ecnt = 0;
memset(edge, 0, sizeof(edge));
memset(head, -1, sizeof(head));
memset(depth, 0, sizeof(depth));
} void addEdge(int u, int v){
edge[ecnt].v = v;
edge[ecnt].nxt = head[u];
head[u] = ecnt++;
} void initfa(int root){
queue <int> que;
que.push(root);
depth[root] = 0;
fa[root][0] = root;
while(!que.empty()){
int u = que.front();
que.pop(); for(int i = 1; i < DEG; i++)
fa[u][i] = fa[fa[u][i - 1]][i - 1]; for(int i = head[u]; i + 1; i = edge[i].nxt){
int v = edge[i].v;
if(v == fa[u][0]) continue;
depth[v] = depth[u] + 1;
fa[v][0] = u;
que.push(v);
}
}
} int LCA(int u, int v){
if(depth[u] > depth[v]) swap(u, v);
int du = depth[u], dv = depth[v];
int tu = u, tv = v;
for(int det = dv - du, i = 0; det; det >>= 1, i++)
if(det & 1) tv = fa[tv][i];
if(tu == tv) return tu;
for(int i = DEG - 1; i >= 0; i--){
if(fa[tu][i] == fa[tv][i]) continue;
tu = fa[tu][i];
tv = fa[tv][i];
}
return fa[tu][0];
}

最近公共祖先(LCA)的三种求解方法的更多相关文章

  1. 学习笔记--最近公共祖先(LCA)的几种求法

    前言: 给定一个有根树,若节点\(z\)是两节点\(x,y\)所有公共祖先深度最大的那一个,则称\(z\)是\(x,y\)的最近公共祖先(\(Least Common Ancestors\)),简称\ ...

  2. 最近公共祖先 LCA 递归非递归

    给定一棵二叉树,找到两个节点的最近公共父节点(LCA).最近公共祖先是两个节点的公共的祖先节点且具有最大深度.假设给出的两个节点都在树中存在. dfs递归写法 查找两个node的最近公共祖先,分三种情 ...

  3. Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集)

    Luogu 2245 星际导航(最小生成树,最近公共祖先LCA,并查集) Description sideman做好了回到Gliese 星球的硬件准备,但是sideman的导航系统还没有完全设计好.为 ...

  4. 【lhyaaa】最近公共祖先LCA——倍增!!!

    高级的算法——倍增!!! 根据LCA的定义,我们可以知道假如有两个节点x和y,则LCA(x,y)是 x 到根的路 径与 y 到根的路径的交汇点,同时也是 x 和 y 之间所有路径中深度最小的节 点,所 ...

  5. POJ 1470 Closest Common Ancestors(最近公共祖先 LCA)

    POJ 1470 Closest Common Ancestors(最近公共祖先 LCA) Description Write a program that takes as input a root ...

  6. POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA)

    POJ 1330 Nearest Common Ancestors / UVALive 2525 Nearest Common Ancestors (最近公共祖先LCA) Description A ...

  7. [模板] 最近公共祖先/lca

    简介 最近公共祖先 \(lca(a,b)\) 指的是a到根的路径和b到n的路径的深度最大的公共点. 定理. 以 \(r\) 为根的树上的路径 \((a,b) = (r,a) + (r,b) - 2 * ...

  8. javase-常用三种遍历方法

    javase-常用三种遍历方法 import java.util.ArrayList; import java.util.Iterator; import java.util.List; public ...

  9. JS面向对象(3) -- Object类,静态属性,闭包,私有属性, call和apply的使用,继承的三种实现方法

    相关链接: JS面向对象(1) -- 简介,入门,系统常用类,自定义类,constructor,typeof,instanceof,对象在内存中的表现形式 JS面向对象(2) -- this的使用,对 ...

随机推荐

  1. RBAC权限管理设计

    一.权限简介 1. 问:为什么程序需要权限控制? 答:生活中的权限限制,① 看灾难片电影<2012>中富人和权贵有权登上诺亚方舟,穷苦老百姓只有等着灾难的来临:② 屌丝们,有没有想过为什么 ...

  2. Windows 记事本的 ANSI、Unicode、UTF-8 这三种编码模式有什么区别?

    [梁海的回答(99票)]: 简答.一些细节暂无精力查证,如果说错了还请指出. 一句话建议:涉及兼容性考量时,不要用记事本,用专业的文本编辑器保存为不带 BOM 的UTF-8. * * * 如果是为了跨 ...

  3. linux usb总线驱动(一)

    目录 linux usb总线驱动框架 USB 介绍 传输类型 控制器接口 2440接口 基本流程 alloc_dev choose_address hub_port_init usb_get_devi ...

  4. 【Sql Server】SQL SERVER 收缩日志

    事务日志记录着在相关数据库上的操作,同时还存储数据库恢复(recovery)的相关信息. 收缩日志的原因有很多种,有些是考虑空间不足,有些则是应用程序限制导致的. 下面介绍的是在简单模式下,进行收缩操 ...

  5. Node.js实战项目学习系列(1) 初识Node.js

    前言 一直想好好学习node.js都是半途而废的状态,这次沉下心来,想好好的学习下node.js.打算写一个系列的文章大概10几篇文章,会一直以实际案例作为贯穿的学习. 什么是node Node.js ...

  6. MySQL数据库学习2 - 数据库的操作

    一.系统数据库 二.创建数据库 三.数据库相关操作 四.了解内容 一.系统数据库 执行如下命令,查看系统库 show databases; information_schema: 虚拟库,不占用磁盘空 ...

  7. vue组件化的应用

    前言:vue组件化的应用涉及到vue-cli的内容,所以在应用之前是需要安装node和vue-cli的,具体如何安装我就不一一赘述了.可能一会儿我心情好的时候,可以去整理一下. 1.应用的内容:在一个 ...

  8. CentOS7离线安装MySQL

    1.删除原有的mariadb,不然mysql装不进去 mariadb-libs-5.5.52-1.el7.x86_64 rpm -qa|grep mariadb rpm -e --nodeps mar ...

  9. 使用tablayout和recyclerview的时候,报重复添加Fragment错误

    原因: 在添加的子Fragment报错了, 出现了空值错误, 此时报出来错误是前一个Fragment重复添加

  10. 让你爱不释手的 Python 模块

     一. logzero 在一个完整的信息系统里面,日志系统是一个非常重要的功能组成部分.它可以记录下系统所产生的所有行为.我们可以使用日志系统所记录的信息为系统进行排错,优化系统的性能,或者根据这些 ...