如何在多个二叉树中实现更局部化、更高效的最近公共祖先算法?

3
我有多个二叉树存储在一个数组中。每个位置要么是nil(或null,根据您的语言选择),要么是一个固定的元组,存储两个数字:两个“子节点”的索引。没有节点只有一个子节点——它可以是没有或者有两个。
将每个位置看作只存储指向其子节点的二进制节点,而没有固有值。
以这个二叉树系统为例:
    0       1
   / \     / \
  2   3   4   5
 / \         / \
6   7       8   9
   / \
 10   11
相关联的数组将是:
   0       1       2      3     4      5      6      7        8     9     10    11
[ [2,3] , [4,5] , [6,7] , nil , nil , [8,9] , nil , [10,11] , nil , nil , nil , nil ]

我已经编写了查找节点直接父节点的简单函数(只需从前面搜索,直到找到包含子节点的节点)。

此外,假设在相关时间内,所有树都在几个到几千层之间。

我想要找到一个函数。

P(m,n)

要找到m和n的最近公共祖先,更正式地说,LCA被定义为具有m和n作为后代(孩子、孙子等)的“最低”或最深节点。如果没有,则nil是有效的返回值。

以下是一些例子,给定我们的树:

P( 6,11)   # => 2
P( 3,10)   # => 0
P( 8, 6)   # => nil
P( 2,11)   # => 2

我找到的主要方法是使用欧拉遍历,它将给定的树(将节点A添加为0和1的不可见父节点,其“值”为-1)转换为:

A-0-2-6-2-7-10-7-11-7-2-0-3-0-A-1-4-1-5-8-5-9-5-1-A

从中,只需要找到在给定的m和n之间具有最低数字的节点;例如,要查找P(6,11),请在跟踪记录中查找6和11。它们之间的最低数字是2,这就是您的答案。如果A(-1)位于它们之间,则返回nil。

 -- Calculating P(6,11) --

A-0-2-6-2-7-10-7-11-7-2-0-3-0-A-1-4-1-5-8-5-9-5-1-A
      ^ ^        ^
      | |        |
      m lowest   n

很遗憾,我认为找到一个可能有数千层深度的树的欧拉迹会对机器造成一定负担...而且因为我的树在编程过程中不断改变,每次想要找到LCA时,我都必须重新计算欧拉迹并将其保存在内存中。
考虑到我使用的框架,是否有更节省内存的方法? 也许可以向上迭代? 我能想到的一种方法是“计算”两个节点的生成/深度,并爬升最低节点,直到它与最高深度相匹配,并逐步增加两个节点,直到它们找到相似之处。
但这将涉及从级别(例如3025)向上爬升两次以计算代数,并且首先使用非常低效的爬升算法,然后重新爬升。
还有其他更好的方法吗?
澄清:
按照此系统构建的方式,“每个子项的编号都大于其父项”。
这并不保证如果n在第X代,则没有在第(X-1)代中比n大的节点。例如:
0 / \ / \ / \ 1 2 6 / \ / \ / \ 2 3 9 10 7 8 / \ / \ 4 5 11 12
是一个有效的树系统。
此外,树的构建方式的一个特点是同一父项的两个直接子项将始终按顺序编号。
4个回答

1

您的示例中,节点是否按顺序排列,其中子节点的ID大于父节点的ID?如果是这样,您可以尝试类似于归并排序的方法来查找它们。例如,6和11的父树如下:

6  -> 2 -> 0
11 -> 7 -> 2 -> 0

所以也许算法会是:

left = left_start
right = right_start

while left > 0 and right > 0
    if left = right
        return left
    else if left > right
        left = parent(left)
    else
        right = parent(right)

这将运行为:

left    right
----    -----
 6        11    (right -> 7)
 6        7     (right -> 2)
 6        2     (left -> 2)
 2        2     (return 2)

这个正确吗?


这个非常好用,而且伪线性地与两个节点之间的距离相关,而不是与树的大小相关。我相信这相当优雅。有人刚刚发布了这个解决方案,但你先做到了。谢谢。我简直无法相信这是多么简单明了。 - Justin L.

1

也许这可以帮到你:树上动态LCA查询

摘要:

Richard Cole, Ramesh Hariharan

我们展示了如何在树上维护一个数据结构,使得以下操作都可以在最坏情况下常数时间内完成。1. 插入叶子和内部节点。2. 删除叶子。3. 删除只有一个孩子的内部节点。4. 确定任意两个节点的最近公共祖先。

会议:离散算法研讨会 - SODA 1999


0

我已经用Haskell解决了你的问题。假设你知道森林的根,该解决方案需要与森林大小成线性时间,并且需要恒定的额外内存。你可以在http://pastebin.com/ha4gqU0n找到完整的代码。

该解决方案是递归的,主要思想是您可以在子树上调用一个函数,该函数返回以下四个结果之一:

  • 子树既不包含m也不包含n
  • 子树包含m但不包含n
  • 子树包含n但不包含m
  • 子树同时包含mn,并且它们最近公共祖先的索引为k

没有子节点的节点可能包含mn或两者都不包含,您只需返回适当的结果即可。

如果具有索引k的节点有两个子节点,则将结果组合如下:

join :: Int -> Result -> Result -> Result
join _ (HasBoth k) _ = HasBoth k
join _ _ (HasBoth k) = HasBoth k
join _ HasNeither r = r
join _ r HasNeither = r
join k HasLeft HasRight = HasBoth k
join k HasRight HasLeft = HasBoth k

计算出结果后,您必须检查节点本身的索引k,如果k等于mn,则将扩展join操作的结果。

我的代码使用代数数据类型,但我已经小心地假设您只需要以下操作:

  • 获取节点的索引
  • 查找节点是否为空,如果不为空,则查找其两个子节点

由于您的问题与语言无关,因此我希望您能够适应我的解决方案。

您可以添加各种性能调整。例如,如果您找到具有仅两个节点mn之一的根节点,则可以立即退出,因为您知道没有公共祖先。另外,如果您查看一个子树并且它具有公共祖先,则可以忽略其他子树(我使用惰性评估免费获得该功能)。

您的问题主要是关于如何节省内存。如果线性时间解决方案太慢,则可能需要辅助数据结构。空间换时间是我们存在的恶魔。


尽管选择了另一个答案,但我仍然觉得这个答案非常有趣;当我将其应用于更通用的数据结构时,我会牢记这一点。 - Justin L.

0

我认为你可以通过反向循环数组,始终用其父元素替换两个索引中较高的那一个,直到它们相等或找不到更多的父元素为止:

(defun lowest-common-ancestor (array node-index-1 node-index-2)
  (cond ((or (null node-index-1)
             (null node-index-2))
         nil)
        ((= node-index-1 node-index-2)
         node-index-1)
        ((< node-index-1 node-index-2)
         (lowest-common-ancestor array
                                 node-index-1
                                 (find-parent array node-index-2)))
        (t
         (lowest-common-ancestor array
                                 (find-parent array node-index-1)
                                 node-index-2))))

是的,我忽略了另一个(而且它还遗漏了一个基本情况)。 - Svante

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接