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

Posted

技术标签:

【中文标题】在无向图中查找所有大小为 N 的子树【英文标题】:Find all subtrees of size N in an undirected graph 【发布时间】:2011-04-17 07:47:17 【问题描述】:

给定一个无向图,我想生成所有大小为 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(删除了根的图,N,新的任意选择的根),并重复直到剩余图的大小

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

那么这会奏效吗?有什么重大缺陷吗?有任何更简单/已知/规范的方法吗?

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

【问题讨论】:

您说您不想将所有图形都保存在内存中。但是内存中所有大小为 N 的图呢? 只是为了确保您的术语被理解,以下陈述是否正确? 2 个连接的顶点形成大小为 1 的图。连接成三角形的 3 个顶点形成大小为 3 的图,有 3 个大小为 1 的子图,有 3 个大小为 2 的子图。对吧? 我也不想在内存中保存所有大小为 N 的图形。由于分支因子(平均节点度)很高,N 大小的图的数量远大于所有小于 N 的大小的图的数量,所以无论如何,这些语句或多或少是等价的。 是的,您对子树数量的描述是正确的。后一种情况也有 0 个大小为 3 的子图,因为不允许循环。 你是说每个节点只有一个层次的指标?也就是每个节点都知道自己是祖父母却不知道自己的子孙是谁?因此,您是在说“如果 Node 是祖父母,那么后代的整个可能样本空间是多少?” 【参考方案1】:

这需要与存储图形所需的内存量成正比的内存量。它将只返回一次所需大小的树的每个子图。

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

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

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)

【讨论】:

【参考方案2】:

假设您可以破坏原始图表或制作可破坏的副本,我想出了一些可行的方法,但可能完全是sadomaso,因为我没有计算它的 O-Ntiness。它可能适用于小型子树。

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

以 6 个节点的图为例,找到所有大小为 2 的子图(对不起,我完全缺乏艺术表现力):

同样适用于更大的图表,但应该分更多步骤完成。

假设:

Z 最多分支节点的边数 M 所需的子树大小 S 步数 Ns 步中的节点数 假设对节点进行快速排序

最坏的情况: S*(Ns^2 + MNsZ)

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

问题是:无法计算真正的 omicron,因为每个步骤中的节点会根据图形的情况而减少...

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

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

也许逆向会更好地使用 CPU,对具有最大边数的节点进行排序和处理...开始时可能会更少,但是您将从每个节点中提取更多子图...

另一种可能性是计算图中出现次数最少的 egde 计数并从具有它的节点开始,这将减少提取子图时的内存使用和迭代次数...

【讨论】:

对于一个中等大小的高度连接图,可能的答案数量大得离谱。除非您在生成所有结果之前轻松迭代结果,否则您将耗尽内存。 同意btilly。另外,您说“处理每个节点”,但并没有真正详细说明。就我而言,该图是“国王图”。例如,一个 4x4 网格中有 16 个节点的图,有 42 条边。您如何有效地处理所有大小为 15 的树(例如)?我同意,完成后删除节点的策略是一个很好的策略,但第一次迭代是关键,你没有详细说明。 我真的认为这是自我描述的:“处理每个节点”的意思是“从该节点中提取所有大小为 x 的图”,而如果第一步太大,您可以轻松地将其划分为子步骤,例如每一步最多有 10 个节点。 Marino,不错的方法,但您的方法存在缺陷,它不适用于循环图,您的方法仅适用于非循环图。如果图中有一个循环怎么办?你能用循环图编辑你的答案吗?【参考方案3】:

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

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

我会递归到一个 for each 并传入一个“模板”元组

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

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

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

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

【讨论】:

对不起 Matthew - 我最初在原始问题中包含了单词路径,然后删除了大多数引用(用树替换它),但我发现我错过了一些 - 我已经将它重新编辑为纠正那个。所以澄清一下,我对路径(有向与否)不感兴趣,而对原始图的子树不感兴趣,它们都是指定大小的原始图的所有 连接的、非循环的子图。在您的示例中,将有一棵大小为 1 的此类树,其中包含 A-B 边。两个节点的两条边的情况是不可能的,因为这个图没有多重边,所以最多只有一条边。【参考方案4】:

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

【讨论】:

内存有问题,但不是您建议的方式。将图本身保存在内存中很简单(比如有 16 个节点和 42 条边),但即使这个图也至少有数万亿个子图。我无法在内存中保存数万亿张图表 :( 也许我误解了你的解决方案,但我并不担心重复输出。我担心重叠的解决方案。如果您的原始图是一棵树,那么它不是问题,否则,假设您有一个带有边 (0,1)、(0,2)、(1,3)、(2,3) 的菱形图。跨度> 【参考方案5】:

对于 Kn 的图,任意两对顶点之间大约有 n! 条路径。我还没有浏览你的代码,但这是我要做的。

    选择一对顶点。 从一个顶点开始并尝试以递归方式到达目标顶点(类似于 dfs,但不完全是)。我认为这会输出所选顶点之间的所有路径。 您可以对所有可能的顶点对执行上述操作以获得所有简单路径。

【讨论】:

如何有效地将其限制为长度为 N 的路径?另外,我认为这不会找到所有子树,它只会找到两个顶点之间的路径(例如,它永远不会产生任何顶点度数大于 2 的路径,对吧?)。所以意识到我的描述是错误的,我实际上想找到所有树的子图,而不是将其限制为路径。 大小 N 是什么意思?树的直径(最长路径)? 树中的边数,上面也会说明。【参考方案6】:

看来下面的解决方案会起作用。

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

【讨论】:

请注意,我不想计算树,而是实际生成它们。【参考方案7】:

我认为您的解决方案是行不通的,尽管它可以工作。主要问题是子问题可能会产生重叠的树,因此当您将它们合并时,您最终不会得到大小为 n 的树。您可以拒绝所有存在重叠的解决方案,但您最终可能会做比需要更多的工作。

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

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

【讨论】:

你能举一个具体的例子,我的算法会产生重复的输出吗?我看不出来,但确实应该有一个小例子。我看不出我们如何检查 N 个节点的子集,因为 N 个节点没有定义一个图。从与 N 个节点相邻的边中,你选择了哪些边?也就是说,“这些 N 个节点形成一棵树”的说法并不是很好,因为它可能是真或假,具体取决于您选择的边。 我不担心重复输出。我担心重叠的解决方案。如果您的原始图是一棵树,那么它不是问题,否则,假设您有一个带有边 (0,1)、(0,2)、(1,3)、(2,3) 的菱形图。假设您在节点 0 上运行子例程,N = 2。您的下一级递归调用将返回树 (1,3) 和 (2,3)。当您尝试将它们放在一起时,您不会得到一棵树,因为节点 3 是两个子树的一部分。 至于选择哪些边,您从原始图中选择所有边,其两端都在您的子图中。如果你有一个邻接列表开始,子图可以是隐式的,或者如果你有一个 O(1) 时间的方法来查找你的子图中的哪些节点,你可以在 O(E +V) 时间内构造它。 【参考方案8】:

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

这种方法是典型的图枚举算法——您通常需要跟踪一些本身就是图的构建块;如果你试图只处理节点和边,它就会变得难以处理。

【讨论】:

【参考方案9】:

这个算法很大,在这里发布并不容易。但这里是指向reservation search 算法的链接,您可以使用它做您想做的事。 This pdf 文件包含这两种算法。另外,如果你懂俄语,你可以看看this。

【讨论】:

【参考方案10】:

所以你有一个带有边 e_1、e_2、...、e_E 的图。

如果我理解正确,您希望枚举所有子图,这些子图是树并包含 N 条边。

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

编辑:

我们还可以利用树是树的组合这一事实,即每棵大小为 N 的树都可以通过向大小为 N-1 的树添加一条边来“生长”。令 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的决策树。

【讨论】:

这当然有效,但在我的情况下,与子图的数量相比,树的比例会非常小(这可能是大多数图的普遍情况),所以这不符合 O 的标准(实际图数) 好吧,说句公道话,您的问题中没有包含该标准 :) 您只谈论空间复杂度。【参考方案11】:

我认为问题没有明确说明。您提到该图是无向的,并且您尝试查找的子图的大小为 N。缺少的是边的数量以及无论何时您正在寻找二叉树或允许拥有多棵树的树。另外-您是否对同一棵树的镜像反射感兴趣,或者换句话说,列出兄弟姐妹的顺序是否重要?

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

 N^(N-2) 

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

【讨论】:

嗨,Alexei,我不确定您所说的“缺少的是边数”是什么意思。对二叉树没有限制。我正在寻找的树是图论意义上的典型树 - 顶点通常在图中没有任何顺序,它们在形成图的树中也没有。我不是 100% 确定您所说的“镜像反射”是什么意思,但基本上每棵树都完全由它包含的(无序的)边缘集定义,并且不应该有重复。 关于您提出的解决方案,您如何建议有效地“丢弃对称树”?这些将支配枚举,并导致它在时间(生成和丢弃树)和空间(记住所有树以便丢弃是可能的)上不可行 - 因此即使树的实际数量是可处理的,这一代策略不会。 如果你有一个 3 节点树,根 A 和叶 B 和 C (A (B, C)) 据我所知,它的镜像将是 (A (C, B))。 我在this sense 中使用“树” - 没有根,也没有任何节点的排序。基本上,这里的“树”是指一个连通的非循环图。

以上是关于在无向图中查找所有大小为 N 的子树的主要内容,如果未能解决你的问题,请参考以下文章

在具有特定成本的无向图中查找路径

luoguP4841 城市规划

在无向图中查找多边形

有向/无向图中搜环

第六章学习小结

循环有向图和无向图