题意:

给定一棵有根树T,给出若干个查询lca(u, v)(通常查询数量较大),每次求树T中两个顶点u和v的最近公共祖先,即找一个节点,同时是u和v的祖先,并且深度尽可能大(尽可能远离树根)。通常有以下几种算法:

  • 在线算法,每次读入一个查询,处理这个查询,给出答案。
  • 离线算法,一次性读入所有查询,统一进行处理,给出所有答案。

在线:

倍增(基于二分搜索):

基本思想就是让u和v同时走到同一高度,然后再一起一步步往上走。

将父亲结点的父亲结点利用起来,依次计算,便可以得到从当前结点向上走2k步所到达的顶点,这样便有了k以内的点的所有信息,进行二分查找答案即可~

预处理时间复杂度O(nlogn),查询时间复杂度O(logn)。

关键代码:

首先预处理阶段

//DFS预处理所有结点的深度和父节点
void dfs(int v, int p, int d)
{
pa[0][v] = p;
dept[v] = d;
for(int i = head[v]; i != -1; i = edge[i].next){
int u = edge[i].to;
if(u == p) continue;
dfs(u, v, d + 1);
}
}
void init()
{
dfs(root, -1, 0);
//预处理祖先,向上走2^i所到的结点
for(int i = 0; i < maxm - 1; i++){
for(int j = 1; j <= V; j++){
if(pa[i][j] < 0) pa[i + 1][j] = -1;
else pa[i + 1][j] = pa[i][pa[i][j]];
}
}
}

计算u和v的lca

int lca(int u, int v)
{
//让u和v 向上走到同一高度
if(dept[u] > dept[v]) swap(u, v);
for(int i = 0; i < maxm; i++){
if((dept[v] - dept[u]) >>i &1)
v = pa[i][v];
}
if(u == v) return u; //二分搜索计算lca
for(int i = maxm - 1; i >= 0; i--){
if(pa[i][u] != pa[i][v]){
u = pa[i][u];
v = pa[i][v];
}
}
return pa[0][u];
}

基于RMQ的算法:

初始化过程O(nlogn),查询过程O(1)。

有根树处理的一个技巧就是将树转化为从根DFS标号后得到的序列。而这种算法的基本思想就是将树看成一个无向图,u和v的公共祖先一定在u和v之间的最短路上。

算法分三步:

  • 首先DFS对结点从跟开始标号,用数组vs保存访问顺序,height记录深度。每条边恰好经过两次,因此一共记录了2n−1个结点
  • 计算对于每个顶点首次出现子的下标,保存在id中。
  • 获取LCA(u,v):LCA(u,v)=vs[id[u]≤i≤id[v]中深度最小的i]

预处理:

void dfs(int u, int pre, int dept)
{
vs[cnt] = u;
height[cnt] = dept;
id[u] = cnt++;
for(int i = head[u]; i != -1; i = edge[i].next){
dfs(edge[i].to, u, dept + 1);
vs[cnt] = u;
height[cnt++] = dept;
}
}
void init()
{
cnt = 1; //vs数组下标从1开始
dfs(root, root, 0);
st.init(2 * V - 1);
}

而最后一步属于RMQ(Range Minimum/Maximum Query),即区间最值查询问题,我们可以用线段树解决,也可以使用ST(Sparse Table)算法,在O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询。

预处理使用动态规划,设dp[i][j]是从i开始的2j个数中的深度最小的值的下标。则有状态转移方程:

if(height[dp[i][j - 1]] < height[dp[i + (1<<(j - 1))][j - 1]])
dp[i][j] = dp[i][j - 1];
else
dp[i][j] = dp[i + (1<<(j - 1))][j - 1];

初始化:

 for(int i = 1; i <= n; i++)  dp[i][0] = i;

查询:

int query(int a, int b)
{
if(a > b) swap(a, b);
int k = lg[b - a + 1] ;
if(height[dp[a][k]] <= height[dp[b - (1<<k) + 1][k]])
return dp[a][k];
else
return dp[b - (1<<k) + 1][k];
}

离线Tarjan算法:

讲的很好

Tarjan算法是离线算法,基于后序DFS和并查集。

算法从根节点root开始搜索,每次递归搜索所有的子树,然后处理跟当前根节点相关的所有查询。

算法用集合表示一类节点,这些节点跟集合外的点的LCA都一样,并把这个LCA设为这个集合的祖先。当搜索到节点x时,创建一个由x本身组成的集合,这个集合的祖先为x自己。然后递归搜索x的所有儿子节点。

所有子树处理完毕之后,处理当前根节点x相关的查询。遍历x的所有查询,如果查询的另一个节点v已经访问过了,那么x和v的LCA即为v所在集合的祖先。

建树可以用数组写链表也可以用vector保存,而查询可以用矩阵保存,这样可以减少重复,也可以用链表的形式,将一个结点的查询连在一起。

Tarjan关键代码:

void LCA(int u)
{
ance[u] = u;
vis[u] = 1;
for(int i = head[u]; i != -1; i = edge[i].next){
int v = edge[i].to;
if(vis[v]) continue;
LCA(v);//访问子树
unite(u, v);//子树与当前结点合并
ance[_find(u)] = u;//祖先为u
}
for(int i = h[u]; i != -1; i = query[i].next){
int v = query[i].q;
if(vis[v]) ans[query[i].index] = ance[_find(v)];
}
}

//感觉这个ance数组完全可以不用~~

最近公共祖先(Least Common Ancestors)的更多相关文章

  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. [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 ...

  5. [Swift]LeetCode236. 二叉树的最近公共祖先 | Lowest Common Ancestor of a Binary Tree

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

  6. 最近公共祖先 · Lowest Common Ancestor

    [抄题]: Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree. “Th ...

  7. 最近公共祖先 LCA 倍增算法

          树上倍增求LCA LCA指的是最近公共祖先(Least Common Ancestors),如下图所示: 4和5的LCA就是2 那怎么求呢?最粗暴的方法就是先dfs一次,处理出每个点的深度 ...

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

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

  9. [总结]最近公共祖先(倍增求LCA)

    目录 一.定义 二.LCA的实现流程 1. 预处理 2. 计算LCA 三.例题 例1:P3379 [模板]最近公共祖先(LCA) 四.树上差分 1. 边差分 2. 点差分 3. 例题 一.定义 给定一 ...

  10. 编程算法 - 二叉树的最低公共祖先 代码(C)

    二叉树的最低公共祖先 代码(C) 本文地址: http://blog.csdn.net/caroline_wendy 二叉树的最低公共祖先(lowest common ancestor), 首先先序遍 ...

随机推荐

  1. JVM内存区域参数配置

    转自:https://www.jianshu.com/p/5946c0a414b5 需要提前了解的知识点: JVM内存模型 JVM垃圾回收算法 下图是JVM内存区域划分的逻辑图   JVM内存区域逻辑 ...

  2. configure: error: The LBL Packet Capture Library, libpcap, was not found!

    configure: error:  The LBL Packet Capture Library, libpcap, was not found! yum install libpcap*

  3. sql语句分为三类(DML,DDL,DCL)-介绍

    本文知识来源自:<Oracle专家高级编程> 分享作者:Vashon 时间:20150415 DDL is Data Definition Language statements. Som ...

  4. laravel学习笔记(一)

    laravel 简述 优点:优雅.简洁.工程化(项目架构,协同开发) 版本:2011 June 1.0 ,LTS(long time) ,laravel 5.4 功能:队列.搜索.数据库搜索.定时脚本 ...

  5. react基础语法(四) state学习

    <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title> ...

  6. 入门Promise的用法

    new Promise(function(resolve,reject){ resolve(); //数据处理完成 reject(); //数据处理出错 }).then(function A(){ / ...

  7. 洛谷 P1618 三连击(升级版)

    题目描述 将1,2,…,9共9个数分成三组,分别组成三个三位数,且使这三个三位数的比例是A:B:C,试求出所有满足条件的三个三位数,若无解,输出“No!!!”. //感谢黄小U饮品完善题意 输入输出格 ...

  8. C++_pthread read-write lock_读写锁_visual studio 2015下配置

    pthread下载地址:https://sourceware.org/pthreads-win32/ 1. 项目->属性->VC++目录 包含目录里添加:pthread所在路径\pthre ...

  9. Android(java)学习笔记158:多线程断点下载的原理(JavaSE实现)

    1. 为什么需要多线程下载?     服务器的资源有限,同时的平均地分配给每个客户端.开启的线程越多抢占的服务的资源就越多,下载的速度就越块. 2. 下载速度的限制条件? (1)你的电脑手机宽带的带宽 ...

  10. Ubuntu下编辑并编译运行c++程序

    一.使用vim编辑c++代码: vim hello.cpp 输入如下代码: #include <iostream> using namespace std; int main() { co ...