Реализация первого поиска глубины в C# с использованием списка и стека

Я хочу создать первый поиск глубины, в котором я был несколько успешным.

вот мой код до сих пор (кроме моего конструктора, обратите внимание, что классы вершин и ребер содержат только свойства, ничего важного для публикации здесь):

private Stack<Vertex> workerStack = new Stack<Vertex>();
private List<Vertex> vertices = new List<Vertex>();
private List<Edge> edges = new List<Edge>();

private int numberOfVertices;
private int numberOfClosedVertices;
private int visitNumber = 1;

private void StartSearch()
{
    // Make sure to visit all vertices
    while (numberOfClosedVertices < numberOfVertices && workerStack.Count > 0)
    {
        // Get top element in stack and mark it as visited
        Vertex workingVertex = workerStack.Pop();
        workingVertex.State = State.Visited;

        workingVertex.VisitNumber = visitNumber;
        visitNumber++;

        numberOfClosedVertices++;

        // Get all edges connected to the working vertex
        foreach (Vertex vertex in GetConnectedVertices(workingVertex))
        {
            vertex.Parent = workingVertex;
            workerStack.Push(vertex);
        }
    }
}

private List<Vertex> GetConnectedVertices(Vertex vertex)
{
    List<Vertex> vertices = new List<Vertex>();

    // Get all vertices connected to vertex and is unvisited, then add them to the vertices list
    edges.FindAll(edge => edge.VertexSource == vertex && edge.VertexTarget.State == State.Unvisited).ForEach(edge => vertices.Add(edge.VertexTarget));

    return vertices;
}

он работает так, что все вершины посещают, но не в правильном порядке.

вот сравнение того, как мой посещается по сравнению с Википедией: Comparison

Кажется, что мой повернут вокруг и справа налево.

вы знаете, что вызывает его? (Также любые советы по моей реализации были бы очень признательны)

спасибо

EDIT: я получил свой ответ, но все еще хотел показать конечный результат для метода GetConnectedVertices:

private List<Vertex> GetConnectedVertices(Vertex vertex)
{
    List<Vertex> connectingVertices = new List<Vertex>();

    (from edge in edges
     where edge.VertexSource == vertex && edge.VertexTarget.State == State.Unvisited
     select edge).
     Reverse().
     ToList().
     ForEach(edge => connectingVertices.Add(edge.VertexTarget));

    return connectingVertices;
}

6 ответов


кажется, мой повернут и начинается справа налево. Вы знаете, что его вызывает?

как отмечали другие, вы нажимаете узлы-для-посещения-далее в стеке слева направо. Это означает, что они снимаются справа налево, так как стек меняет порядок. Стеки последний-в-первого-вне.

вы можете исправить проблему, заставив GetConnectedVertices построить стек, а не список. Таким образом, связанные вершины обратный два раза, один раз, когда они идут на возвращенный стек и один раз, когда они идут на реальный стек.

также любые советы по моей реализации были бы очень признательны

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

во-первых, предположим, что вы хотите выполнить два глубоких поиска этой структуры данных одновременно время. Либо потому, что вы делали это в нескольких потоках, либо потому, что у вас есть вложенный цикл, в котором внутренний цикл делает DFS для другого элемента, чем внешний цикл. Что происходит? Они мешают друг другу, потому что оба пытаются мутировать поля "состояние" и "VisitNumber". Это действительно плохая идея, чтобы иметь то, что должно быть "чистой" операцией, как поиск на самом деле сделать вашу структуру данных "грязной".

Это также делает невозможным использование постоянные неизменяемые данные для представления избыточных частей графика.

кроме того, я заметил, что вы опускаете код, который очищает. Когда "состояние" когда-либо возвращается к своему исходному значению? Что делать, если вы сделали второй DFS? Он немедленно потерпит неудачу, так как корень уже посещен.

лучшим выбором по всем этим причинам является сохранение" посещенного " состояния в своем собственном объекте, а не в каждой вершине.

далее, Почему все государственные объекты, частные переменные класса? Это простой алгоритм; нет необходимости создавать для него целый класс. Алгоритм поиска глубины сначала должен принимать график для поиска как формальный параметр, а не как состояние объекта, и он должен поддерживать свое собственное локальное состояние по мере необходимости в локальных переменных, а не полях.

далее-абстракция, графика... ну, это не абстракция. Это два списка, один из вершин и один из краев. Откуда мы знаем, что эти два списка вообще непротиворечивы? Предполагать есть вершины, которые не находятся в списке вершин, но находятся в списке ребер. Как это предотвратить? Вам нужна абстракция графа. Пусть реализация абстракции графа беспокоится о том, как представить ребра и найти соседей.

далее, ваше использование ForEach является законным и распространенным, но это заставляет мою голову болеть. Трудно читать ваш код и рассуждать об этом со всеми лямбдами. У нас есть совершенно хорошее заявление "foreach". Использовать его.

Далее, вы мутируете "родительское" свойство, но совсем не ясно, для чего это свойство и почему оно мутирует во время обхода. Вершины в произвольном графе не имеют "родителей", если граф не является деревом, а если граф является деревом, то нет необходимости отслеживать состояние" посещено"; в дереве нет петель. Что здесь происходит? Этот код просто странный, и нет необходимости выполнять DFS.

далее, ваш вспомогательный метод с именем GetConnectedVertices является ложью. Это не подключайтесь вершины, она подключается не уже посещенных вершин. Методы, названия которых лгут, очень запутанны.

наконец, это утверждает, что первый поиск глубины, но он ничего не ищет! где вещь, которую ищут? Где результат возвращается? Это вовсе не обыск, это обход.

начать сначала. Что тебе надо? глубина-первый обход графа с учетом начальной вершины. Затем реализовать что. Начните с определения того, что вы проходите. Диаграмма. Какая услуга вам нужна от графика? Способ получения множества соседних вершин:

interface IGraph
{
    IEnumerable<Vertex> GetNeighbours(Vertex v);
}

что ваш метод возвращения? Последовательность вершин по глубине-первого порядка. Что для этого нужно? Начальная вершина. OK:

static class Extensions
{
    public static IEnumerable<Vertex> DepthFirstTraversal(
        this IGraph graph, 
        Vertex start) 
    { ... }
}

теперь у нас есть тривиальная реализация первого поиска глубины; теперь вы можете использовать предложение Where:

IGraph myGraph = whatever;
Vertex start = whatever;
Vertex result = myGraph.DepthFirstTraversal(start)
                       .Where(v=>something)
                       .FirstOrDefault();

OK, так как мы собираемся реализовать этот метод, чтобы он делает ли обход без разрушения состояния графика? Поддерживайте свое собственное внешнее состояние:

public static IEnumerable<Vertex> DepthFirstTraversal(
    this IGraph graph, 
    Vertex start) 
{
    var visited = new HashSet<Vertex>();
    var stack = new Stack<Vertex>();

    stack.Push(start);

    while(stack.Count != 0)
    {
        var current = stack.Pop();

        if(!visited.Add(current))
            continue;

        yield return current;

        var neighbours = graph.GetNeighbours(current)
                              .Where(n=>!visited.Contains(n));

        // If you don't care about the left-to-right order, remove the Reverse
        foreach(var neighbour in neighbours.Reverse()) 
            stack.Push(neighbour);
    }
}

видите, насколько это чище и короче? Никакой мутации состояния. Не возиться со списками edge. Нет плохо названных вспомогательных функций. И код действительно делает то, что он говорит: пересекает график.

мы также получаем преимущества блоков итераторов; а именно, если кто-то использует это для поиска DF, то итерация отбрасывается при выполнении критериев поиска. Мы не нужно делать полный обход, если мы найдем результат раньше.


я обобщил код @Eric для обхода DFS для любого T чтобы все работало для любого типа, у которого есть дети - я думал, что поделюсь:

public static IEnumerable<T> DepthFirstTraversal<T>(
    T start,
    Func<T, IEnumerable<T>> getNeighbours)
{
    var visited = new HashSet<T>();
    var stack = new Stack<T>();
    stack.Push(start);

    while (stack.Count != 0)
    {
        var current = stack.Pop();
        visited.Add(current);
        yield return current;

        var neighbours = getNeighbours(current).Where(node => !visited.Contains(node));

        // If you don't care about the left-to-right order, remove the Reverse
        foreach(var neighbour in neighbours.Reverse())
        {
            stack.Push(neighbour);
        }
    }
}

пример использования:

var nodes = DepthFirstTraversal(myNode, n => n.Neighbours);

проблема заключается в порядке поиска элементов. Ваш for each на StartSearch не гарантирует порядок элементов. Не вы FindAll на GetConnectedVertices метод. Давайте посмотрим на эту строку:

edges.FindAll(edge => edge.VertexSource == vertex && edge.VertexTarget.State == State.Unvisited).ForEach(edge => vertices.Add(edge.VertexTarget));

вы должны добавить OrderBy() для того чтобы обеспечить нужный порядок.


элементы будут выскочили из стека в обратном порядке, как они нажимаются на него:

Стаха.метод push() приводит к: 1 2 3 4 5

стек.поп() результаты: 5 4 3 2 1 (так: справа налево)


вам может понравиться это:

        public static bool DepthFirstSearch<T>(this IEnumerable<T> vertices, T rootVertex, T targetVertex, Func<T, IEnumerable<T>> getConnectedVertices, Func<T, T, bool> matchFunction = null)
    {
        if (getConnectedVertices == null)
        {
            throw new ArgumentNullException("getConnectedVertices");
        }
        if (matchFunction == null)
        {
            matchFunction = (t, u) => object.Equals(t, u);
        }
        var directlyConnectedVertices = getConnectedVertices(rootVertex);
        foreach (var vertex in directlyConnectedVertices)
        {
            if (matchFunction(vertex, targetVertex))
            {
                return true;
            }
            else if (vertices.DepthFirstSearch(vertex, targetVertex, getConnectedVertices, matchFunction))
            {
                return true;
            }
        }
        return false;
    }

Это моя имплементация, один стек достаточно хорош. Перед циклом foreach выполняется реверс.

    /// <summary>
    /// Depth first search implementation in c#
    /// </summary>
    /// <typeparam name="T">Type of tree structure item</typeparam>
    /// <typeparam name="TChilds">Type of childs collection</typeparam>
    /// <param name="node">Starting node to search</param>
    /// <param name="ChildsProperty">Property to return child node</param>
    /// <param name="Match">Predicate for matching</param>
    /// <returns>The instance of matched result, null if not found</returns>
    public static T DepthFirstSearch<T, TChilds>(this T node, Func<T, TChilds> ChildsProperty, Predicate<T> Match) 
        where T:class
    {
        if (!(ChildsProperty(node) is IEnumerable<T>))
            throw new ArgumentException("ChildsProperty must be IEnumerable<T>");

        Stack<T> stack = new Stack<T>();
        stack.Push(node);
        while (stack.Count > 0) {
            T thisNode = stack.Pop();
            #if DEBUG
            System.Diagnostics.Debug.WriteLine(thisNode.ToString());
            #endif
            if (Match(thisNode))
                return thisNode;
            if (ChildsProperty(thisNode) != null) {
                foreach (T child in (ChildsProperty(thisNode) as IEnumerable<T>).Reverse()) 
                    stack.Push(child);
            }
        }
        return null;
    }