Tree的所有可能分区(集群)

时间:2017-01-29 10:48:30

标签: python algorithm tree

假设树具有 n 顶点和 n-1 边缘。所有节点都已连接。我想将树分成几部分,每个部分必须包含至少 2 个顶点。

我可以用多少种方法做到这一点?

示例:

     1
   /   \
  2     3
 / \     \
4   5     6

对于上面给出的树,答案是 3

1. [1,2,3,4,5,6] 
2. [1,3,6] and [2,4,5]
3. [3,6] and [1,2,4,5]

        1                   1               1    
      /   \                  \             /   
1)   2     3      2)    2     3      3)   2     3 
    / \     \          / \     \         / \     \ 
   4   5     6        4   5     6       4   5     6 

2 个答案:

答案 0 :(得分:2)

这是一个线性时间DP。设P(u)为以u为根的子树的有效分区数。让A(u)为"几乎有效"以u为根的子树的分区,即除了u之外的每个节点都属于具有至少两个顶点的部分。我们有复发

P(u) = product_{v in Children(u)} (2 P(v) + A(v)) - A(u)
A(u) = product_{v in Children(u)} P(v),

可以在线性时间内进行评估。

重复背后的直觉是对u进行案例分析,u是所考虑的子树的根。

  1. u与其子女无关。分区几乎是有效的;所有以u子项为根的子树都有有效分区。

  2. 1 / \ 2 3 / \ \ 4 5 6 与其一个或多个孩子在一起。同一部分的孩子有效或几乎有效;不同部分的孩子是有效的。有效的孩子有两种选择(与父母一起加入或不加入);几乎有效的孩子都有一个(加入他们的父母)。

  3. 例如,在树上

    A(4) = 1  (empty product is 1)
    P(4) = 1 - A(4) = 0
    A(5) = 1
    P(5) = 1 - A(5) = 0
    A(2) = P(4) P(5) = 0
    P(2) = (2 P(4) + A(4)) (2 P(5) + A(5)) - A(2) = 1
    A(6) = 1
    P(6) = 1 - A(6) = 0
    A(3) = P(6) = 0
    P(3) = (2 P(6) + A(6)) - A(3) = 1
    A(1) = P(2) P(3) = 1
    P(1) = (2 P(2) + A(2)) (2 P(3) + A(3)) - A(1) = 3.
    

    我们有

       1
     /   \
    2     3
           \
            6
    

    在树上

    A(2) = 1
    P(2) = 1 - A(2) = 0
    A(6) = 1
    P(6) = 1 - A(6) = 0
    A(3) = P(6) = 0
    P(3) = (2 P(6) + A(6)) - A(3) = 1
    A(1) = P(2) P(3) = 0
    P(1) = (2 P(2) + A(2)) (2 P(3) + A(3)) - A(1) = 2.
    

    我们有

    double calcTipsPerHour = totalTips / resultTotalHours;
    

答案 1 :(得分:1)

我想出了一个解决方案。

我的脚本未经过优化,可能需要很长时间才能运行。此外,它假设树是二进制的,就像你在问题中给出的例子一样。

def count_partitions(tree):
    if len(tree.nodes()) <= 1:
        return 0
    elif len(tree.nodes()) == 2:
        return 1
    else:
        count = 1
        for edge in tree.edges():
            upper, lower = tree.cut(edge)
            count += count_partitions(upper) * count_partitions(lower)
        return count

那么,它是如何运作的。

事实上,如果树有零个或一个节点,则不能在每个至少两个节点的树中进行分区。如果一棵树有两个节点,那么只有一个这样的分区。

如果树具有大于2的任意数量的节点,则至少存在一个这样的分区(count = 1)。所以一个接一个地(for edge in tree.edges),我从树中移除每个边(tree.cut(edge))。删除边缘的这种操作导致两棵树。然后,分区数等于这两个子树(count_partitions(upper) * count_partitions(lower))的分区数的乘积。

你可以在你的例子上测试它;它将按预期返回3

关于复杂性,它并不是很好。我不知道是否还有其他方法可以提高效率,但是这个方法在我看来就像 O(卡片(边缘)!):在给定状态下{ {1}}边,对于每条边,执行n个其他边上的循环。

以下是对随机创建的小树执行时间的评估:

  • 5个节点:0.0005s
  • 6个节点:0.0019s
  • 7个节点:0.0057s
  • 8个节点:0.022s
  • 9个节点:0.096s
  • 10个节点:0.44s
  • 11个节点:2.144s(从这里开始,我在更少的树上进行测试)
  • 12个节点:11.85s

实施细节

我写了一个n-1类(和一个Tree)类,它主要有三种方法:Nodenodesedges。我很快就编写了这个脚本,所以我只想尝试一些有效的东西,无论效率如何。必须有更好的方法来实现这一点。

cut方法是最重要的(不过它真的很复杂)。它沿着边缘分割两部分树,然后返回两棵新树。

Tree.cut

<强>更新

正如评论中所讨论的,我试图记住这个功能。

我选择记住树的节点,因为它在我的实现中没问题。结果当然比以前更好;这是比较执行时间:

  • 5个节点:0.0006s
  • 6个节点:0.0013s
  • 7个节点:0.0021s
  • 8个节点:0.0036s
  • 9个节点:0.0067s
  • 10个节点:0.012s
  • 11个节点:0.022s
  • 12个节点:0.040s
  • 13个节点:0.076s
  • 20个节点:8.07s

在main函数的过程中多次创建相同的子树,因此memoization非常有用。

这是memoization的代码(非常具体到这个实现):

def cut(self, edge):
    upper_tree = Tree(self)
    lower_tree = Tree()
    for node in upper_tree.nodes():
        if node.value == edge[0].value:
            if node.left and node.left.value == edge[1].value:
                lower_tree.root = node.left
                node.left = None
            elif node.right and node.right.value == edge[1].value:
                lower_tree.root = node.right
                node.right = None

            return (upper_tree, lower_tree)