Как разделить набор узлов на подмножества, каждый из которых образует направленный ациклический граф - PullRequest
0 голосов
/ 17 октября 2018

В проекте на C # у меня есть набор тестов, которые необходимо выполнить.Каждый тест имеет свою собственную коллекцию тестов, от которых он зависит.Сеть тестов необходима для формирования Направленного ациклического графа (DAG).

Используя обозначения A -> B -> C, где A, B, C представляют тесты, тогда

C зависитна B, B зависит от A.

У меня уже есть алгоритм, который упорядочивает тесты, чтобы я мог их обработать так, чтобы все зависимости были соблюдены.То есть, порядок означает, что зависимости каждого теста оцениваются до того, как сам тест оценивается для всего графика.

Я хотел бы иметь алгоритм, который сначала берет набор тестов и может разбивать их на отдельныеDAG-графики, если таковые имеются.Тесты в каждом DAG не нужно заказывать, так как это можно сделать отдельно.Причина этого заключается в том, что я мог запустить каждую независимую группу обеспечения доступности баз данных как отдельную задачу и таким образом добиться некоторой эффективности.

Итак, рассмотрим набор тестов 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 #, чтобы помочь с запросом к Эрику.

    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 голосов
/ 12 декабря 2018

Ответ 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;
}

Надеюсь, вы согласитесь, что это простой абстрактный тип данных.

Когда у нас есть мультидикционар, мы очень близки к тому, чтобы иметь ориентированный граф.Тем не менее, мы не можем использовать этот multidictionary как ориентированный граф, потому что он не представляет концепцию узла графа без исходящих ребер.Итак, давайте создадим простой тип ориентированного графа, который использует мультидикционар:

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!
}

Здесь есть несколько тонких моментов;Вы понимаете, почему я использовал ToList и Select?

ОК, теперь у нас есть ориентированный граф, чтобы представить наш граф зависимостей.Нам нужен неориентированный граф для нашего алгоритма.Но самый простой способ создать неориентированный граф - это создать ориентированный граф и просто добавить и удалить ребра попарно!

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();
}

Super.Чтобы упростить преобразование, давайте добавим вспомогательный метод к ориентированному графу:

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;
}
0 голосов
/ 24 октября 2018

В псевдокоде такой алгоритм может выглядеть примерно так:

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
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...