如何将一组节点划分为每个子集,每个子​​集形成一个有向无环图

时间:2018-10-17 08:50:20

标签: c# graph-theory directed-acyclic-graphs

在C#项目中,我要执行一组测试。每个测试都有自己依赖的测试集合。需要测试网络才能形成有向无环图(DAG)。

使用符号A-> B-> C,其中A,B,C代表测试,然后

C取决于B, B取决于A。

我已经有一个算法来对测试进行排序,以便我可以按顺序处理它们,从而尊重所有依赖性。也就是说,顺序意味着在评估整个图本身之前先评估每个测试的依存关系。

我想拥有一种算法,该算法首先进行一组测试,然后将它们划分为单独的DAG图(如果存在)。不需要单独订购每个DAG中的测试。这样做的原因是,我可以将每个独立的DAG作为单独的Task运行,并以此方式获得一定的效率。

因此,请考虑依赖项为的测试集A,B,C,D,E,F:

A -> B -> C

D -> C

E -> F

从算法中,我需要2组测试,

Set 1) A,B,C,D

Set 2) E,F

更新:C#代码可帮助请求Eric。

    public class Graph
{
    private List<Node> _nodes = new List<Node>();

    public IReadOnlyList<Node> Nodes => _nodes;

    public void AddNode(Node node)
    {
        _nodes.Add(node);
    }

    public void RemoveRange(IEnumerable<Node> nodes)
    {
        foreach (var item in nodes)
        {
            _nodes.Remove(item);
        }
    }
}

public class Node
{
    public Node(string name)
    {
        Name = name;
    }

    private List<Node> _dependants = new List<Node>();

    public string Name { get; private set; }

    public IReadOnlyList<Node> Dependents => _dependants;

    public void AddDependent(Node node)
    {
        _dependants.Add(node);
    }
}

public class Set
{
    private List<Node> _elements = new List<Node>();

    public void AddRange(IEnumerable<Node> nodes)
    {
        _elements = new List<Node>(nodes);
    }

    public IReadOnlyList<Node> Elements => _elements;
}

internal class Program
{
    private static void Main(string[] args)
    {
        List<Set> sets = new List<Set>();

        var graph = new Graph();

        var a = new Node("A");
        var b = new Node("B");
        var c = new Node("C");
        var d = new Node("D");
        var e = new Node("E");
        var f = new Node("F");

        graph.AddNode(a);
        graph.AddNode(b);
        graph.AddNode(c);
        graph.AddNode(d);
        graph.AddNode(e);
        graph.AddNode(f);

        c.AddDependent(b);
        b.AddDependent(a);
        c.AddDependent(d);
        f.AddDependent(e);

        while (graph.Nodes.Count > 0)
        {
            var set = new Set();

            var pickNode = graph.Nodes[0];

            // Get reachable nodes
            // 1. NOT SURE WHAT YOU MEAN HERE AND HOW TO DO THIS IN C# 
            // 2. ALSO, DOES THE SET INCLUDE THE PICKED NODE?
        }
    }
}

更新2:

用于对节点进行排序的代码示例

    private enum MarkType
    {
        None,
        Permanent,
        Temporary
    }

    private static IEnumerable<T> GetSortedNodes<T>(DirectedGraph<T> directedGraph)
    {
        List<T> L = new List<T>();

        var allNodes = directedGraph.Nodes();

        Dictionary<T, (MarkType, T)> nodePairDictionary = allNodes.ToDictionary(n => n, n => (MarkType.None, n));

        foreach (var node in allNodes)
        {
            var nodePair = nodePairDictionary[node];
            Visit(nodePair);
        }

        return L.Reverse<T>().ToList();

        void Visit((MarkType markType, T node) nodePair)
        {

            if (nodePair.markType == MarkType.Permanent)
            {
                return;
            }

            if (nodePair.markType == MarkType.Temporary)
            {
                throw new Exception("NOT A DAG");
            }

            nodePair.markType = MarkType.Temporary;

            foreach (var dependentNode in directedGraph.Edges(nodePair.node))
            {
                var depNodePair = nodePairDictionary[dependentNode];
                Visit(depNodePair);
            }

            nodePair.markType = MarkType.Permanent;

            L.Insert(0, nodePair.node);
        }

    }

2 个答案:

答案 0 :(得分:4)

pniederh的答案给出了联合查找算法的版本(有些过于复杂);正如我在评论中指出的那样,关于如何使这些算法高效进行了大量研究。

在您的特定情况下,另一种有效的算法是这样的:

  • 创建代表无向图的类型。
  • 在图中添加代表每个任务的节点。
  • 在图上添加一条边-记住,它是无向的-表示每个依赖项。
  • 创建一组列表。
  • 图中是否有任何节点?如果否,那么您就完成了,结果就是集合列表。
  • 从图中选择任何节点。
  • 在该节点上运行图遍历并累积可访问节点的集合。
  • 将其粘贴到列表中。
  • 从图中删除该集合中的所有节点。
  • 返回检查图中的节点。

完成后,您将看到一组集合,其中每个集合都包含彼此具有直接或间接依赖关系的任务,并且每个任务都恰好出现在一个集合中。这是由对称依赖项对等关系引入的对等类的集合。


更新:还有一些关于如何实现此问题的问题。

这是一个简单但不是特别有效的实现。这里的想法是从更简单的数据结构构建越来越多的复杂数据结构。

我想要的第一件事是一本多词典的书。普通字典从键映射到值。我想要一个从键到一组值的映射。我们可以通过NuGet下载任何数量的实现,但是编写我们自己的准系统实现是快速简便的:

public class MultiDictionary<K, V>
{
    private readonly Dictionary<K, HashSet<V>> d = new Dictionary<K, HashSet<V>>();
    public void Add(K k, V v)
    {
        if (!d.ContainsKey(k)) d.Add(k, new HashSet<V>());
        d[k].Add(v);
    }
    public void Remove(K k, V v)
    {
        if (d.ContainsKey(k))
        {
            d[k].Remove(v);
            if (d[k].Count == 0) d.Remove(k);
        }
    }
    public void Remove(K k) => d.Remove(k);
    public IEnumerable<V> GetValues(K k) => d.ContainsKey(k) ? d[k] : Enumerable.Empty<V>();
    public IEnumerable<K> GetKeys() => d.Keys;
}

我希望你同意这是一个简单的抽象数据类型。

一旦我们有了一个多维字典,我们将非常接近有向图。但是,我们不能将此多字典用作有向图,因为它不代表没有输出边的图节点的概念。因此,让我们构建一个使用多字典的简单有向图类型:

public class DirectedGraph<T>
{
    private readonly HashSet<T> nodes = new HashSet<T>();
    private readonly MultiDictionary<T, T> edges = new MultiDictionary<T, T>();
    public void AddNode(T node) => nodes.Add(node);
    public void AddEdge(T n1, T n2)
    {
        AddNode(n1);
        AddNode(n2);
        edges.Add(n1, n2);
    }
    public void RemoveEdge(T n1, T n2) => edges.Remove(n1, n2);
    public void RemoveNode(T n)
    {
        // TODO: This algorithm is very inefficient if the graph is
        // TODO: large; can you think of ways to improve it?
        // Remove the incoming edges
        foreach (T n1 in nodes)
            RemoveEdge(n1, n);       
        // Remove the outgoing edges
        foreach (T n2 in edges.GetValues(n).ToList())
            RemoveEdge(n, n2);
        // The node is now isolated; remove it.
        nodes.Remove(n);
    }
    public IEnumerable<T> Edges(T n) => edges.GetValues(n);
    public IEnumerable<T> Nodes() => nodes.Select(x => x);
    public HashSet<T> ReachableNodes(T n) { ??? }
    // We'll come back to this one!
}

这里有一些细微之处;看到我为什么使用ToListSelect了吗?

好的,我们现在有了一个有向图,用来表示我们的依赖图。我们的算法需要一个无向图。但是,制作无向图的最简单方法是制作有向图,然后成对添加和删除边!

public class UndirectedGraph<T>
{
    private readonly DirectedGraph<T> g = new DirectedGraph<T>();
    public void AddNode(T node) => g.AddNode(node);
    public void AddEdge(T n1, T n2)
    {
        g.AddEdge(n1, n2);
        g.AddEdge(n2, n1);
    }
    public void RemoveEdge(T n1, T n2)
    {
        g.RemoveEdge(n1, n2);
        g.RemoveEdge(n2, n1);
    }
    public void RemoveNode(T n) => g.RemoveNode(n);
    public IEnumerable<T> Edges(T n) => g.Edges(n);
    public IEnumerable<T> Nodes() => g.Nodes();
}

超级。为了使转换更容易,让我们向有向图添加一个辅助方法:

public UndirectedGraph<T> ToUndirected()
{
    var u = new UndirectedGraph<T>();
    foreach (T n1 in nodes)
    {
        u.AddNode(n1);
        foreach (T n2 in Edges(n1))
            u.AddEdge(n1, n2);
    }
    return u;
}

现在,我们算法的关键是在给定节点的情况下获取可到达的节点集的能力。我希望您同意到目前为止的一切都很简单。这很棘手:

public HashSet<T> ReachableNodes(T n)
{
    var reachable = new HashSet<T>();
    if (nodes.Contains(n))
    {
        var stack = new Stack<T>();
        stack.Push(n);
        while (stack.Count > 0)
        {
            var current = stack.Pop();
            if (!reachable.Contains(current))
            {
                reachable.Add(current);
                foreach (T n2 in Edges(current))
                    stack.Push(n2);
            }
        }
    }
    return reachable;
}

这是有向图的深度优先遍历,可检测循环并返回给定节点的传递闭包。 请仔细研究此算法,因为它是理解的关键

我们将为我们的无向图添加一个辅助方法:

public HashSet<T> ReachableNodes(T n) => g.ReachableNodes(n);

现在,我们拥有制作算法所需的所有部分。我们只是将我直接给出的算法描述转换为代码:

static IEnumerable<HashSet<T>> GetEquivalenceClasses<T>(DirectedGraph<T> d)
{
    var u = d.ToUndirected();
    var results = new List<HashSet<T>>();
    while (u.Nodes().Any())
    {
        T current = u.Nodes().First();
        HashSet<T> reachable = u.ReachableNodes(current);
        results.Add(reachable);
        foreach (T n in reachable)
            u.RemoveNode(n);
    }
    return results;
}

让我们一起旋转吧

    var d = new DirectedGraph<string>();
    d.AddEdge("A", "B");
    d.AddEdge("B", "C");
    d.AddEdge("D", "C");
    d.AddEdge("E", "F");
    foreach (var eq in GetEquivalenceClasses(d))
        Console.WriteLine(string.Join(",", eq));

果然:

A,B,C,D
E,F

有道理吗?


更新:删除节点是昂贵的部分,我刚刚意识到,我们不需要这样做。该算法的非破坏性版本是:

static IEnumerable<HashSet<T>> GetEquivalenceClasses<T>(DirectedGraph<T> d)
{
    var u = d.ToUndirected();
    var results = new List<HashSet<T>>();
    var done = new HashSet<T>();
    foreach(T current in u.Nodes())
    {
        if (done.Contains(current))
          continue;
        HashSet<T> reachable = u.ReachableNodes(current);
        results.Add(reachable);
        foreach(T n in reachable)
          done.Add(n);
    }
    return results;
}

答案 1 :(得分:1)

在伪代码中,这样的算法可能看起来像这样:

Create a list of buckets
foreach (Node n in Nodes)
{
    Find a bucket that contains n, or create a new bucket for it.
    foreach (Node dependentNode in n.DependentNodes)
    {
        if (dependentNode is in any bucket)
        {
            move n and its dependencies to that bucket;
        }
        else
        {
            add depenentNode to the same bucket as N;
        }
    }
}

遍历所有节点之后,存储桶现在应该表示不相关的不同集合。

注意:我强烈怀疑这不是最有效的算法。但是对于有限数量的节点,这应该足够了。

和往常一样,我建议提供大量的单元测试以确保正确性,并在性能问题时进行概要分析。

以下是作为示例的单元测试的最小实现:

[TestFixture]
public class PartitionTests
{
    public class Node
    {
        private List<Node> subNodes = new List<Node>();

        public Node(string name)
        {
            this.Name = name;
        }
        public IEnumerable<Node> DependentNodes { get { return this.subNodes; } }

        public string Name { get; }

        internal void AddDependentNode(Node subNode)
        {
            subNodes.Add(subNode);
        }
        public override string ToString()
        {
            //just to make debugging easier in this example
            return this.Name;
        }
    }

    [Test]
    public void PartitionTest1()
    {
        #region prepare
        Node A = new Node("A");
        Node B = new Node("B");
        Node C = new Node("C");
        Node D = new Node("D");
        Node E = new Node("E");
        Node F = new Node("F");

        A.AddDependentNode(B);
        B.AddDependentNode(C);
        D.AddDependentNode(C);
        E.AddDependentNode(F);

        var allNodes = new List<Node>() { A, B, C, D, E, F };
        #endregion

        #region Implementation
        var buckets = new List<List<Node>>();
        foreach (var n in allNodes)
        {
            var existingBucket = buckets.FirstOrDefault(b => b.Contains(n));
            if (existingBucket == null)
            {
                existingBucket = new List<Node>() { n };
            }
            foreach (var dependentNode in n.DependentNodes)
            {
                var otherBucket = buckets.FirstOrDefault(b => b.Contains(dependentNode));
                if (otherBucket == null)
                {
                    existingBucket.Add(dependentNode);
                }
                else
                {
                    existingBucket.Remove(n);
                    otherBucket.Add(n);
                    foreach (var alreadyPlacedNode in existingBucket)
                    {
                        existingBucket.Remove(alreadyPlacedNode);
                        if (!otherBucket.Contains(alreadyPlacedNode))
                        {
                            otherBucket.Add(alreadyPlacedNode);
                        }
                    }
                }
            }
            if (!buckets.Contains(existingBucket) && existingBucket.Any())
            {
                buckets.Add(existingBucket);
            }
        }
        #endregion

        #region test
        Assert.AreEqual(2, buckets.Count, "Expect two buckets");
        Assert.AreEqual(4, buckets[0].Count);  //we should not rely on the order of buckets here
        Assert.AreEqual(2, buckets[1].Count);
        CollectionAssert.Contains(buckets[0], A);
        CollectionAssert.Contains(buckets[0], B);
        CollectionAssert.Contains(buckets[0], C);
        CollectionAssert.Contains(buckets[0], D);
        CollectionAssert.Contains(buckets[1], E);
        CollectionAssert.Contains(buckets[1], F);
        #endregion
    }
}
相关问题