获取每个节点的最大树深度

Posted

技术标签:

【中文标题】获取每个节点的最大树深度【英文标题】:Get maximum tree depth for each node 【发布时间】:2022-01-13 13:57:34 【问题描述】:

假设我们有一棵树,每个节点可以有多个孩子,而孩子可以有更多的孩子等等。

以这棵树为例:

- Node 1
    - Node 1.1
    - Node 1.2
        - Node 1.2.1
            - Node 1.2.1.1
            - Node 1.2.1.2
        - Node 1.2.2
    - Node 1.3
        - Node 1.3.1

节点 1 的深度 = 0(根)

节点 1.1、1.2、1.3 的深度 = 1 等等

对于每个节点,我想计算它可以达到的最大深度。 例如,最大节点 1。深度为 3(树的深度与节点 1.2.1.1 一样深)。而节点 1.3 最大深度 = 1(子树达到节点 1.3.1 的深度)

现在我可以做的是创建一个函数,它接受一个子树并计算到最深的节点,然后返回深度值。但这需要为每个节点调用该函数,这对我来说似乎非常低效。

我想一次性创建树并计算最大深度。

我保持代码非常简单,因为我的函数包含许多其他操作(例如在我从头开始构建树时生成新的子节点,但为简单起见,我省略了这些部分)。 但基本上,我是这样遍历树的:

def create_behavior_tree(depth, max_depth, behavior_tree)
  for child in behavior_tree.children:
    if depth > max_depth:
      max_depth = depth
    if len(child) > 0: # Expanding node
      max_depth = create_behavior_tree(depth + 1, max_depth, child)
      child.max_depth = max_depth  # problem: stores the values in "reverse"
    else: # Single node without children
      child.max_depth = depth


create_behavior_tree(1, 0, Tree)

但是,当我这样做时,我无法达到外部节点的最新 max_depth 值,只能在最里面的节点内达到(因为这是递归)。 所以这将计算:节点 1 最大深度 = 0,节点 1.2 最大深度 = 1,节点 1.2.1 最大深度 = 2 等等。实际上是相反的。

那么,也许我需要在这里使用全局变量?

EDIT - 我的函数的更详细版本

def create_behavior_tree(depth, behavior_tree, children, max_tree_depth, node_count):
    if depth <= max_tree_depth:
        for child in children:
            # add behavior node
            if type(child) == Behaviour:
                behavior_tree.add_children([child])
                node_count += 1  # counting total nodes in tree
            # add composite node
            if type(child) == Composite:
                # replace by behavior node (reached max tree depth)
                if depth == max_tree_depth:
                    node = create_behaviour_node()
                    behavior_tree.add_children([node])
                    node_count += 1
                else:
                    behavior_tree.add_children([child])
                    node_count += 1
                    # further expand behavior tree 
                    children = create_random_children(size=3)
                    _, node_count = create_behavior_tree(depth + 1, node, children, max_tree_depth, node_count)
    return behavior_tree, node_count

random_children = create_random_children(size=3)  # Create 3 random children
root = py_trees.composites.Selector("Selector")

create_behavior_tree(1, root, random_children, 5, 0)

【问题讨论】:

没有全局变量!编写一个以节点为参数并返回最大值的递归函数。该节点下的深度。如果没有子节点,它应该返回 0,否则在子节点上递归调用函数的结果的 1 + max。 @MarkLavin 这是我最初的想法。但是,当我创建树(通过递归)时,它已经遍历了每个节点。难道不能在那个过程中一次获得这些最大值吗? @ggorlen 我编辑了我的帖子。它基于 0(因此如果一个节点没有子节点,则最大深度应为 0) 【参考方案1】:

利用递归的自相似性,这与任何其他一次 O(n) 深度/树高算法相同,除了在备份调用堆栈时为每个节点设置最大深度属性:

def set_all_depths(tree):
    if not tree:
        return 0

    tree.max_depth = (
        max(map(set_all_depths, tree.children)) 
        if tree.children else 0
    )
    return tree.max_depth + 1

def print_tree(tree, depth=0):
    if tree:
        print("    " * depth + 
              f"tree.val [max_depth: tree.max_depth]")

        for child in tree.children:
            print_tree(child, depth + 1)


class TreeNode:
    def __init__(self, val, children=None, max_depth=None):
        self.val = val
        self.children = children or []
        self.max_depth = max_depth


if __name__ == "__main__":
    tree = TreeNode(
        "1",
        [
            TreeNode("1.1"),
            TreeNode(
                "1.2",
                [
                    TreeNode(
                        "1.2.1",
                        [
                            TreeNode("1.2.1.1"),
                            TreeNode("1.2.1.2"),
                        ],
                    ),
                    TreeNode("1.2.2"),
                ],
            ),
            TreeNode(
                "1.3",
                [
                    TreeNode("1.3.1"),
                ],
            ),
        ],
    )
    set_all_depths(tree)
    print_tree(tree)

输出:

1 [max_depth: 3]
    1.1 [max_depth: 0]
    1.2 [max_depth: 2]
        1.2.1 [max_depth: 1]
            1.2.1.1 [max_depth: 0]
            1.2.1.2 [max_depth: 0]
        1.2.2 [max_depth: 0]
    1.3 [max_depth: 1]
        1.3.1 [max_depth: 0]

基于后续的cmets,如果想一次性做树并分配最大深度,一种方法是从children中挑出最大深度,分配给备份途中的每个节点调用堆栈。

这是一个与库无关的概念证明,因为我没有新示例中的类:

import random
from random import randint


def make_random_tree(max_depth=4):
    if max_depth <= 0:
        return TreeNode(Id.make(), max_depth=0)

    node = TreeNode(Id.make())
    node.children = [
        make_random_tree(max_depth - 1) for _ in range(randint(0, 4))
    ]
    node.max_depth = 1 + (
        max([x.max_depth for x in node.children])
        if node.children else 0
    )
    return node

def print_tree(tree, depth=0):
    if tree:
        print("    " * depth + 
              f"tree.val [max_depth: tree.max_depth]")

        for child in tree.children:
            print_tree(child, depth + 1)


class TreeNode:
    def __init__(self, val, children=None, max_depth=None):
        self.val = val
        self.children = children or []
        self.max_depth = max_depth


class Id:
    identifier = 0

    def make():
        Id.identifier += 1
        return Id.identifier


if __name__ == "__main__":
    random.seed(1)
    tree = make_random_tree()
    print_tree(tree)

输出:

1 [max_depth: 4]
    2 [max_depth: 3]
        3 [max_depth: 1]
        4 [max_depth: 2]
            5 [max_depth: 1]
            6 [max_depth: 1]
                7 [max_depth: 0]
                8 [max_depth: 0]
                9 [max_depth: 0]
        10 [max_depth: 2]
            11 [max_depth: 1]
                12 [max_depth: 0]
                13 [max_depth: 0]
                14 [max_depth: 0]
            15 [max_depth: 1]
                16 [max_depth: 0]
                17 [max_depth: 0]
                18 [max_depth: 0]
            19 [max_depth: 1]
                20 [max_depth: 0]
        21 [max_depth: 1]

也就是说,在第二遍中分配深度仍然是 O(n),因此除非您有真正的大型或性能关键代码,否则一次性完成所有操作可能是过早的优化。

【讨论】:

这实际上是我想要的。但是,我很难在我的函数中实现这一点(查看我的最新编辑)。我的树是随机生成的。所以我没有在函数中传递已经创建的树。它需要一个根节点,然后向其添加 3 个随机子节点。然后可以再次扩展其中一些孩子,直到达到最大树深度 无论你是否在现场创建节点都是一样的算法。 max_depth 不能自顶向下计算;您必须将该信息作为单独的返回值、某个子节点上的属性、按节点索引的查找表传递回调用堆栈……这可能是最简单的方法,只需检查一个子节点并查看什么max_depth 它具有并加 1 以计算当前节点的最大深度。如果在基本情况下没有子节点/当前节点,则深度为 0(取决于您是从 0 还是从 1 计算深度)。 另外,除非树很大,否则它只是另一个将其作为单独步骤计算的过程;仍然是 O(n)。否则可能是过早的优化。 我上面的两条评论可能具有误导性:您想从所有孩子那里获取最大深度。我更新了答案,通过概念证明更详细地涵盖了这个案例。【参考方案2】:

您可以使用广度优先搜索来获取所有节点的深度以及从根到每个节点的路径,然后迭代所有节点的结果,产生最大深度:

from collections import deque
tree = '1': '1': None, '2': '1': '1': None, '2': None, '2': None, '3': '1': None
def bfs(t):
   d = deque([(a, [a], 0, b) for a, b in t.items()])
   while d:
      yield (n:=d.popleft())[:-1]
      if n[-1] is not None:
         d.extend([(a, n[1]+[a], n[2]+1, b) for a, b in n[-1].items()])

nodes = list(bfs(tree))
r = '.'.join(b):max(y for _, x, y in nodes if a in x) - c for a, b, c in nodes

输出:

'1': 3, '1.1': 2, '1.2': 2, '1.3': 1, '1.2.1': 1, '1.2.2': 1, '1.3.1': 1, '1.2.1.1': 0, '1.2.1.2': 0

【讨论】:

【参考方案3】:

如果有人还想添加有关节点当前深度以及节点包含多少节点(计数节点)的信息,我扩展了 @ggorlen 的答案

def create_depth_map(self, tree, depth, node_count):
    results = list(zip(*map(lambda x: self.create_depth_map(*x), [[child, depth + 1, node_count] for child in tree.children])))
    tree.max_depth = (max(results[0]) if tree.children else 0)
    tree.current_depth = depth
    node_count += (sum(results[1]) if tree.children else 0)
    tree.node_count = node_count
    return tree.max_depth + 1, node_count

所以它看起来像:

Selector [current_depth: 0] [max_depth: 5] [node_count: 17]
    Action [current_depth: 1] [max_depth: 0] [node_count: 1]
    Inverter [current_depth: 1] [max_depth: 3] [node_count: 7]
        Sequence [current_depth: 2] [max_depth: 2] [node_count: 6]
            Action [current_depth: 3] [max_depth: 0] [node_count: 1]
            Sequence [current_depth: 3] [max_depth: 1] [node_count: 4]
                Action [current_depth: 4] [max_depth: 0] [node_count: 1]
                Condition [current_depth: 4] [max_depth: 0] [node_count: 1]       
                Condition [current_depth: 4] [max_depth: 0] [node_count: 1]       
    Inverter [current_depth: 1] [max_depth: 4] [node_count: 8]
        Sequence [current_depth: 2] [max_depth: 3] [node_count: 7]
            Sequence [current_depth: 3] [max_depth: 2] [node_count: 5]
                Parallel [current_depth: 4] [max_depth: 1] [node_count: 3]        
                    Action [current_depth: 5] [max_depth: 0] [node_count: 1]      
                    Condition [current_depth: 5] [max_depth: 0] [node_count: 1]   
                Action [current_depth: 4] [max_depth: 0] [node_count: 1]
            Action [current_depth: 3] [max_depth: 0] [node_count: 1]

非常感谢 ggorlen,这让我更进一步!

【讨论】:

以上是关于获取每个节点的最大树深度的主要内容,如果未能解决你的问题,请参考以下文章

Dijit 树,如何提高根下有 500 个子节点的大树的性能

R包“树”:如何控制最大树深度?

LeetCode 572

LeetCode 572

CCF 201503-04 网络延时

深度优先搜索