【问题标题】:O(1) algorithm to determine if node is descendant of another node in a multiway tree?O(1)算法来确定节点是否是多路树中另一个节点的后代?
【发布时间】:2021-08-11 01:15:17
【问题描述】:

想象一下下面的树:

    A
   / \
  B   C
 / \   \
D   E   F

我正在寻找一种方法来查询例如 F 是否是 A 的后代(注意:F 不需要是 A 的 直接 后代),在这个特定的情况将是真实的。只有有限数量的潜在父节点需要针对更大的潜在后代节点池进行测试。

在测试一个节点是否是潜在父池中某个节点的后代时,需要针对所有潜在父节点进行测试。

这是 a 想出的:

  • 将多路树转换为特里树,即为上述树中的每个节点分配以下前缀:

     A = 1
     B = 11
     C = 12
     D = 111
     E = 112
     F = 121
    
  • 然后,为每个可能的前缀大小保留一个位数组并添加要测试的父节点,即如果将 C 添加到潜在的父节点池中,请执行以下操作:

      1    2    3  <- Prefix length
    
    *[1]  [1]  ...
     [2] *[2]  ...
     [3]  [3]  ...
     [4]  [4]  ...
     ...  ...
    
  • 当测试一个节点是否是潜在父节点的后代时,取其 trie 前缀,在第一个“前缀数组”(见上文)中查找第一个字符,如果存在,则在第二个“前缀数组”等等,即测试 F 导致:

     F = 1    2    1
    
       *[1]  [1]  ...
        [2] *[2]  ...
        [3]  [3]  ...
        [4]  [4]  ...
        ...  ...
    

    所以是F,是C的后代。

这个测试似乎是最坏情况 O(n),其中 n = 最大前缀长度 = 最大树深度,所以它的最坏情况正好等于直接上树并比较节点的明显方法。但是,如果测试节点位于树的底部附近并且潜在的父节点位于顶部的某个位置,则此方法的性能要好得多。结合这两种算法将减轻两种最坏的情况。但是,内存开销是一个问题。

还有其他方法吗?任何指针都非常感谢!

【问题讨论】:

  • 这总是二叉树吗?

标签: algorithm tree trie descendant multiway-tree


【解决方案1】:

您的输入树总是静态的吗?如果是这样,那么您可以使用最低共同祖先算法在 O(1) 时间内使用 O(n) 时间/空间构造来回答后代问题。 LCA 查询给定两个节点,并询问哪个是树中的最低节点,其子树包含这两个节点。然后您可以使用单个 LCA 查询来回答 IsDescendent 查询,如果 LCA(A, B) == A 或 LCA(A, B) == B,则一个是另一个的后代。

Topcoder algorithm tuorial 对问题进行了彻底的讨论,并提供了不同代码复杂性/效率级别的一些解决方案。

【讨论】:

  • 优秀的教程,谢谢!学到了一些东西 :) 不幸的是,我的树不是静态的,但我认为当树的深度总是大致相同时,我认为 sqrt-parts LCA 的想法可以很容易地用于动态使用,就像我的例子一样。
【解决方案2】:

我不知道这是否适合您的问题,但是将层次结构存储在数据库中的一种方法是存储“路径”,并具有快速“给我从该节点向下的所有内容”功能。

例如,对于如下所示的树:

    +-- b
    |
a --+       +-- d
    |       |
    +-- c --+
            |
            +-- e

假设上述树中的字母是每一行的“id”,您可以按如下方式存储行:

id    path
a     a
b     a*b
c     a*c
d     a*c*d
e     a*c*e

要查找特定节点的所有后代,您可以对路径列执行“STARTSWITH”查询,即。路径以a*c*开头的所有节点

要确定一个特定节点是否是另一个节点的后代,您将查看最长路径是否以最短路径开始。

例如:

  • e 是 a 的后代,因为 a*c*ea 开头
  • d 是 c 的后代,因为 a*c*da*c 开头

这对您的实例有用吗?

【讨论】:

  • 这正是我所提议的,仅适用于数据库,据我所知 :) 在本机实现中,STARTSWITH 将是缓慢的部分 - 对于具有最大前缀的 m 个潜在父节点n 的长度,我认为这将是 O(nm),而位数组使这个 O(n),以指数内存增长为代价:(
  • 路径长度可能是 O(log(n) ) 所以从 O(log(n)) 开始
  • 静态映射表可以工作吗? IE。您分解所有组合,因此您将为每个节点存储一个后代列表,该列表填充所有后代,无论深度如何?构建查找表需要相当多的启动成本,但之后将能够在恒定时间内(摊销)回答“哪些节点是 X 的后代”,至少如果您使用哈希表/字典的话。跨度>
【解决方案3】:

遍历任何树都需要“深度树”步骤。因此,如果您保持平衡的树结构,则可以证明您的 lookup 操作需要 O(log n) 操作。据我了解,您的树看起来很特别,您无法以平衡的方式维护它,对吗?所以 O(n) 将是可能的。但这在创建树的过程中是不好的,所以你可能会在你使用 lookup 之前死掉......

根据与插入相比您需要查找操作的频率,您可以决定在插入期间付费以维护额外数据结构体。如果你真的需要摊销O(1),我会建议一个散列。在每个插入操作中,您将节点的 所有 父节点放入哈希表中。根据您的描述,这可能是给定 插入 上的 O(n) 个项目。如果你这样做 n inserts,这听起来很糟糕(接近 O(n^2)),但实际上你的树不能降级 不好,所以你可能会得到 O(n log n) 的摊销整体 hastable 大小。 (实际上,log n 部分取决于你的树的退化程度。如果你期望它最大程度退化,不要这样做。)

因此,您将在每个 插入 上支付大约 O(log n),并获得哈希表效率 O(1) strong>查找。

【讨论】:

  • 我刚刚看到我没有充分澄清我的问题,对此感到抱歉:(问题是存在潜在的父节点池,例如 A、B、C - 那么我会想看看 F 是否是父池中任何节点的后代。根据您的 hashmap 想法,这仍然是 O(n),但内存开销应该少得多:)
  • 实际上,我不会听从自己的建议。我不太喜欢hashmaps,我更喜欢保证。其次,我认为我在描述中忽略了一些东西......但如果不是:无论“节点池”是什么意思,我认为哈希条目也应该在那里工作。用 C++ 语言来说,它实际上是我所想的set&lt;&gt;。条目将是pair&lt;Node,Node&gt;,您将使用insert( make_pair(parent,child) )。然后,如果您想检查任何child.isThatAParent(potentialParent) 是否只是return set.find( make_pair(node,potentialParent)!=end)。在 O(1). 中检查任何给定的对
【解决方案4】:

对于 M 路树,而​​不是位数组,为什么不将二进制“trie id”(每级使用 M 位) 存储在每个节点上?对于您的示例(假设 M==2)A=0b01, B=0b0101, C=0b1001, ...

那么你就可以在O(1)中做测试了:

bool IsParent(node* child, node* parent)
{ 
   return ((child->id & parent->id) == parent->id)
}

如果您有一个快速的FindMSB() 函数返回最高有效位集的位置,您可以将存储压缩到每个级别的 ceil(lg2(M)) 位:

mask = (1<<( FindMSB(parent->id)+1) ) -1;
retunr (child->id&mask == parent->id);

【讨论】:

  • 您能否详细说明如何计算该 trie id?请记住,它是一个多路树,每个级别可能有许多节点。
  • 我假设 A) 您已经可以按照上述形式计算 ID,并且 B) M 是固定的。对于第一个函数,只需执行foreach digit: id= (id&lt;&lt;M) | (1&lt;&lt;(digit-1))。对于第二种方法,它是:id = (id&lt;&lt;B) | digit,其中B 是保持 M 所需的位数(即:findMSB(M)+1)。
  • 如果 M 超过 32/64 位,只需使用一个长数组,然后在 ANDing 时遍历所有 id 部分?我喜欢这样:)
  • -或者-如果您有存储空间并且不想乱用位域,请将 id 存储为与您显示的格式完全相同的字节字符串:例如 A 是 "\x01", F是“\x01\x02\x01”。每个节点最多可以处理 255 个子节点,您可以使用 strstr(child,parent)==parent 进行测试
  • 不,表示是正确的,是我第一条评论中的构造算法错误。我们希望级别 0 的 id 位于最低有效位中,这样我们就不必在添加更多级别时将其向左移动。要将 trie id 分配给编号为 1..M 的节点的子节点,构造应为 trie_id = (1 &lt;&lt; (childNum-1)) | parent_id
【解决方案5】:

在前序遍历中,每组后代都是连续的。以您为例,

A B D E C F
+---------+ A
  +---+ B
    + D
      + E
        +-+ C
          + F

如果你可以预处理,那么你需要做的就是给每个节点编号并计算后代区间。

如果您无法进行预处理,则 link/cut tree 为更新和查询提供 O(log n) 性能。

【讨论】:

  • 注意:log n 的性能取决于特定的树拓扑。
【解决方案6】:

您可以回答“节点 A 是节点 B 的后代吗?”形式的查询。在恒定时间内,仅使用两个辅助数组。

对树进行预处理,以深度优先顺序访问,并且对于每个节点A,将其在访问中的开始和结束时间存储在两个数组Start[]和End[]中。

那么,让我们说End[u]和Start[u]分别是节点u访问的结束时间和开始时间。

那么节点 u 是节点 v 的后代当且仅当:

开始[v]

你已经完成了,检查这个条件只需要在数组 Start 和 End 中查找两次

【讨论】:

  • 不幸的是树不是静态的,所以我不能真正预处理它。
【解决方案7】:

看看Nested set model 选择很有效但是更新太慢了

【讨论】:

    【解决方案8】:

    对于它的价值,您在这里要求的等同于测试一个类是否是类层次结构中另一个类的子类型,而在 CPython 等实现中,这只是完成了老式的“迭代父类”寻找父母”的方式。

    【讨论】:

      猜你喜欢
      • 2018-10-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多