给定多个二叉树的更加本地化,​​高效的最低共同祖先算法?

时间:2010-06-12 00:49:40

标签: algorithm language-agnostic binary-tree

我有多个二叉树存储为数组。在每个槽中都是nil(或null;选择你的语言)或存储两个数字的固定元组:两个“children”的索引。没有节点只有一个孩子 - 它不是一个或两个。

将每个插槽视为仅存储指向其子节点的二进制节点,并且没有固有的值。

采用这种二叉树系统:

    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

我能找到的主要方法是使用Euler跟踪的方法,该方法将给定的树(添加节点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

不幸的是,我确实认为找到一棵树的Euler痕迹可能数千级别的深度有点机器负担......并且因为我的树在整个过程中不断变化编程过程中,每次我想找到LCA时,我都必须重新计算Euler跟踪并且每次都将它保存在内存中。

考虑到我正在使用的框架,是否有更高效的内存方式?一个可能向上迭代?我能想到的一种方法是“计算”两个节点的生成/深度,并攀爬最低节点,直到它与最高节点的深度相匹配,然后增加两个节点直到找到相似的人。

但这涉及从3025级别上升到0级,两次,以计算代数,并首先使用非常低效的攀爬算法,以及然后重新爬上去。

还有其他更好的方法吗?


澄清

在建立这个系统的过程中,每个孩子的数字都会超过他们的父母

保证如果n在X代中,则生成(X-1)中没有大于n的节点。例如:

        0
       / \
      /   \
     /     \
    1       2        6
   / \     / \      / \
  2   3   9  10    7   8
     / \              / \
    4   5            11 12

是一个有效的树系统。

此外,树木构建方式的工件是同一父母的两个直系子女将始终连续编号。

4 个答案:

答案 0 :(得分:1)

在您的示例中,节点是否按顺序排列,其中子节点的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)

这是对的吗?

答案 1 :(得分:1)

也许这会有所帮助:Dynamic LCA Queries on Trees

摘要:

  

Richard Cole,Ramesh Hariharan

     

我们将展示如何维护数据   允许的树木结构   全部进行以下操作   最坏情况下的恒定时间。 1.插入   叶子和内部节点。 2。   删除叶子。 3.删除   只有一个孩子的内部节点。 4。   确定最不共同的祖先   任何两个节点。

     

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

答案 2 :(得分: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中的一个,则可以立即退出,因为您知道没有共同的祖先。此外,如果您查看一个子树并且它具有共同的祖先,您可以忽略另一个子树(我使用延迟评估免费获得的子树)。

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

答案 3 :(得分: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))))