将有向无环图 (DAG) 转换为树

Posted

技术标签:

【中文标题】将有向无环图 (DAG) 转换为树【英文标题】:Converting Directed Acyclic Graph (DAG) to tree 【发布时间】:2011-09-17 20:46:17 【问题描述】:

我正在尝试实现将有向无环图转换为树的算法(为了好玩,学习,kata,命名它)。于是我想出了数据结构Node:

/// <summary>
/// Represeting a node in DAG or Tree
/// </summary>
/// <typeparam name="T">Value of the node</typeparam>
public class Node<T> 

    /// <summary>
    /// creats a node with no child nodes
    /// </summary>
    /// <param name="value">Value of the node</param>
    public Node(T value)
    
        Value = value;
        ChildNodes = new List<Node<T>>();
    

    /// <summary>
    /// Creates a node with given value and copy the collection of child nodes
    /// </summary>
    /// <param name="value">value of the node</param>
    /// <param name="childNodes">collection of child nodes</param>
    public Node(T value, IEnumerable<Node<T>> childNodes)
    
        if (childNodes == null)
        
            throw new ArgumentNullException("childNodes");
        
        ChildNodes = new List<Node<T>>(childNodes);
        Value = value;
    

    /// <summary>
    /// Determines if the node has any child node
    /// </summary>
    /// <returns>true if has any</returns>
    public bool HasChildNodes
    
        get  return this.ChildNodes.Count != 0; 
    


    /// <summary>
    /// Travearse the Graph recursively
    /// </summary>
    /// <param name="root">root node</param>
    /// <param name="visitor">visitor for each node</param>
    public void Traverse(Node<T> root, Action<Node<T>> visitor)
    
        if (root == null)
        
            throw new ArgumentNullException("root");
        
        if (visitor == null)
        
            throw new ArgumentNullException("visitor");
        

        visitor(root); 
        foreach (var node in root.ChildNodes)
        
            Traverse(node, visitor);
        
    

    /// <summary>
    /// Value of the node
    /// </summary>
    public T Value  get; private set; 

    /// <summary>
    /// List of all child nodes
    /// </summary>
    public List<Node<T>> ChildNodes  get; private set; 

这很简单。方法:

/// <summary>
/// Helper class for Node 
/// </summary>
/// <typeparam name="T">Value of a node</typeparam>
public static class NodeHelper

    /// <summary>
    /// Converts Directed Acyclic Graph to Tree data structure using recursion.
    /// </summary>
    /// <param name="root">root of DAG</param>
    /// <param name="seenNodes">keep track of child elements to find multiple connections (f.e. A connects with B and C and B also connects with C)</param>
    /// <returns>root node of the tree</returns>
    public static Node<T> DAG2TreeRec<T>(this Node<T> root, HashSet<Node<T>> seenNodes)
    
        if (root == null)
        
            throw new ArgumentNullException("root");
        
        if (seenNodes == null)
        
            throw new ArgumentNullException("seenNodes");
        

        var length = root.ChildNodes.Count;
        for (int i = 0; i < length; ++i)
        
            var node = root.ChildNodes[i];
            if (seenNodes.Contains(node))
            
                var nodeClone = new Node<T>(node.Value, node.ChildNodes);
                node = nodeClone;
            
            else
            
                seenNodes.Add(node);
            
            DAG2TreeRec(node, seenNodes);
        
        return root;
    
    /// <summary>
    /// Converts Directed Acyclic Graph to Tree data structure using explicite stack.
    /// </summary>
    /// <param name="root">root of DAG</param>
    /// <param name="seenNodes">keep track of child elements to find multiple connections (f.e. A connects with B and C and B also connects with C)</param>
    /// <returns>root node of the tree</returns>
    public static Node<T> DAG2Tree<T>(this Node<T> root, HashSet<Node<T>> seenNodes)
    
        if (root == null)
        
            throw new ArgumentNullException("root");
        
        if (seenNodes == null)
        
            throw new ArgumentNullException("seenNodes");
        

        var stack = new Stack<Node<T>>();
        stack.Push(root);

        while (stack.Count > 0) 
        
            var tempNode = stack.Pop();
            var length = tempNode.ChildNodes.Count;
            for (int i = 0; i < length; ++i)
            
                var node = tempNode.ChildNodes[i];
                if (seenNodes.Contains(node))
                
                    var nodeClone = new Node<T>(node.Value, node.ChildNodes);
                    node = nodeClone;
                
                else
                
                    seenNodes.Add(node);
                
               stack.Push(node);
            
         
        return root;
    

和测试:

    static void Main(string[] args)
    
        // Jitter preheat
        Dag2TreeTest();
        Dag2TreeRecTest();

        Console.WriteLine("Running time ");
        Dag2TreeTest();
        Dag2TreeRecTest();

        Console.ReadKey();
    

    public static void Dag2TreeTest()
    
        HashSet<Node<int>> hashSet = new HashSet<Node<int>>();

        Node<int> root = BulidDummyDAG();

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        var treeNode = root.DAG2Tree<int>(hashSet);
        stopwatch.Stop();

        Console.WriteLine(string.Format("Dag 2 Tree = 0ms",stopwatch.ElapsedMilliseconds));

    

    private static Node<int> BulidDummyDAG()
    
        Node<int> node2 = new Node<int>(2);
        Node<int> node4 = new Node<int>(4);
        Node<int> node3 = new Node<int>(3);
        Node<int> node5 = new Node<int>(5);
        Node<int> node6 = new Node<int>(6);
        Node<int> node7 = new Node<int>(7);
        Node<int> node8 = new Node<int>(8);
        Node<int> node9 = new Node<int>(9);
        Node<int> node10 = new Node<int>(10);
        Node<int> root  = new Node<int>(1);

        //making DAG                   
        root.ChildNodes.Add(node2);    
        root.ChildNodes.Add(node3);    
        node3.ChildNodes.Add(node2);   
        node3.ChildNodes.Add(node4);   
        root.ChildNodes.Add(node5);    
        node4.ChildNodes.Add(node6);   
        node4.ChildNodes.Add(node7);
        node5.ChildNodes.Add(node8);
        node2.ChildNodes.Add(node9);
        node9.ChildNodes.Add(node8);
        node9.ChildNodes.Add(node10);

        var length = 10000;
        Node<int> tempRoot = node10; 
        for (int i = 0; i < length; i++)
        
            var nextChildNode = new Node<int>(11 + i);
            tempRoot.ChildNodes.Add(nextChildNode);
            tempRoot = nextChildNode;
        

        return root;
    

    public static void Dag2TreeRecTest()
    
        HashSet<Node<int>> hashSet = new HashSet<Node<int>>();

        Node<int> root = BulidDummyDAG();

        Stopwatch stopwatch = new Stopwatch();
        stopwatch.Start();
        var treeNode = root.DAG2TreeRec<int>(hashSet);
        stopwatch.Stop();

        Console.WriteLine(string.Format("Dag 2 Tree Rec = 0ms",stopwatch.ElapsedMilliseconds));
    

另外,数据结构需要改进:

覆盖 GetHash、toString、Equals、== 运算符 实现 IComparable LinkedList 可能是更好的选择

此外,在转换之前,还有一些需要检查的事项:

多重图 如果是 DAG(循环) DAG 中的钻石 DAG 中的多个根

总而言之,它归结为几个问题: 如何提高转化率? 由于这是一个递归,因此可能会炸毁堆栈。我可以添加堆栈来记住它。如果我做 continuation-passing style,我会更有效率吗?

我觉得在这种情况下不可变数据结构会更好。对吗?

Childs 是正确的名字吗? :)

【问题讨论】:

在回答您的问题“Childs 是正确的名字吗?”时,Children 会是一个更好的名字,甚至是ChildNodes 100% 确定子节点在树中。图(各种)也有子节点? 在图论中你通常谈论顶点(vertexes)和边。顶点代表您所称的节点,边代表两个顶点之间的“链接”。 Children 更好,因为Childs 在英语中不存在。 一组直接连接的顶点的正确术语是Neighbors 你能写出你使用的算法的伪代码吗? 【参考方案1】:
    你最好发帖到CodeReview 孩子错了 => 孩子

    你不必使用 HashSet,你可以很容易地使用 List>,因为这里只检查引用就足够了。 (因此不需要 GetHashCode、Equals 和运算符覆盖)

    更简单的方法是序列化您的类,然后使用 XmlSerializer 再次将其反序列化为第二个对象。 在序列化和反序列化时,1 个对象被引用 2 次将成为 2 个具有不同引用的对象。

【讨论】:

+1 用于序列化然后反序列化到 XmlSerializer 中的第二个对象的想法。【参考方案2】:

算法:

正如您所观察到的,一些节点在输出中出现了两次。如果节点 2 有子节点,则整个子树将出现两次。如果您希望每个节点只出现一次,请替换

if (hashSet.Contains(node))

    var nodeClone = new Node<T>(node.Value, node.Childs);
    node = nodeClone;

if (hashSet.Contains(node))

    // node already seen -> do nothing

我不会太担心堆栈的大小或递归的性能。但是,您可以用Breadth-first-search 替换您的深度优先搜索,这将导致更接近根的节点被更早地访问,从而产生更“自然”的树(在您的图片中,您已经按 BFS 顺序对节点进行了编号)。

 var seenNodes = new HashSet<Node>();
 var q = new Queue<Node>();
 q.Enqueue(root);
 seenNodes.Add(root);   

 while (q.Count > 0) 
     var node = q.Dequeue();
     foreach (var child in node.Childs) 
         if (!seenNodes.Contains(child )) 
             seenNodes.Add(child);
             q.Enqueue(child);
     
 

算法处理菱形和循环。

多个根

只需声明一个包含所有顶点的类 Graph

class Graph

    public List<Node> Nodes  get; private set; 
    public Graph()
    
        Nodes = new List<Node>();
        


代码:

hashSet 可以命名为 seenNodes

代替

var length = root.Childs.Count;
for (int i = 0; i < length; ++i)

    var node = root.Childs[i];

foreach (var child in root.Childs)

在 Traverse 中,访问者是完全不必要的。您可能宁愿有一个方法来产生树的所有节点(以相同的顺序遍历),并且由用户对节点执行任何操作:

foreach(var node in root.TraverseRecursive())

    Console.WriteLine(node.Value);

如果重写 GetHashCode 和 Equals,算法将无法再区分具有相同值的两个不同节点,这可能不是您想要的。

我看不出 LinkedList 比 List 更好的任何理由,除了 List 在添加节点时所做的重新分配(容量 2、4、8、16、...)。

【讨论】:

1:我很不明白“什么都不做”。 1 -> 2 和 3 -> 2 之间仍然存在联系。 2:.Net 在递归解决方案方面做得很好,但我已经使用显式堆栈实现了另一个版本(更新后的帖子 - 广度优先搜索肯定更好)。 3:类 Graph 应该检查我是否没有添加另一个根”。4:同意 5:我不能,因为我正在改变集合。6:是的 7:是的 8:我也没有 @5 并且这个 for 循环对于 Jitter 的优化来说要快一些。还有一些 DAG 生成器非常适合测试。

以上是关于将有向无环图 (DAG) 转换为树的主要内容,如果未能解决你的问题,请参考以下文章

如何将有向无环图 (DAG) 存储为 JSON?

有向无环图描述表达式(C语言)

有向无环图

有向无环图——描述表达式

DAG(有向无环图)易懂介绍

为啥 HPX 要求未来的“那么”成为 DAG(有向无环图)的一部分?