在无向图中查找大小为N的所有子树

时间:2011-04-17 07:47:17

标签: algorithm language-agnostic graph tree graph-theory

给定一个无向图,我想生成所有子图,这些子图是大小为N的树,其中 size 指的是树中边的数量。

我知道它们中有很多(至少对于具有恒定连接性的图形指数多少) - 但这没关系,因为我相信节点和边缘的数量使得这对于至少微小的N值来说是易处理的(比如说10或更少)。

该算法应该具有内存效率 - 也就是说,它不需要同时在内存中包含所有图形或其中一些大的子集,因为即使对于相对较小的图形,这也可能超过可用内存。所以像DFS这样的东西是可取的。

根据起始图graph和所需长度N,这是我在伪代码中的想法:

选择任意节点root作为起点,然后拨打alltrees(graph, N, root)

alltrees(graph, N, root)
 given that node root has degree M, find all M-tuples with integer, non-negative values whose values sum to N (for example, for 3 children and N=2, you have (0,0,2), (0,2,0), (2,0,0), (0,1,1), (1,0,1), (1,1,0), I think)
 for each tuple (X1, X2, ... XM) above
   create a subgraph "current" initially empty
   for each integer Xi in X1...XM (the current tuple)
    if Xi is nonzero
     add edge i incident on root to the current tree
     add alltrees(graph with root removed, N-1, node adjacent to root along edge i)
   add the current tree to the set of all trees
 return the set of all trees

这只找到包含所选初始根的树,所以现在删除这个节点并调用alltrees(删除了root的图,N,新任意选择的root),并重复直到剩下的图的大小< N(因为不存在所需大小的树)。

我也忘记了每个被访问节点(每个根节点用于一些alltrees调用)需要被标记,并且上面考虑的子集应该只是相邻的未标记子节点。我想我们需要考虑没有未标记的孩子存在的情况,但深度> 0,这意味着这个“分支”未能达到所需的深度,并且不能形成解集的一部分(因此可以中止与该元组相关的整个内循环)。

这样做会有效吗?有什么重大缺陷吗?有没有更简单/已知/规范的方法呢?

上面列出的算法的一个问题是它不满足内存效率要求,因为递归将在内存中保存大量树。

11 个答案:

答案 0 :(得分:9)

这需要一定量的内存,与存储图形所需的内存成比例。它将返回每个具有所需大小的树的子图。

请记住,我只是在这里输入了它。可能有错误。但这个想法是你一次一个地走节点,每个节点搜索包含该节点的所有树,但没有先前搜索过的节点。 (因为那些已经用尽了。)内部搜索是通过将边缘列出到树中的节点以及决定是否将其包含在树中的每个边缘来递归完成的。 (如果它会创建一个循环,或者添加一个耗尽的节点,那么就不能包含该边缘。)如果将它包含在树中,则使用的节点会增长,并且您可能有新的可能边缘添加到搜索中。

为了减少内存使用,剩下要查看的边缘由递归调用的所有级别操作,而不是在每个级别复制该数据的更明显的方法。如果复制了该列表,则总内存使用量将达到树的大小乘以图中边数的数量。

def find_all_trees(graph, tree_length):
    exhausted_node = set([])
    used_node = set([])
    used_edge = set([])
    current_edge_groups = []

    def finish_all_trees(remaining_length, edge_group, edge_position):
        while edge_group < len(current_edge_groups):
            edges = current_edge_groups[edge_group]
            while edge_position < len(edges):
                edge = edges[edge_position]
                edge_position += 1
                (node1, node2) = nodes(edge)
                if node1 in exhausted_node or node2 in exhausted_node:
                    continue
                node = node1
                if node1 in used_node:
                    if node2 in used_node:
                        continue
                    else:
                        node = node2
                used_node.add(node)
                used_edge.add(edge)
                edge_groups.append(neighbors(graph, node))
                if 1 == remaining_length:
                    yield build_tree(graph, used_node, used_edge)
                else:
                    for tree in finish_all_trees(remaining_length -1
                                                , edge_group, edge_position):
                        yield tree
                edge_groups.pop()
                used_edge.delete(edge)
                used_node.delete(node)
            edge_position = 0
            edge_group += 1

    for node in all_nodes(graph):
        used_node.add(node)
        edge_groups.append(neighbors(graph, node))
        for tree in finish_all_trees(tree_length, 0, 0):
            yield tree
        edge_groups.pop()
        used_node.delete(node)
        exhausted_node.add(node)

答案 1 :(得分:4)

假设你可以摧毁原始图形或制作一个可销毁的副本,我找到了可以工作但可能完全悲伤的东西,因为我没有计算它的O-Ntiness。它可能适用于小子树。

  • 在每个步骤中逐步完成:
  • 对图形节点进行排序,以便获得按相邻边数ASC
  • 排序的节点列表
  • 处理第一个边缘数相同的所有节点
  • 删除这些节点

有关6个节点查找所有2个子图的图表示例(对不起我完全缺乏艺术表达):

enter image description here

同样适用于更大的图表,但应该采取更多步骤。

假设:

  • 大多数分支节点的Z个边缘
  • M所需的子树大小
  • S步骤
  • 步骤中的Ns个节点数
  • 假设快速排序节点

最坏情况:  S *(Ns ^ 2 + M Ns Z)

平均情况:  S *(NslogNs + M Ns (Z / 2))

问题是:无法计算真实的omicron,因为每个步骤中的节点将减少,具体取决于图表...

使用这种方法解决整个问题可能非常耗费时间在连接节点很多的图形上,但它可以是并行化的,您可以执行一个或两个步骤,删除脱位节点,提取所有子图,然后选择剩下的另一种方法,但你会从图中删除很多节点,这样可以减少剩余的运行时间......

不幸的是,这种方法会使GPU受益,而不是CPU,因为每一步都有大量具有相同边数的节点....如果不使用并行化,这种方法可能很糟糕......

对于CPU来说,反转可能会更好,排序并继续使用具有最大边数的节点......那些在启动时可能会更少,但是您将从每个节点中提取更多的子图... < / p>

另一种可能性是计算图中发生率最低的egde计数,并从拥有它的节点开始,这将减轻内存使用和提取子图的迭代次数......

答案 2 :(得分:1)

如果内存是最大的问题,您可以使用正式验证工具使用NP-ish解决方案。即,猜测大小为N的节点的子集,并检查它是否是图形。为了节省空间,您可以使用BDD(http://en.wikipedia.org/wiki/Binary_decision_diagram)来表示原始图形的节点和边缘。另外,您可以使用符号算法来检查您猜测的图形是否真的是图形 - 因此您无需在任何时候构建原始图形(也不是N尺寸的图形)。你的内存消耗应该是(在大O中)log(n)(其中n是原始图的大小)来存储原始图,而另一个log(N)来存储每个“小图” “ 你要。 另一个工具(应该更好)是使用SAT求解器。即,构造一个SAT公式,如果子图是一个图并将其提供给SAT求解器,则该公式为真。

答案 3 :(得分:1)

除非我正在阅读错误的问题,否则人们似乎过于复杂了。 这只是“N个边缘内的所有可能路径”,并且您允许循环。 对于两个节点:A,B和一个边缘,您的结果将是: AA,AB,BA,BB

对于两个节点,结果将是两条边: AAA,AAB,ABA,ABB,BAA,BAB,BBA,BBB

我会为每个人递归并传递一个“模板”元组

N=edge count
TempTuple = Tuple_of_N_Items ' (01,02,03,...0n) (Could also be an ordered list!)
ListOfTuple_of_N_Items ' Paths (could also be an ordered list!)
edgeDepth = N

Method (Nodes, edgeDepth, TupleTemplate, ListOfTuples, EdgeTotal)
edgeDepth -=1
For Each Node In Nodes
    if edgeDepth = 0 'Last Edge
        ListOfTuples.Add New Tuple from TupleTemplate + Node ' (x,y,z,...,Node)
    else
        NewTupleTemplate = TupleTemplate + Node ' (x,y,z,Node,...,0n)
        Method(Nodes, edgeDepth, NewTupleTemplate, ListOfTuples, EdgeTotal
next

这将为给定的边数计数创建每个可能的顶点组合 缺少的是工厂在给定边数的情况下生成元组。

最终得到一个可能的路径列表,操作是节点^(N + 1)

如果使用有序列表而不是元组,则无需担心工厂创建对象。

答案 4 :(得分:0)

对于 K n 的图形,在任意两对顶点之间存在大约 n!路径。我没有完成你的代码,但这就是我要做的。

  1. 选择一对顶点。
  2. 从顶点开始并尝试递归地到达目标顶点(类似于dfs但不完全相同)。我认为这将输出所选顶点之间的所有路径。
  3. 您可以对所有可能的顶点对执行上述操作以获取所有简单路径。

答案 5 :(得分:0)

似乎以下解决方案可行。

将所有分区转换为所有顶点集的两部分。然后计算结尾位于不同部分(k)的边数;这些边对应于树的边缘,它们连接第一和第二部分的子树。以递归方式计算两个部分的答案(p1,p2)。然后,可以将整个图的答案计算为k * p1 * p2的所有这些分区的总和。但是所有树木都会被考虑N次:每个边缘一次。因此,总和必须除以N才能得到答案。

答案 6 :(得分:0)

这个算法很大,不容易在这里发布。但这里有reservation search算法的链接,您可以使用它来做您想做的事情。 This pdf文件包含两种算法。此外,如果您了解俄语,您可以查看this

答案 7 :(得分:0)

我认为你的解决方案不起作用,尽管它可以起作用。主要的问题是子问题可能会产生重叠的树,所以当你采用它们的并集时,你最终得不到一个大小为n的树。您可以拒绝所有存在重叠的解决方案,但最终可能会做更多的工作而不是需要。

由于您可以使用指数运行时,并且可能会编写2 ^ n个树,因此使用V.2 ^ V算法并不是一件坏事。因此,最简单的方法是生成节点中所有可能的子集,然后测试每个节点是否形成树。由于测试节点的子集是否形成树可能需要O(E.V)时间,我们可能会讨论V ^ 2.V ^ n时间,除非你有一个O(1)度的图。这可以通过以两个连续子集恰好在一个被交换的节点中不同的方式枚举子集来稍微改进。在这种情况下,您只需检查新节点是否连接到任何现有节点,这可以通过保留所有现有节点的哈希表,与新节点的传出边数成比例地进行。

接下来的问题是如何枚举给定大小的所有子集 这样在连续子集之间交换的元素不超过一个。我会把它作为练习让你弄明白:)

答案 8 :(得分:0)

我认为在this site有一个很好的算法(使用Perl实现)(寻找TGE),但是如果你想在商业上使用它,你需要联系作者。该算法与您的问题类似,但通过使过程包含当前工作子树作为参数(而不是单个节点)来避免递归爆炸。这样,可以选择性地包括/排除从子树发出的每个边缘,并且在扩展树(具有新边缘)和/或缩小图形(没有边缘)上递归。

这种方法是典型的图枚举算法 - 您通常需要跟踪少数本身就是图形的构建块;如果你试图只处理节点和边缘,那就变得难以处理。

答案 9 :(得分:0)

因此,您的图表边缘为e_1,e_2,...,e_E。

如果我理解正确,你想要枚举所有树木,包含N个边缘。

一个简单的解决方案是生成每个E选择N个子图并检查它们是否为树。 你考虑过这种方法吗?当然,如果E太大,那么这是不可行的。

编辑:

我们还可以使用树是树的组合的事实,即,通过将边添加到大小为N-1的树,可以“生长”每个大小为N的树。设E是图中边的集合。然后算法就可以这样了。

T = E
n = 1
while n<N
    newT = empty set
    for each tree t in T
        for each edge e in E
            if t+e is a tree of size n+1 which is not yet in newT
                add t+e to newT 
    T = newT
    n = n+1

在此算法结束时,T是大小为N的所有子树的集合。如果空间是个问题,请不要保留树的完整列表,而是使用紧凑表示,例如将T表示为决策树使用ID3

答案 10 :(得分:-2)

我认为问题不明确。你提到图表是无向的,你试图找到的子图的大小为N.缺少的是边数,每当树正在寻找二进制或你允许多树时。另外 - 您是否对同一棵树的镜像反射感兴趣,或者换句话说,哪些兄弟姐妹的列表很重要?

如果你试图找到的树中的单个节点允许有两个以上的兄弟,这应该是允许的,因为你没有对初始图表指定任何限制,并且你提到结果子图应该包含所有节点。 您可以通过执行深度优先遍历来枚举所有具有树形式的子图。您需要在遍历期间为每个兄弟重复遍历图形。当您需要以root身份重复每个节点的操作时。 丢弃对称树,你最终将

 N^(N-2) 

树,如果您的图表是完全连接的网格,或者您需要应用Kirchhoff's Matrix-tree theorem