您的位置:首页 > 产品设计 > UI/UE

Range Minimum Query and Lowest Common Ancestor[翻译]

2013-05-31 16:04 423 查看
Range Minimum Query and Lowest Common Ancestor

【原文见 http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=lowestCommonAncestor

作者:
By danielp

Topcoder Member

翻译: 农夫三拳@seu

Introduction

Notations

Range
Minimum Query (RMQ)

Trivial
algorithms for RMQ

A
<O(N), O(sqrt(N))> solution

Sparse
Table (ST) algorithm

Segment
Trees

Lowest
Common Ancestor (LCA)

A
<O(N), O(sqrt(N))> solution

Another
easy solution in <O(N logN, O(logN)>

Reduction
from LCA to RMQ

From
RMQ to LCA

An
<O(N), O(1)> algorithm for the restricted RMQ

Conclusion

Introduction

在一棵树中查找一对结点的最近公共祖先(LCA)的问题在20世纪末期已经被仔细的研究过了,并且它现在已经成为算法中图论的基本算法了。这个问题之所以有趣并不是因为处理它的算法很有技巧,而是因为它在字符串处理和生物学计算中的广泛应用,例如,当LCA和后缀树或者其他树形结构在一起使用时。Harel
and Tarjan是首先深入研究这个问题的人,他们得出:在对输入树LCA进行线性处理后,查询可以在常数时间内得到答案。他们的工作已经得到了广泛的延伸,这篇教程将展示一些有趣的方法,而它们还也可以用在其他的问题上。

让我们考虑一个不太抽象的LCA的例子:生命树。地球上当前的居住者是由其他物种进化而来已经是一个不争的事实。这种进化结构可以表示成一棵树,其中节点表示物种,而它的孩子结点表示从该物种直接进化得到的物种。现在通过在树中查找一些结点的LCA把具有相似特征的物种划分成组,我们可以找出两个物种共同的祖先,并且我们可以知道它们所拥有的相似特征是来自于那个祖先。

Range Minimum Query(RMQ)被用在数组中用来查找两个指定索引中具有最小值的元素的位置。我们后面将会看到LCA问题可以归约成一个带限制的RMQ问题,其中相邻的数组元素相差1。

尽管如此, RMQ并不是仅仅和LCA一起用的。当他们在和后缀数组(一个新的数据结构,它支持和后缀树同等效率的字符串查询,但是使用更少的内存且编码很简单)一起使用时,在字符串处理中扮演着相当重要的角色。

在这篇教程中,我们将首先讨论RMQ。我们将给出解决这个问题的多种方法--有一些速度比较慢但是容易编码,而其他的则更快。在第二部分我们将讨论LCA和RMQ之间的关系。首先我们先回顾一下不使用RMQ来解决LCA的两个简单方法;然后我们将指出RMQ和LCA问题其实是等价的;并且,最后,我们将看到RMQ问题怎样规约成它的限制版本,并且对于这个特殊情况给出一个最快的算法。

Notations

假设一个算法预处理时间为 f(n),查询时间为g(n)。这个算法复杂度的标记为<f(n), g(n)>。我们将用RMQA(i, j)来表示数组中索引i和j之间最小值的位置。 uv的离树T根结点最远的公共祖先用LCAT(u,
v)
表示。

Range Minimum Query(RMQ)

给定数组A[0, N-1]找出给定的两个索引间的最小值的位置。

void process1(int M[MAXN][MAXN], int A[MAXN], int N)

void process2(int M[MAXN][LOGMAXN], int A[MAXN], int N)

[/code]

一旦我们预处理了这些值,让我们看看怎样使用它们去计算RMQA(i, j)。思路是选择两个能够完全覆盖区间[i..j]的块并且找到它们之间的最小值。设k = [log(j - i + 1)].。为了计算RMQA(i,
j)
我们可以使用下面的公式

void initialize(int node, int b, int e, int M[MAXIND], int A[MAXN], int N)

[/code]

上面的函数映射出了这棵树建造的方式。当计算一些区间的最小值位置时,我们应当首先查看子结点的值。调用函数的时候使用 node = 1, b = 0e = N-1

现在我们可以开始进行查询了。如果我们想要查找区间[i,
j]
中的最小值的位置时,我们可以使用下一个简单的函数:

int query(int node, int b, int e, int M[MAXIND], int A[MAXN], int i, int j)

你应该使用node = 1, b = 0e = N - 1来调用这个函数,因为分配给第一个结点的区间是[0, N-1]

可以很容易的看出任何查询都可以在O(log N)内完成。注意当我们碰到完整的in/out区间时我们停止了,因此数中的路径最多分裂一次。用线段树我们获得了<O(N), O(logN)>的算法。线段树非常强大,不仅仅是因为它能够用在RMQ上,

还因为它是一个非常灵活的数据结构,它能够解决动态版本的RMQ问题和大量的区间搜索问题。

Lowest Common Ancestor (LCA)

给定一棵树T和两个节点uv,找出uv的离根节点最远的公共祖先。下面是一个例子(这篇教程中所有的例子中树的根结点均为1):

[align=center][/align]
[/code]

现在,我们可以很容易的进行查询了。为了找到LCA(x,y),我们首先找出它所在的段,然后再用普通的方法计算它。下面是代码:

int LCA(int T[MAXN], int P[MAXN], int L[MAXN], int x, int y)


这个函数最多执行2 * sqrt(H)次操作。通过使用这个方法,我们得到了<O(N), O(sqrt(H))>的算法,这里H指的是树的高度。在最坏的情况下H=N,因此总的复杂度为<O(N), O(sqrt(N))>。这个算法的主要好处是易于编码(Division1中的程序员应该在15分钟内完成这段代码)。

Another easy solution in <O(N logN, O(logN)>

如果我们对这个需要一个更快的解决方法,我们可以使用动态规划。首先我们构建一张表P[1,N][1,logN],这里P[i][j]指的是结点i的第2j个祖先。为了计算这个值,我们可以使用下面的递归:

void process3(int N, int T[MAXN], int P[MAXN][LOGMAXN])

这个过程将花费O(N logN) 的时间和空间。现在让我们看看如何查询。用L[i]来表示节点i在树中所处的层数。可以看到,如果pq在树中的同一层中,我们可以使用一个类二分查找的方法进行搜索。因此,对于2j次方(界于log[L[p]0之间,降序),如果P[p][j]
!= P[q][j]
,那么可以知道LCA(p, q)必然在更高的层中,因此我们继续搜索LCA(p = P[p][j], q = P[q][j])。最后,pq都有了相同的祖先,因此返回T[p]。让我们看看如果L[p]
!= L[q]
的情况。 不妨假设L[p] < L[q]。我们可以使用类似的二分搜索方法来查找与q在同一层次的p的祖先,然后我们在用下面所描述的方法计算LCA。整个函数如下:

int query(int N, int P[MAXN][LOGMAXN], int T[MAXN],

int L[MAXN], int p, int q)


现在,我们可以看到这个函数最多需要执行2*log(H)次的操作,这里的H是树的高度。在最坏情况下H=N,因此总的时间复杂度为<O(NlogN),O(logN)>。这个方案非常易编码,并且它比前一个要快。

Reduction from LCA to RMQ

现在,让我们看看怎样用RMQ来计算LCA查询。事实上,我们可以在线性时间里将LCA问题规约到RMQ问题,因此每一个解决RMQ的问题都可以解决LCA问题。让我们通过例子来说明怎么规约的:

void computeTree(int A[MAXN], int N, int T[MAXN])

[/code]
An<O(N), O(1)> algorithm for the restricted RMQ

现在我们知道了一般的RMQ问题可以使用LCA归约成约束版本。这里,数组中相邻的元素差值为1.我们可以使用一个更快的<O(N), O(1)> 的算法。下面我们将在数组A[0,N-1]上解决RMQ问题,这里|A[i]-A[i+1]|=1,i=[1,N-1]。我们将把A转换为一个二元的有着N-1个元素的数组,其中A[i]=A[i]-A[i+1]。很显然A中的元素只有可能是+1或者-1。注意原来的A[i]的值现在是A[1],A[2],...,A[i]的和加上原来的A[0]。尽管如此,下面我们根本不需要原来的值。

为了解决这个问题的约束版本,我们需要将A分成l = [(log N) / 2]的大小块.让A'[i]A中第i块的最小值,B[i]A中最小块值的位置。A和B的长度均为N/l。现在我们利用第一节中讨论的ST算法预处理A'数组。这个将花费O(N/l
* log(N/l))=O(N)
的时间和空间。经过预处理之后,我们可以在O(1)时间内在很多块上进行查询。具体的查询过程和上面说过的一样。注意每个块的长度为l=[(logN)/2],这个非常的小。同样,要注意A是一个二元数组。二元数组的总的元素的大小l满足2l=sqrt(N)。因此,对于每一个二元数组中的块l,我们需要在表P中查询每一对索引的RMQ。这个可以在O(sqrt(N)*l2)=O(N)
的时间和空间内解决。为了索引表P,可以预处理A中的每一个块并且将其存储在数组T[1,N/l]中。块的类型可以成为一个二进制数如果把-1替换成0,把+1替换成1。

现在,对于询问 RMQA(i, j) 我们有两种情况:

ij在同一个块中,因此我们使用在PT中计算的值

ij在不同的块中,因此我们计算三个值:从ii所在块的末尾的PT中的最小值,所有ij中块中的通过与处理得到的最小值以及从j所在块ij在同一个块中,因此我们使用在PT中计算的值jPT的最小值;最后我们我们只要计算三个值中最小值的位置即可。

Conclusion

RMQ和LCA是密切相关的问题,因为他们互相之间都可以规约。有许多算法可以用来解决它们,并且他们适应于一类问题。

下面是一些用来练习线段树,LCA和RMQ的题目:

SRM 310 -> Floating Median

http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.pku.edu.cn/JudgeOnline/problem?id=1986&refer=

http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.pku.edu.cn/JudgeOnline/problem?id=2374&refer=

http://www.topcoder.com/tc?module=LinkTracking&link=http://acmicpc-live-archive.uva.es/nuevoportal/data/problem.php?p=2045&refer=

http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.pku.edu.cn/JudgeOnline/problem?id=2763&refer=

http://www.topcoder.com/tc?module=LinkTracking&link=http://www.spoj.pl/problems/QTREE2/&refer=

http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.uva.es/p/v109/10938.html&refer=

http://www.topcoder.com/tc?module=LinkTracking&link=http://acm.sgu.ru/problem.php?contest=0%26problem=155&refer=

References

- "Theoretical and Practical Improvements on the RMQ-Problem,
with Applications to LCA and LCE" [PDF] by Johannes Fischer and Volker Heunn

- "The LCA Problem Revisited" [PPT] by Michael
A.Bender and Martin Farach-Colton - a very good presentation, ideal for quick learning of some LCA and RMQ aproaches

- "Faster algorithms
for finding lowest common ancestors in directed acyclic graphs" [PDF] by Artur Czumaj, Miroslav Kowaluk and Andrzej Lingas
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: