摘要:

  本文主要介绍了解决LCA(最近公共祖先问题)的两种算法,分别是离线Tarjan算法和在线算法,着重展示了在具体题目中的应用细节。


  最近公共祖先是指对于一棵有根树T的两个结点u和v,它们的LCA(T,u,v)表示一个结点x,满足x是u和v的公共祖先且x深度尽可能的大(也即最近)。

  求最近公共祖先有两种方法:一种是离线求解算法,也就是将询问全部存起来,处理完之后一次回答所有询问;另一种方法就是在线求解算法,对于每次询问,动态地回答。

离线算法Tarjan

  Tarjan算法就是利用深度优先搜索的框架,对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可以确定这棵子树之内的LCA问题都已经解决。其他的LCA问题肯定都在这个子树之外,这时把子树所形成集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。

  之后继续搜索下一棵子树,直到当前结点的所有子树搜索完,这时把当前结点也设为已经被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到v的询问,且v已经被检查过,则由于进行的是深度优先搜索,当前结点和v的LCA一定还没有被检查过,然而这个最近公共祖先的包含v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。

算法实现如下:

 const int maxn = ;//结点数
bool vis[maxn];
int tree[maxn][maxn], ans[maxn][maxn], fa[maxn];
//tree[u][0]表示结点u有几个孩子,分别是
//ans[u][v]表示u和v的LCA
//fa[i]表示i的祖先
set<int> query[maxn];//保存关于u结点的询问 int n;
void init() {
for(int i = ; i <= n; i++) {
fa[i] = i;
vis[i] = ;
}
} int Find(int x) {//查找一个集合的祖先
return fa[x] == x ? x : fa[x] = Find(fa[x]);
} void Union(int x, int y) {//合并两个集合,将x并入y中
int fx = Find(x);
int fy = Find(y);
fa[fx] = fy;
} void dfs(int u) {//dfs遍历树
vis[u] = ;
for(int i = ; i <= tree[u][]; i++) {
int v = tree[u][i];
if(vis[v]) continue;
dfs(v);
Union(v, u);//当遍历完一棵子树的时候,就将子树和父亲合并
}
for(set<int>::iterator it = query[u].begin();
it != query[u].end(); it++) {//当处理完u结点的子树时,就可以回答部分有关u的询问
int v = *it;
if(vis[v]) { //如果v被访问过,就可以回答这个询问
ans[u][v] = Find(v); //u and v 的LCA就是v所在集合的公共祖先
query[v].erase(query[u].find(u));//将v中的此询问删掉
}
}
}

  Tarjan算法可以解决LCA查询要求实现知道全部查询提问,如果LCA要求即问即答,就需要使用在线算法。

在线算法

  在线算法需要对该树进行预处理,生成三个序列:欧拉序列、深度序列、遍历结点第一次出现的时间序列,然后通过RMQ(区间最值查询)来O(1)地回答问题。

巧妙的是只用对树进行一次深度优先遍历,就可以得到这三个序列了。

  结点第一次出现的时间:就是深度优先遍历的过程中第一次遍历到这个结点的时间,该序列的长度是n,记为pos数组,即pos[u] = 3,表示u结点是第三个遍历到的。

  欧拉序列:按照深度优先遍历,依次经过的结点按照遍历顺序全部记录下来,包括回溯的过程,也就是一个点可能被记录多次。该序列的长度由深搜的过程决定,记为t数组。

  深度序列:该序列的长度和欧拉序列的长度一致,记录的是欧拉序列中对应结点的深度,记为dep。

  有了这三个序列,假设我们需要查询LCA(T,u,v),通过pos[u]和pos[v]可以知道u和v结点在t数组和dep数组中是第几个,利用深度优先遍历的过程,可以知道在dep[pos[u]]~dep[pos[v]]中深度最小的结点就是LCA(T,u,v)了。

算法实现如下:

 const int maxn = ;

 int tot, ans[maxn][maxn], link[maxn][maxn];//link存树的结构
int dep[maxn * ], pos[maxn], t[maxn * ];
int dp[maxn * ][];//存储区间最值
bool v[maxn]; void dfs(int u, int dfn) {
if(!v[u]) {
v[u] = ;
pos[u] = tot;
}
dep[tot] = dfn; //深度序列
t[tot++] = u; //欧拉序列
for(int i = ; i <= link[u][]; i++) {
dfs(link[u][i], dfn + ); dep[tot] = dfn;
t[tot++] = u;
}
return;
} void init() {
for(int j = ; ( << j) <= tot; j++) {
for(int i = ; i + ( << j) <= tot; i++) {
if(j == )
dp[i][j] = i;
else {
if(dep[dp[i][j - ]] < dep[dp[i + ( << (j - ))][j - ]])
dp[i][j] = dp[i][j - ];
else
dp[i][j] = dp[i + ( << (j - ))][j - ];
}
}
}
} int RMQ(int p1, int p2) {
int k = log2(p2 - p1 + );
if( (<<k) < p2 - p1 + ) k++;
if(dep[dp[p1][k]] < dep[dp[p2 - ( << k) + ][k]])
return t[dp[p1][k]];
else
return t[dp[p2 - ( << k) + ][k]];
} int lca(int v1, int v2) {
if(pos[v1] < pos[v2])
return RMQ(pos[v1], pos[v2]);
else
return RMQ(pos[v2], pos[v1]);
}

  下面看一道例题:HDU 2586 How far away ?

题意

  输出一棵有根数,问任意两个结点间的距离

解题思路

  首先问一次计算一次不是什么好的办法,我们可以将每个结点到根结点的距离预处理出来,然后找到两个结点的最近公共祖先,然后答案就是dis[u] + dis[v] - 2 * dis[lca(u, v)]。因为是即问即答,所以采用在线的方法。

  注意RMQ中k的计算方式有所不同,采用之前的方法计算会发生数组访问越界。

代码如下:

 #include <cstdio>
#include <vector>
#include <cmath>
#include <cstring> using namespace std; const int maxn = ;
struct E{
int v, ne, d;
E(){}
E(int _v, int _n, int _d): v(_v), ne(_n), d(_d){}
}e[maxn * ]; int t[maxn * ], dep[maxn * ], pos[maxn], dis[maxn];
int head[maxn], esize;
bool vis[maxn];
int dp[maxn * ][];
int n, m, tot; void init() {
esize = tot = ;
memset(vis, , sizeof(vis));
memset(dis, , sizeof(dis));
memset(head, -, sizeof(head));
} void add(int u, int v, int d) {
e[esize] = E(v, head[u], d);
head[u] = esize++;
} void dfs(int u, int de) {
if(!vis[u]) {
vis[u] = ;
pos[u] = tot;
}
dep[tot] = de;
t[tot++] = u; for(int i = head[u]; i != -; i = e[i].ne) {
int v = e[i].v;
int d = e[i].d;
if(vis[v]) continue;
dis[v] = dis[u] + d;
dfs(v, de + ); dep[tot] = de;
t[tot++] = u;
}
return;
} void cdep() {
for(int j = ; ( << j) < tot; j++) {
for(int i = ; i + ( << j) < tot; i++) {
if(j == )
dp[i][j] = i;
else {
if(dep[dp[i][j - ]] < dep[dp[i + ( << (j - ))][j - ]])
dp[i][j] = dp[i][j - ];
else
dp[i][j] = dp[i + ( << (j - ))][j - ];
}
}
}
} int RMQ(int u, int v) {
int k = ;
k = log2(v - u + );
if(( << k) < v - u + ) k++;
/*int len = v - u + 1, k = 0;
k = log(len * 1.0)/log(2.0);*/
if(dep[dp[u][k]] < dep[dp[v - ( << k) + ][k]])
return t[dp[u][k]];
else
return t[dp[v - ( << k) + ][k]];
} int lca(int u, int v) {
if(pos[u] < pos[v])
return RMQ(pos[u], pos[v]);
else
return RMQ(pos[v], pos[u]);
} int main()
{
int T;
scanf("%d", &T);
while(T--) {
scanf("%d%d", &n, &m);
init();
for(int i = ; i < n; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
add(u, v, w);
add(v, u, w);
}
dfs(, );
cdep(); int u, v;
while(m--) {
scanf("%d%d", &u, &v);
printf("%d\n", dis[u] + dis[v] - * dis[lca(u, v)]);
}
}
return ;
}

最近公共祖先(least common ancestors,LCA)的更多相关文章

  1. 最近公共祖先(least common ancestors algorithm)

    lca问题是最近公共祖先问题,一般是针对树结构的.现在有两种方法来解决这样的问题 1. On-line algorithm 用比较长的时间做预处理.然后对每次询问进行回答. 思路:对于一棵树中的两个节 ...

  2. 最近公共祖先 Lowest Common Ancestors

    基于深度的LCA算法:  对于两个结点u.v,它们的深度分别为depth(u).depth(v),对于其公共祖先w,深度为depth(w),u需要向上回溯depth(u)-depth(w)步,v需要d ...

  3. 最近公共祖先 Least Common Ancestors(LCA)算法 --- 与RMQ问题的转换

    [简介] LCA(T,u,v):在有根树T中,询问一个距离根最远的结点x,使得x同时为结点u.v的祖先. RMQ(A,i,j):对于线性序列A中,询问区间[i,j]上的最值.见我的博客---RMQ - ...

  4. POJ 1330 Nearest Common Ancestors(LCA模板)

    给定一棵树求任意两个节点的公共祖先 tarjan离线求LCA思想是,先把所有的查询保存起来,然后dfs一遍树的时候在判断.如果当前节点是要求的两个节点当中的一个,那么再判断另外一个是否已经访问过,如果 ...

  5. POJ.1330 Nearest Common Ancestors (LCA 倍增)

    POJ.1330 Nearest Common Ancestors (LCA 倍增) 题意分析 给出一棵树,树上有n个点(n-1)条边,n-1个父子的边的关系a-b.接下来给出xy,求出xy的lca节 ...

  6. poj 1330 Nearest Common Ancestors LCA

    题目链接:http://poj.org/problem?id=1330 A rooted tree is a well-known data structure in computer science ...

  7. pku 1330 Nearest Common Ancestors LCA离线

    pku 1330 Nearest Common Ancestors 题目链接: http://poj.org/problem?id=1330 题目大意: 给定一棵树的边关系,注意是有向边,因为这个WA ...

  8. POJ 1330 Nearest Common Ancestors LCA题解

    Nearest Common Ancestors Time Limit: 1000MS   Memory Limit: 10000K Total Submissions: 19728   Accept ...

  9. [Swift]LeetCode235. 二叉搜索树的最近公共祖先 | Lowest Common Ancestor of a Binary Search Tree

    Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BS ...

随机推荐

  1. Day03(黑客成长日记)------>元祖及列表的增减改查

    #昨日作业解析: # s = 'sadagwa'# i = 0# while i < len(s):# s1 = s[i]# print(s1)# i += 1# while使用技巧,先找递增变 ...

  2. 记录做一个类似于探探的卡片式布局的Recycleview有数据一直不显示

    使用了别人的项目 https://github.com/JerryChan123/ReSwipeCard/blob/master/README_zh.md 之前找recycleview有数据不显示的原 ...

  3. JAVA:简单添加菜单界面(swing)

    package com.le.menu; import java.awt.Color; import java.awt.Container; import java.awt.FlowLayout; i ...

  4. 初识大数据(二. Hadoop是什么)

    hadoop是一个由Apache基金会所发布的用于大规模集群上的分布式系统并行编程基础框架.目前已经是大数据领域最流行的开发架构.并且已经从HDFS.MapReduce.Hbase三大核心组件成长为一 ...

  5. Spring aop框架使用的jar包

    除了前两个jar包,后面的jar包spring框架包中都有,前两个jar包的下载地址:https://pan.baidu.com/s/1L-GLGT1c8vnwFwqLxzzZuw

  6. java(一) 基础部分

    1.11.简单讲一下java的跨平台原理 Java通过不同的系统.不同版本.不同位数的java虚拟机(jvm),来屏蔽不同的系统指令集差异而对外体统统一的接口(java API),对于我们普通的jav ...

  7. autium designer smart pdf一个小问题

    今天在使用ad的smart  pdf时遇到一个小问题  就是 使用的是AD17,生成PDF,PDF没有把芯片的引脚标号显示出来(还有其它的芯片也是一样的,但是奇怪的是:只有在原理图元器件右边的没有显示 ...

  8. ssh框架 基本整合

    struts的基本配置 <struts> <constant name="struts.devModel" value="true" /> ...

  9. SpringBoot 通过 Exploded Archives 的方式部署

    之前部署 SpringBoot 一直是用可执行 jar 的方式. java -jar codergroup-1.0.0.jar 就可以启动项目,为了能在后台运行,通常我们会使用这行命令 nohup j ...

  10. linux下postgres的安装

    软件包的下载 在浏览器中访问https://www.enterprisedb.com/download-postgresql-binaries 然后选择适合自己的版本,我选择的是linux64位下的1 ...