合并两棵树的算法

Algorithm for combining two trees

提问人:Mark Seemann 提问时间:7/9/2023 最后编辑:Mark Seemann 更新时间:7/11/2023 访问量:222

问:

我正在寻找一种算法来合并或组合两棵树(如果可能的话)。虽然我尝试搜索一般的网络,特别是 Stack Overflow,但我无法找到任何与我所追求的东西相似的东西。

由于这个问题似乎是普遍性的,我发现它不太可能不是一个已解决的问题。因此,我倾向于认为我找不到任何东西源于我使用了不正确或不精确的搜索词。

我不是要求任何人为我编写代码。问题是是否有针对以下类型的问题的已知算法。

给定两棵树,如果它们有共同的节点,我想合并它们。

例如

1         1         1
|         |         |
+-2   +   +-4   =   +-2
|                   |
+-3                 +-3
                    |
                    +-4

因为等号左边的两棵树有一个共同的节点()。1

也可能是两棵树有多个共同节点的情况:

1           1           1
|           |           |
+-2         +-2         +-2
| |         | |         | |
| +-3   +   | +-6   =   | +-3
| |         | |         | |
| +-4       | +-7       | +-4
|           |           | |
+-5         +-8         | +-6
                        | |
                        | +-7
                        |
                        +-5
                        |
                        +-8

因为子树从根共享一条公共路径。


编辑 2023-07-08 18:18Z我们可以假设所有节点标签都是唯一的。在示例中,出于可读性原因,我使用了整数,但实际上,节点将由 GUID 标识。

评论者要求提供更多示例,因此我将尝试再添加一个示例。从本质上讲,我需要将一棵树“拼接”到另一棵树的位置,如果它们共享一个子路径。同样,请记住,节点 ID 可以假定是唯一的。

1             2           1
|             |           |
+-2           +-4         +-2
| |             |         | |
| +-3     +     +-8   =   | +-3
| |             |         | |
| +-4           +-9       | +-4
|   |                     |   |
|   +-5                   |   +-5
|   |                     |   |
|   +-6                   |   +-6
|                         |   |
+-7                       |   +-8
                          |   |
                          |   +-9
                          |
                          +-7

在这里,路径存在于两棵树中,因此结果将第一棵树路径的子树与第二棵树路径的子树组合在一起。(这些 ASCII 树不是那么容易手动完成的,所以我希望我已经正确地渲染了它们......2, 42, 42-4

一般来说,我们可以假设子树(一旦检测到)kan 与一些半组操作一起添加。在上面的示例中,我使用了列表连接,但在我的实际用例中,我将使用集合联合。

结束编辑


完全可以想象,两棵树没有任何共同点,因此不可能将它们组合在一起。在这种情况下,该函数应返回两棵树而不合并它们。

因此,我正在寻找一种具有如下类型/签名的算法:

Tree a -> Tree a -> [Tree a],

使用 Haskell 语法,或者

IEnumerable<Tree<T>> Combine<T>(Tree<T> x, Tree<T> y)

使用 C# 语法。

是否存在相关算法,如果存在,它的名称是什么,我在哪里可以阅读更多关于它的信息?

这并不是说我不能把一些东西放在一起,但如果存在已知的解决方案或算法,我想避免重新发明轮子。

算法 与语言无关

评论

3赞 trincot 7/9/2023
如果两个树对一个节点具有相同的标签,但该节点的路径不同,该怎么办?例如,它们都有一个节点,但第一棵树有它的路径,而第二棵树有 or ?aroot-x-aroot-aroot-y-a
0赞 Mark Seemann 7/9/2023
我试图编辑问题以提供更多信息,所以也许投票结束的人会非常友善,要么撤回投票,要么告诉我需要什么更多信息。在考虑响应时间时,请考虑到时区 - 你所在的地方可能是白天,但在我的地方是晚上,所以如果足够不走运,我可能需要 8-10 个小时才能回答......
0赞 maxplus 7/9/2023
我无法在注释中绘制这些漂亮的 ASCII 图,因此我必须使用无序的父>子关系集(即边)来描述树。你的第一个例子,第二个.这种替代表示是否正确?是需要支持的内容,还是应该正常失败的无效合并操作,或者是否保证永远不会被请求?Merge({1->2, 1->3}, {1->4}) = {1->2, 1->3, 1->4}Merge({1->2, 2->3, 2->4, 1->5}, {1->2, 2->6, 2->7, 1->8}) = {1->2, 1->5, 1->8, 2->3, 2->4, 2->6, 2->7}Merge({1->2, 2->3}, {1->3, 3->4}) = {1->2, 2->3, 3->4}
0赞 Mark Seemann 7/9/2023
@maxplus 感谢您的阐述。我没有想到你的最后一个例子,因为在我的场景中,这保证永远不会被请求。
0赞 maxplus 7/9/2023
然后,如果两棵树共享相同的根,则它与用于 json 或 trie 合并的算法相同。如果没有,则需要在另一棵树中找到其中一棵树的根,并且应该在此时执行类似 json 的合并。我不知道后一种情况是否被广泛使用。如果这确实是您需要的,我可以在答案中更详细地描述它,无论是否实现 Python/C++。

答:

1赞 maxplus 7/9/2023 #1

如果要合并的两棵树具有相等的根 GUID,则算法如下:递归合并函数接受两棵具有相等根的树,返回具有该根的合并树。返回的树根的子项恰好在其中一个输入树中是根的子项,而在两个输入树中作为根的子项的子项以递归方式合并。

如果要合并的两棵树具有不同的根 GUID,则首先其中一个根必须位于另一棵树中,然后在该点执行相同的 GUID 合并。如果 GUID 具有可比性,并且增加了根到叶的次数(如所有三个示例中的情况),则在另一个树中搜索更大的根 GUID 就足够了,否则需要搜索两个根。在树中查找 GUID 的算法也是一个类似于 DFS 的递归函数。如果两个根都不存在在另一棵树中,则这些树是不相交的,不会合并。

F# 中的示例实现演示了 4 个有效的合并请求和 3 个无效的合并请求,在 sharplab.io 上运行

type Tree<'a> =
  struct
    val rootObject : 'a
    val children : Map<int, Tree<'a>>
    new(r, c) = { rootObject = r; children = c }
  end

type Obj =
  struct
    val guid : int
    new(g) = { guid = g }
  end

let rec mergeSameRoot (tree1 : Tree<Obj>) (tree2 : Tree<Obj>) : Tree<Obj> =
  let children : Map<int, Tree<Obj>> =
    Map.fold (fun (acc : Map<int, Tree<Obj>>) (guid : int) (subtree2 : Tree<Obj>) ->
      match Map.tryFind guid acc with
      | Some (subtree1 : Tree<Obj>) ->
          Map.add guid (mergeSameRoot subtree1 subtree2) acc
      | None -> Map.add guid subtree2 acc
    ) tree1.children tree2.children
  Tree (tree1.rootObject, children)

let rec tryMergeIntoFirst (tree1 : Tree<Obj>) (tree2 : Tree<Obj>) : Tree<Obj> option =
  if tree1.rootObject.guid = tree2.rootObject.guid then
    Some <| mergeSameRoot tree1 tree2
  elif Map.isEmpty tree1.children then
    None
  else
    let update : Option<int * Tree<Obj>> =
      Map.fold (fun (acc : Option<int * Tree<Obj>>) (guid : int) (subtree : Tree<Obj>) ->
        match tryMergeIntoFirst subtree tree2 with
        | Some (result : Tree<Obj>) -> Some (guid, result)
        | None -> acc
      ) None tree1.children
    match update with
      | Some (guid : int, subtree: Tree<Obj>) ->
          Some <| Tree (tree1.rootObject, Map.add guid subtree tree1.children)
      | None -> None

let tryMergeEither (tree1 : Tree<Obj>) (tree2 : Tree<Obj>) : Tree<Obj> option =
  match tryMergeIntoFirst tree1 tree2 with
  | Some (result : Tree<Obj>) -> Some result
  | None -> tryMergeIntoFirst tree2 tree1

let tryMergeReturnList (tree1 : Tree<Obj>) (tree2 : Tree<Obj>) : Tree<Obj> list =
  match tryMergeEither tree1 tree2 with
  | Some (result : Tree<Obj>) -> [result]
  | None -> [tree1; tree2]

let examplesDemo () =
  let rec printTree (tree1 : Tree<Obj>) =
    printf "%d: {" tree1.rootObject.guid
    Map.iter (fun k v -> printTree v) tree1.children
    printf "}, "

  let printMerge tree1 tree2 =
    printTree tree1
    printf "\n+\n"
    printTree tree2
    printf "\n=\n"
    match tryMergeReturnList tree1 tree2 with
    | [a] -> printTree a
    | [a; b] -> printf "Disjoint"
    | _ -> failwith "Unreachable code"
    printf "\n\n"

  let keyValue obj value = (obj, Tree (Obj obj, value))
  let leaf obj = keyValue obj Map.empty

  let example1Tree1 = Tree (Obj 1, Map.ofList [leaf 2; leaf 3])
  let example1Tree2 = Tree (Obj 1, Map.ofList [leaf 4])

  let example2Tree1 = Tree (Obj 1, Map.ofList [leaf 5;
    keyValue 2 <| Map.ofList [leaf 3; leaf 4]
  ])
  let example2Tree2 = Tree (Obj 1, Map.ofList [leaf 8;
    keyValue 2 <| Map.ofList [leaf 6; leaf 7]
  ])

  let example3Tree1 = Tree (Obj 1, Map.ofList [leaf 7;
    keyValue 2 <| Map.ofList [leaf 3;
      keyValue 4 <| Map.ofList [leaf 5; leaf 6]
    ]
  ])
  let example3Tree2 = Tree (Obj 2, Map.ofList [
    keyValue 4 <| Map.ofList [leaf 8; leaf 9]
  ])

  let disjointTree1 = Tree (Obj 1, Map.ofList [leaf 2])
  let disjointTree2 = Tree (Obj 3, Map.ofList [leaf 4])

  printMerge example1Tree1 example1Tree2
  printMerge example2Tree1 example2Tree2
  printMerge example3Tree1 example3Tree2
  printMerge disjointTree1 disjointTree2

  printfn "\nInvalid requests:"

  let silentError1Tree1 = Tree (Obj 1, Map.ofList [leaf 2])
  let silentError1Tree2 = Tree (Obj 2, Map.ofList [leaf 1])

  let silentError2Tree1 = Tree (Obj 1, Map.ofList [
    keyValue 2 <| Map.ofList [leaf 3]
  ])
  let silentError2Tree2 = Tree (Obj 1, Map.ofList [leaf 3])

  let silentError3Tree1 = Tree (Obj 1, Map.ofList [leaf 2; leaf 3])
  let silentError3Tree2 = Tree (Obj 2, Map.ofList [leaf 3])

  printMerge silentError1Tree1 silentError1Tree2
  printMerge silentError2Tree1 silentError2Tree2
  printMerge silentError3Tree1 silentError3Tree2

examplesDemo ()

评论

0赞 n. m. could be an AI 7/9/2023
第三个例子呢?
0赞 maxplus 7/9/2023
@n.m.willseey'allonReddit,“在我的特殊情况下,根将是一样的”。我的理解是不需要第三个例子。如果事实证明是,我会更新答案
0赞 Mark Seemann 7/11/2023 #2

鼓励解释和详细说明问题的一个好处是,它可以帮助你更好地理解问题。多亏了所有的问题,我认为我现在有一个算法。至少,我有一个似乎有效的概念证明。

简而言之,算法是这样的:找到两棵树边并集的跨森林。

详细说明:将每棵树转换为一组边,取这两组边的并集,然后找到并集的跨森林。如果两棵树只有一个共同的节点,则该生成林将是一个生成树(我认为)。


为了说明这一点,我创建了一个 Haskell 概念验证。函数本身几乎是微不足道的,而我不得不编写一些相当复杂的代码来适应 Data.Graph 笨拙的 API。

一、所需进口:

import Data.Set (Set, (\\))
import qualified Data.Set as Set
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Foldable (toList)
import Data.Graph
import Data.Tree

我将展示一些代码以及如何在 GHCi 中与它进行交互,因此,首先,这里是 OP 中的树:

t1 = Node 1 [Node 2 [], Node 3 []]
t2 = Node 1 [Node 4 []]
t3 = Node 1 [Node 2 [Node 3 [], Node 4 []], Node 5 []]
t4 = Node 1 [Node 2 [Node 6 [], Node 7 []], Node 8 []]
t5 = Node 1 [Node 2 [Node 3 [], Node 4 [Node 5 [], Node 6 []]], Node 7 []]
t6 = Node 2 [Node 4 [Node 8 [], Node 9 []]]

将树转换为边列表是单行的(如果忽略类型声明,这是可选的,但惯用):

edgesOfTree :: Tree a -> [(a, a)]
edgesOfTree (Node x xs) = xs >>= \t -> (x, rootLabel t) : edgesOfTree t

使用它会得到如下结果:

ghci> edgesOfTree t1
[(1,2),(1,3)]
ghci> edgesOfTree t6
[(2,4),(4,8),(4,9)]

表示相同数据的另一种方法是使用邻接列表。虽然我想我可以编写一个函数来直接转换为这种格式,但显然我发现分两步完成更容易(或者更确切地说:不那么困难 - 我在开玩笑?Tree a

edgesToAdjacencyList :: (Foldable t, Ord k, Ord a) => t (k, a) -> Map k (Set a)
edgesToAdjacencyList =
  foldr (\(p, c) -> Map.insertWith Set.union p (Set.singleton c)) Map.empty

以下是这两个函数的使用方式:

ghci> edgesToAdjacencyList $ edgesOfTree t1
fromList [(1,fromList [2,3])]
ghci> edgesToAdjacencyList $ edgesOfTree t6
fromList [(2,fromList [4]),(4,fromList [8,9])]

下一步是我决定使用 Data.Graph 将这种表示形式转换回一个或多个树的直接结果。graphFromEdges 函数有一个非常特殊的 API,因此我需要一个函数来转换为其所需的输入格式。

这很容易成为概念验证中最复杂的代码,但我认为它是 Data.Graph 的产物,而不是算法本身的基本复杂性。换言之,实现细节:

toGraphEdges :: Ord a => Map a (Set a) -> [(a, a, [a])]
toGraphEdges m =
  let edges = (\(k, s) -> (k, k, toList s)) <$> Map.toList m
      leaves =
        fmap (\x -> (x, x, []))
        $ toList
        $ Set.unions (Map.elems m) \\ Set.fromList (Map.keys m)
  in edges ++ leaves

使此函数复杂化的是,该函数不会从邻接列表中推断叶子,因此必须显式列出它们。graphFromEdges

以下是 GHCi 中的样子:

ghci> toGraphEdges $ edgesToAdjacencyList $ edgesOfTree t1
[(1,1,[2,3]),(2,2,[]),(3,3,[])]
ghci> toGraphEdges $ edgesToAdjacencyList $ edgesOfTree t6
[(2,2,[4]),(4,4,[8,9]),(8,8,[]),(9,9,[])]

由于该函数采用三元组列表,因此一个小实用程序函数也被证明是有帮助的:graphFromEdges

fstOf3 :: (a, b, c) -> a
fstOf3 (x, _, _) = x

现在,所有必需的构建块都已到位,以实现所需的功能:

unite :: Ord a => Tree a -> Tree a -> [Tree a]
unite x y =
  let edges = edgesOfTree x ++ edgesOfTree y
      (graph, nodeFromVertex, _) =
        graphFromEdges $ toGraphEdges $ edgesToAdjacencyList edges
  in fmap (fstOf3 . nodeFromVertex) <$> dff graph

第一个表达式将两个树转换为边列表并将它们连接起来。此时,列表可能包含重复项,但由于将列表转换为集合,因此可以有效地处理该问题。toGraphEdges

以下是 OP 中的示例:

ghci> putStrLn $ drawForest $ fmap show <$> unite t1 t2
1
|
+- 2
|
+- 3
|
`- 4

如果你不了解 Haskell,这可能看起来很晦涩难懂,但函数调用只是 - 表达式的其余部分(在 的左侧)只是将结果打印为一棵漂亮的树。unite t1 t2unite t1 t2

ghci> putStrLn $ drawForest $ fmap show <$> unite t3 t4
1
|
+- 2
|  |
|  +- 3
|  |
|  +- 4
|  |
|  +- 6
|  |
|  `- 7
|
+- 5
|
`- 8

第三个示例也受支持:

ghci> putStrLn $ drawForest $ fmap show <$> unite t5 t6
1
|
+- 2
|  |
|  +- 3
|  |
|  `- 4
|     |
|     +- 5
|     |
|     +- 6
|     |
|     +- 8
|     |
|     `- 9
|
`- 7

到目前为止,这只是一个概念证明。我还没有将其翻译成 F# 并将其用于我的实际目的,因此我可能需要调整一些细节,但我认为这看起来很有希望。