Introduction
在一棵树中查找一对结点的最近公共祖先(LCA)的问题在20世纪末期已经被仔细的研究过了,并且它现
Tarjan是首先深入研究这个问题的人,他们得出:在对输入树LCA进行线性处理后,查询可以在常数时间内得到答案。他们的工作已经得到了广泛的延伸,这篇教程将展示一些有趣的方法,而它们还也可以用在其他的问题上。
让我们考虑一个不太抽象的LCA的例子:生命树。地球上当前的居住者是由其他物种进化而来已经是一
Range Minimum
Query(RMQ)被用在数组中用来查找两个指定索引中具有最小值的元素的位置。我们后
尽管如此, RMQ并不是仅仅和LCA一起用的。当他们在和后缀数组(一个新的数据结构,它支持和后缀
在这篇教程中,我们将首先讨论RMQ。我们将给出解决这个问题的多种方法--有一些速度比较慢但是容
Notations
假设一个算法预处理时间为
g(n)>。我们将用RMQA(i,
j)来表示数组中索引i和j之间最小值的位置。
v)表示。
Range Minimum
Query(RMQ)
给定数组A[0, N-1]找出给定的两个索引间的最小值的位置。
Trivial algorithms for
RMQ
对每一对索引(i, j),将RMQA(i,
j)存储在M[0, N-1][0,
N-1]表中。普通的计算将得到一个<O(N3),
O(1)>
O(1)>。预处理的函数和下面差不多:
}
这个普通的算法相当的慢并且使用O(N2)的空间,对于大数据它是无法工作的。
An <O(N), O(sqrt(N))> solution
一个比较有趣的点子是把向量分割成sqrt(N)大小的段。我们将在M[0,sqrt(N)-1]为每一个段保存最 小值的位置。
M可以很容易的在O(N)时间内预处理。下面是一个例子:
现在让我们看看怎样计算RMQA(i,
j)。想法是遍历所有在区间中的sqrt(N)段的最小值,并且和区间相
A[M[1]], A[6]
和A[7],并且获得最小值的位置。可以很容易的看出这个算法每一次查询不会超过3 *
sqrt(N)次操作。
这个方法最大的有点是能够快速的编码(对于TopCoder类型的比赛),并且你可以把它改成问题的动
Sparse Table (ST)
algorithm
一个更好的方法预处理RMQ 是对2k
的长度的子数组进行动态规划。我们将使用数组M[0, N-1][0,
logN]
开始,长度为
为了计算M[i][j]我们必须找到前半段区间和后半段区间的最小值。很明显小的片段有这2j
- 1 长度,因此递归如下:
预处理的函数如下:
一旦我们预处理了这些值,让我们看看怎样使用它们去计算RMQA(i,
j)。思路是选择两个能够完全覆盖区间[i..j]的块并且找到它们之间的最小值。设k =
[log(j - i + 1)].。为了计算RMQA(i,
j) 我们可以使用下面的公式
So, the overall complexity of the algorithm is
<O(N logN),
O(1)>。
Segment
trees
为了解决RMQ问题我们也可以使用线段树。线段树是一个类似堆的数据结构,可以在基于区间数组上
j]区间:
- 第一个结点将保存区间[i,
j]区间的信息 - 如果i<j
左右的孩子结点将保存区间[i, (i+j)/2]和[(i+j)/2+1,
j] 的信息
注意具有N个区间元素的线段树的高度为[logN] +
1。下面是区间[0,9]的线段树:
线段树和堆具有相同的结构,因此我们定义x是一个非叶结点,那么左孩子结点为2*x,而右孩子结点为2*x+1。
2 * 2[logN] +
1],这里M[i]保存结点i区间最小值
if
M[node]
//compute
}
上面的函数映射出了这棵树建造的方式。当计算一些区间的最小值位置时,我们应当首先查看子结点
0和e
j]中的最小值的位置时,我们可以使用下一个简单的函数:
int
}
你应该使用node = 1, b = 0和e =
N - 1来调用这个函数,因为分配给第一个结点的区间是[0,
N-1]。
可以很容易的看出任何查询都可以在O(log
N)内完成。注意当我们碰到完整的in/out区间时我们停止
O(logN)>的算法。线段树非常强大,不仅仅是因为它能够用在RMQ上,
还因为它是一个非常灵活的数据结构,它能够解决动态版本的RMQ问题和大量的区间搜索问题。
Lowest Common Ancestor
(LCA)
给定一棵树T和两个节点u和v,找出u和v的离根节点最远的公共祖先。下面是一个例子(这篇教程中
An <O(N),
Another easy solution in 预处理的函数如下: 这个过程将花费O(N logN) Reduction from LCA to E[1, 在,我们需要找到这些结点中的最低层。为了达到这个目的,我们可以使用RMQ。因此 注意L中连续的元素相差为1。 From RMQ to 现在我们需要做的仅仅是用线性时间计算C(A)。这个可以使用栈来实现。初始栈为空。然后我们在栈中插入A的元素。在第i步,A[i]将会紧挨着栈中比A[i]小或者相等的元素插入,并且所有较大的元素将会被移除。在插入结束之前栈中A[i]位置前的元素将成为i的左儿子,A[i]将会成为它之后一个较小元素的右儿子。在每一步中,栈中的第一个元素总是笛卡尔树的根。 如果使用栈来保存元素的索引而不是值,我们可以很轻松的建立树。下面是上述例子中每一步栈的状态: An<O(N),
O(sqrt(N))>
solution
将输入分成同等大小的部分来解决RMQ问题是一个很有趣的方法。这个方法对LCA问题同样适用。大致
现在,对于每一个结点,我们应该知道每一个段的在上一层中的祖先。我们将预处理这些值,并将他
MAXN]中。下面是对于样例中的树的P数组内容(为了简化,对于在第一个段中的所有结点i,P[i]=1):
注意对于每一个段中的上面一部分,P[i]=T[i]。我们可以使用深度优先搜索对P进行预处理(T[i]是树
int
if
P[node]
else
P[node]
dfs(k,
}
//as
x
while
}
* sqrt(H)次操作。通过使用这个方法,我们得到了<O(N),
O(sqrt(H))>的算法,
O(sqrt(N))>。这个算法的主要好处是易于编码(Division1中的程序员应该在15分钟内完成这段代码)。
<O(N logN,
O(logN)>
如果我们对这个需要一个更快的解决方法,我们可以使用动态规划。首先我们构建一张表P[1,N]
for
P[i][j]
}
的时间和空间。现在让我们看看如何查询。用L[i]来表示节点i在树中所处的层数。可以看到,如果p和q在树中的同一层中,我们可以使用一个类二分查找的方法进行搜索。因此,对于2的j次方(界于log[L[p]和0之间,降序),如果P[p][j]
!= P[q][j] ,那么可以知道LCA(p,
q)必然在更高的层中,因此我们继续搜索LCA(p = P[p][j], q =
P[q][j])。最后,p和q都有了相同的祖先,因此返回T[p]。让我们看看如果L[p]
!= L[q]的情况。 不妨假设L[p] <
L[q]。我们可以使用类似的二分搜索方法来查找与q在同一层次的p的祖先,然后我们在用下面所描述的方法计算LCA。整个函数如下:
if
tmp
for
}
现在,我们可以看到这个函数最多需要执行2*log(H)次的操作,这里的H是树的高度。在最坏情况下H=N,
RMQ
现在,让我们看看怎样用RMQ来计算LCA查询。事实上,我们可以在线性时间里将LCA问题规约到RMQ问
注意LCAT(u,
v)是在对T进行dfs过程当中在访问u和v之间离根结点最近的点。因此我们可以考虑树的
2*N-1]
对T进行欧拉环游过程中所有访问到的结点;E[i]是在环游过程中第i个访问的结点
L[1,2*N-1] -
H[1, N] - H[i]
是E中结点i第一次出现的下标(任何出现i的地方都行,当然选第一个不会错)
假定H[u]<H[v](否则你要交换u和v)。可以很容易的看到u和v第一次出现的结点是E[H[u]..H[v]]。现
v) = E[RMQL(H[u], H[v])]
LCA
我们已经看到了LCA问题可以在线性时间规约到RMQ问题。现在让我们来看看怎样把RMQ问题规约到LCA。这个意味着我们实际上可以把一般的RMQ问题规约到带约束的RMQ问题(这里相邻的元素相差1)。为了达到这个目的,我们需要使用笛卡尔树。
对于数组A[0,N-1]的笛卡尔树C(A)是一个二叉树,根节点是A的最小元素,假设i为A数组中最小元素的位置。当i>0时,这个笛卡尔树的左子结点是A[0,i-1]构成的笛卡尔树,其他情况没有左子结点。右结点类似的用A[i+1,N-1]定义。注意对于具有相同元素的数组A,笛卡尔树并不唯一。在这篇教程中,将会使用第一次出现的最小值,因此笛卡尔树看作唯一。可以很容易的看到RMQA(i,
j) = LCAC(i, j)。
下面是一个例子:
Step
Stack
Modifications
made in the tree
0
0
0 is the only node in the
tree.
1
0 1
1 is added at the end of the
stack. Now, 1 is the right son of 0.
2
0 2
2 is added next to 0, and 1 is removed
(A[2] < A[1]). Now, 2 is the right son of 0 and the
left son of 2 is 1.
3
3
A[3] is the smallest element
in the vector so far, so all elements in the stack will be removed
and 3 will become the root of the tree. The left child of 3 is
0.
4
3 4
4 is added next to 3, and the
right son of 3 is 4.
5
3 4 5
5 is added next to 4, and the
right son of 4 is 5.
6
3 4 5 6
6 is added next to 5, and the
right son of 5 is 6.
7
3 4 5 6 7
7 is added next to 6, and the
right son of 6 is 7.
8
3 8
8 is added next to 3, and all
greater elements are removed. 8 is now the right child of 3 and the
left child of 8 is 4.
9
3 8 9
9 is added next to 8, and the
right son of 8 is 9.
注意A中的每个元素最多被增加一次和最多被移除一次。因此上述算法的时间复杂度为O(N)。下面是树的处理函数:
//compute
//we
//the
}
O(1)> algorithm for the restricted
RMQ
现在我们知道了一般的RMQ问题可以使用LCA归约成约束版本。这里,数组中相邻的元素差值为1.我们可以使用一个更快的<O(N),
O(1)> 的算
为了解决这个问题的约束版本,我们需要将A分成l = [(log N) /
2]的大小块.让A'[i]为A中第i块的最小值,B[i]为A中最小块值的位置。A和B的
* 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。
现在,对于询问
我们有两种情况:
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&problem=155&refer=