最近公共祖先(least common ancestors,LCA)
摘要:
本文主要介绍了解决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)的更多相关文章
- 最近公共祖先(least common ancestors algorithm)
lca问题是最近公共祖先问题,一般是针对树结构的.现在有两种方法来解决这样的问题 1. On-line algorithm 用比较长的时间做预处理.然后对每次询问进行回答. 思路:对于一棵树中的两个节 ...
- 最近公共祖先 Lowest Common Ancestors
基于深度的LCA算法: 对于两个结点u.v,它们的深度分别为depth(u).depth(v),对于其公共祖先w,深度为depth(w),u需要向上回溯depth(u)-depth(w)步,v需要d ...
- 最近公共祖先 Least Common Ancestors(LCA)算法 --- 与RMQ问题的转换
[简介] LCA(T,u,v):在有根树T中,询问一个距离根最远的结点x,使得x同时为结点u.v的祖先. RMQ(A,i,j):对于线性序列A中,询问区间[i,j]上的最值.见我的博客---RMQ - ...
- POJ 1330 Nearest Common Ancestors(LCA模板)
给定一棵树求任意两个节点的公共祖先 tarjan离线求LCA思想是,先把所有的查询保存起来,然后dfs一遍树的时候在判断.如果当前节点是要求的两个节点当中的一个,那么再判断另外一个是否已经访问过,如果 ...
- POJ.1330 Nearest Common Ancestors (LCA 倍增)
POJ.1330 Nearest Common Ancestors (LCA 倍增) 题意分析 给出一棵树,树上有n个点(n-1)条边,n-1个父子的边的关系a-b.接下来给出xy,求出xy的lca节 ...
- poj 1330 Nearest Common Ancestors LCA
题目链接:http://poj.org/problem?id=1330 A rooted tree is a well-known data structure in computer science ...
- pku 1330 Nearest Common Ancestors LCA离线
pku 1330 Nearest Common Ancestors 题目链接: http://poj.org/problem?id=1330 题目大意: 给定一棵树的边关系,注意是有向边,因为这个WA ...
- POJ 1330 Nearest Common Ancestors LCA题解
Nearest Common Ancestors Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 19728 Accept ...
- [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 ...
随机推荐
- gulp使用入门
介绍:Gulp 是基于node.js的一个前端自动化构建工具,可以使用它构建自动化工作流程(前端集成开发环境):不仅能对网站资源进行优化,而且在开发过程中很多重复的任务能够使用正确的工具自动完成,大大 ...
- 20145232韩文浩《网络对抗》逆向及BOF基础实践
实践目标 本次实践的对象是一个名为pwn1的linux可执行文件. 该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串. 该程序同时包含另一个代码片段,getShe ...
- U-Boot Makefile分析(4)具体子Makefile的分析
前面分析的都是多数Makefile要读入的文件,这次我们以drivers/mtd/nand/Makefile为例,分析一个具体的子Makefile是如何工作的. 子Makefile的结构是固定的: i ...
- ext__给grid Panel设置绑定事件
使用面板来展示详情信息 1.创建一个面板 (双击添加) 2.给该面板设置itemid的值为:detailPanel 3.给面板设置模板 4.添加下面的内容 id:{id}</br> nam ...
- MyBatis逆向工程自动生成代码
MyBatis逆向工程根据数据库表自动生成mapper.xml,entity类,mapper类,简直不要 太方便好嘛 下面贴上关键配置代码,以免以后找不到 generator.xml <?xml ...
- LVS负载均衡简单配置
一.简单介绍 LVS是 Linux Virtual Server 的简称,也就是Linux虚拟服务器.这是一个由章文嵩博士发起的一个开源项目,它的官方网站是 http://www.linuxvirtu ...
- Maven3-依赖
依赖配置 我们先来看一份简单的依赖声明: <project> ... <dependencies> <dependency> <groupId>...& ...
- [转]深入理解 GRE tunnel
我以前写过一篇介绍 tunnel 的文章,只是做了大体的介绍.里面多数 tunnel 是很容易理解的,因为它们多是一对一的,换句话说,是直接从一端到另一端.比如 IPv6 over IPv4 的 tu ...
- 【Java】利用注解和反射实现一个"低配版"的依赖注入
在Spring中,我们可以通过 @Autowired注解的方式为一个方法中注入参数,那么这种方法背后到底发生了什么呢,这篇文章将讲述如何用Java的注解和反射实现一个“低配版”的依赖注入. 下面是我们 ...
- RichText 富文本开源项目总结
在Android开发中,我们不免会遇到富文本的编辑和展示的需求,以下是本人之前star的富文本编辑器的开源项目,供大家参考: 一.RichEditor 开源项目地址:https://github.co ...