将两个有向图(可能是循环图)合并为一个

时间:2019-03-18 20:55:33

标签: typescript

在我的项目中,我将给定的移动应用程序表示为有向图。移动应用程序的每个屏幕都充当图形节点,并且一条屏幕的边缘链接到另一屏幕。边缘存储在每个父屏幕的内部。使用深度优先搜索,每个边缘也被分类为TREE,FORWARD,CROSS或BACK边缘。所有这些现在都工作正常。

我的问题是我需要将两个图合并为一个图。这两个图可能表示移动应用程序的不同构建。一个版本可能具有以下链:

Screen#1 -> Screen#2 -> Screen#3

下一个版本可能具有以下链接:

Screen#1 -> Screen#X -> Screen#2 -> Screen#3

我需要能够合并这两个图以得到以下图:

Screen#1 -> Screen#X -+
         |            |
         +------------> Screen#2 -> Screen#3

我可以测试每个节点与其他每个节点是否相等。但是我似乎无法设计一种算法,无法将这两个图合并为一个。

我已经在互联网上看了很多东西,但是找不到关于如何合并图形的单个站点或讲座或研究论文。我当前的实现方式(非常容易出错)如下:

1. Identify all nodes in graph A that also exist in graph B (S#1, S#2, S#3)
2. Copy all of these nodes in the final tree..
3. For each of these common nodes, recursively explore their children..
4. If the child exists in the resultant tree, add an edge between the parent and the child. Otherwise, add the child and an edge between the parent and child and repeat the algorithm on the child itself.
5. Do this till there are no unvisited nodes left.

有人可以指出我正确的方向来合并这些图吗?请注意,这两个图可能是循环的。

谢谢, 阿西姆

1 个答案:

答案 0 :(得分:1)

由于您尚未发布Minimal, Complete, and Verifiable example,因此此处的答案可能不完全适合您的用例。希望您仍然可以使用它:


我建议您将图形表示为一组节点数据,这些节点的数据由字符串标签标识,并用成对的节点标签对数组表示边缘。像这样:

// T represents the data type, K represents the union of node labels
class Graph<K extends string, T> {
  nodes: Record<K, T>; 
  edges: Array<{ start: K, end: K }>;

  constructor(nodes: Record<K, T>, edges: Array<{ start: K, end: K }>) {
    this.nodes = Object.assign({}, nodes);
    this.edges = edges.slice();
  }
}

// graphOne is a Graph<"a"|"b"|"c", string>
const graphOne = new Graph(
  {
    a: "NodeA", b: "NodeB", c: "NodeC"
  }, [
    { start: "a", end: "b" },
    { start: "b", end: "c" }
  ]
);

在上面,graphOne是一个简单的图形,其中三个节点分别标记为"a""b""c",并且一条路径从{{1 }}到"a"以及从"b""b"。在这种情况下,存储在每个节点中的数据仅为"c"。在这里,让我们为string类创建一个简单的控制台日志记录方法:

Graph

并使用它:

  print() {
    console.log(this.nodes)
    console.log(this.edges.map(e => e.start + "➡" + e.end));
  }

在这一点上,您可能会反对这种表示方式并不自然地表示您如何使用图,每个节点上都有数据和一些子项,并且您可以通过递归遍历子项遍历图(可能会陷入图循环)等。也就是说,您可能会想到这样的事情:

graphOne.print();
// Object { a: "NodeA", b: "NodeB", c: "NodeC" }
// Array [ "a➡b", "b➡c" ]

幸运的是,您可以相当容易地将interface TraversableGraph<T> { nodes: Array<TraversableNode<T>>; } interface TraversableNode<T> { data: T, children: Array<TraversableNode<T>>; } 转换为Graph。这是一种方法。让我们向TraversableGraph添加一个asTraversableGraph()方法:

Graph

我选择使用getters,以便可遍历图(可能)反映图的突变(例如,您向 asTraversableGraph(): TraversableGraph<T> { return { nodes: (Object.keys(this.nodes) as K[]).map( k => this.asTraverableNode(k)) }; } private asTraverableNode(k: K): TraversableNode<T> { const graph = this; return { get data() { return graph.nodes[k]; }, get children() { return graph.edges.filter(e => e.start === k).map( e => graph.asTraverableNode(e.end)); } } } 和{{1}添加新边}(如果您逐步浏览的话)也将拥有新的优势。但是您不必这样做。

让我们确保其行为合理:

Graph

最后,我们开始合并。使用TraversableGraph表示,这不再是一些奇怪的毛茸茸的/循环的/递归的事情。您只需合并节点并合并边,然后回家。通过向graphOne.asTraversableGraph().nodes[0].data; // "NodeA" graphOne.asTraversableGraph().nodes[0].children[0].data; // "NodeB" 添加Graph方法,这是一种可行的方法:

merge()

请注意,Graph需要两个参数:另一个图和一个 merge<L extends string, U, V>( otherGraph: Graph<L, U>, dataMerge: (x: T | undefined, y: U | undefined) => V ): Graph<K | L, V> { const thisGraph = this as Graph<K | L, T | undefined>; const thatGraph = otherGraph as Graph<K | L, U | undefined>; // merge nodes const nodes = {} as Record<K | L, V>; (Object.keys(thisGraph.nodes) as (K | L)[]).forEach( k => nodes[k] = dataMerge(thisGraph.nodes[k], thatGraph.nodes[k])); (Object.keys(thatGraph.nodes) as (K | L)[]).filter(k => !(k in nodes)).forEach( k => nodes[k] = dataMerge(thisGraph.nodes[k], thatGraph.nodes[k])); // merge edges const edges: Record<string, { start: K | L, end: K | L }> = {}; thisGraph.edges.forEach(e => edges[JSON.stringify(e)] = e); thatGraph.edges.forEach(e => edges[JSON.stringify(e)] = e); return new Graph(nodes, Object.keys(edges).map(k => edges[k])); } 回调,其回调是将第一个图上的节点(如果存在)上的数据与来自第一个图上的节点的数据合并。第二个图上具有相同标签的节点(如果存在),并生成您想要在合并图中看到的数据。

通过遍历每个图的节点列表并在每个图的相同标签节点上运行merge()来合并节点。只需合并两个边缘列表并确保没有重复即可合并边缘(我通过使用边缘上的dataMerge()作为唯一键来做到这一点)。

让我们通过引入另一个图形来查看它是否起作用:

dataMerge()

并合并它,注意JSON.stringify()有点复杂,无法处理存储在// graphTwo is a Graph<"b" | "c" | "d", string> const graphTwo = new Graph( { b: "NodeB", c: "Node C", d: "NodeD" }, [ { start: "b", end: "d" }, { start: "b", end: "c" }, { start: "d", end: "c" } ] ); graphTwo.print(); // Object { b: "NodeB", c: "Node C", d: "NodeD" } // Array(3) [ "b➡d", "b➡c", "d➡c" ] 和存储在dataMerge()中的字符串数据之间的冲突:

graphOne

它有效!即使graphTwo// graphMerged is a Graph<"a" | "b" | "c" | "d", string> const graphMerged = graphOne.merge(graphTwo, (x, y) => typeof x === 'undefined' ? (typeof y === 'undefined' ? "??" : y) : ((x === y || typeof y === 'undefined') ? x : x + "/" + y) ); graphMerged.print(); // Object { a: "NodeA", b: "NodeB", c: "NodeC/Node C", d: "NodeD" } // Array(4) [ "a➡b", "b➡c", "b➡d", "d➡c" ] 具有相同的边缘,合并的图也只有一个b➡c边缘。并且标记为graphOne的节点的名称在graphTwoc)和graphOne"NodeC")中是不同的,因此graphTwo加入了他们("Node C")。

这是上面所有代码的Playground link


希望有帮助。祝你好运!