web-dev-qa-db-fra.com

Implémentation de Depth First Search dans C # à l'aide de List et Stack

Je veux créer une première recherche en profondeur dans laquelle j'ai eu un peu de succès.

Voici mon code jusqu'à présent (sauf mon constructeur, notez que les classes Vertex et Edge ne contiennent que des propriétés, rien d'important à publier ici):

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

Cela fonctionne de la manière dont tous les sommets sont visités, mais pas dans le bon ordre.

Voici une comparaison de la façon dont la mienne est visitée par rapport à wikipedia: Comparison

Il semble que le mien soit retourné et commence de droite à gauche.

Savez-vous ce qui le cause? (De plus, tout conseil sur ma mise en œuvre serait grandement apprécié)

Merci

EDIT: J'ai eu ma réponse, mais je voulais toujours montrer le résultat final pour la méthode 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;
}
27
Dumpen

Il semble que le mien soit retourné et commence de droite à gauche. Savez-vous ce qui le cause? 

Comme d'autres l'ont noté, vous poussez les nœuds à visiter sur la pile dans l'ordre, de gauche à droite. Cela signifie qu'ils sont éjectés de droite à gauche, car une pile inverse l'ordre. Les piles sont dernier entré, premier sorti.

Vous pouvez résoudre le problème en demandant à GetConnectedVertices de construire une pile et non une liste. De cette façon, les sommets connectés sont inversés deux fois, une fois quand ils vont sur la pile retournée et une fois quand ils vont sur la vraie pile.

De plus, tout conseil sur ma mise en œuvre serait grandement apprécié.

La mise en œuvre fonctionne, je suppose, mais elle pose de nombreux problèmes fondamentaux. Si on me présentait ce code pour révision, voici ce que je dirais:

Tout d'abord, supposons que vous souhaitiez effectuer deux recherches en profondeur d'abord sur cette structure de données en même temps. Soit parce que vous le faites sur plusieurs threads, soit parce que vous avez une boucle imbriquée dans laquelle la boucle interne effectue un DFS pour un élément différent de celui de la boucle externe. Ce qui se produit? Ils interfèrent les uns avec les autres car ils essaient tous deux de muter les champs "Etat" et "VisitNumber". C’est une très mauvaise idée d’avoir ce qui devrait être une opération «propre», telle que la recherche, qui rendrait votre structure de données «sale».

Cela rend également impossible l'utilisation de données immuables persistantes pour représenter des parties redondantes de votre graphique.

De plus, je remarque que vous omettez le code qui nettoie. Quand "Etat" revient-il à sa valeur initiale? Et si vous faisiez un DFS de seconde? Cela échouerait immédiatement puisque la racine est déjà visitée.

Un meilleur choix pour toutes ces raisons consiste à conserver l'état "visité" dans son propre objet, et non dans chaque sommet.

Ensuite, pourquoi tous les objets d’état sont-ils des variables privées d’une classe? Ceci est un algorithme simple; il n'y a pas besoin de construire une classe entière pour cela. Un algorithme de recherche en profondeur d'abord devrait prendre le graphe pour effectuer une recherche en tant que paramètre formel, et non en tant qu'état d'objet, et il devrait conserver son propre état local, le cas échéant, dans les variables locales et non dans les champs.

Ensuite, l'abstraction du graphique est ... eh bien, ce n'est pas une abstraction. C'est deux listes, une de vertices et une d'arêtes. Comment savons-nous que ces deux listes sont même cohérentes? Supposons que certains sommets ne figurent pas dans la liste des sommets mais figurent dans la liste des arêtes. Comment empêchez-vous cela? Ce que vous voulez, c'est une abstraction graphique. Laissez l'implémentation d'abstraction de graphe se préoccuper de la manière de représenter les arêtes et de trouver des voisins.

Ensuite, votre utilisation de ForEach est à la fois légale et courante, mais cela me fait mal à la tête. Il est difficile de lire votre code et de raisonner à ce sujet avec tous les lambdas. Nous avons une excellente déclaration "foreach". Utilise le.

Ensuite, vous muez une propriété "parent" mais il n’est pas du tout clair à quoi sert cette propriété ni pourquoi elle est mutée au cours d’une traversée. Les sommets d'un graphe arbitraire n'ont pas de "parents" sauf si le graphe est un arbre. Si le graphe est un arbre, il n'est pas nécessaire de garder trace de l'état "visité"; il n'y a pas de boucles dans un arbre. Qu'est-ce qui se passe ici? Ce code est simplement bizarre et il n’est pas nécessaire d’effectuer une DFS.

Ensuite, votre méthode d'assistance nommée GetConnectedVertices est un mensonge. Il ne reçoit pas les sommets connectés, mais les sommets connectés qui ne sont pas déjà visités. Les méthodes dont les noms mentent sont très déroutantes.

Enfin, cela se prétend être une première recherche en profondeur mais il ne cherche rien! Où est la chose recherchée? Où est le résultat retourné? Ce n'est pas une recherche du tout, c'est un parcours.

Recommencer. Qu'est-ce que tu veux? Une première traversée en profondeur d'un graphe étant donné un sommet de départ . Alors implémentez ça. Commencez par définir ce que vous traversez. Un graphique. De quel service avez-vous besoin d'un graphique? Une façon d'obtenir l'ensemble des sommets voisins:

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

Quelle est votre méthode de retour? Une séquence de sommets en profondeur-premier ordre. Qu'est-ce que ça prend? Un sommet de départ. D'ACCORD:

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

Nous avons maintenant une implémentation triviale de la recherche en profondeur d'abord; vous pouvez maintenant utiliser la clause Where:

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

OK, alors comment allons-nous implémenter cette méthode pour qu’elle effectue une traversée sans détruire l’état du graphe? Maintenir votre propre état externe:

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

Vous voyez combien c'est plus propre et plus court? Aucune mutation d'état. Pas de bricolage avec les listes Edge. Aucune fonction d'assistance mal nommée. Et le code fait ce qu'il dit: traverse un graphique. 

Nous bénéficions également des avantages des blocs d'itérateurs; à savoir, si quelqu'un l'utilise pour une recherche DF, l'itération est abandonnée lorsque les critères de recherche sont remplis. Nous n'avons pas à faire un parcours complet si nous trouvons le résultat plus tôt.

52
Eric Lippert

J'ai généralisé le code de @ Eric pour la traversée DFS pour toute T afin que tout fonctionne avec tout type d'enfants ayant des enfants - je pensais partager

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

Exemple d'utilisation:

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

Le problème réside dans l'ordre dans lequel vous recherchez les éléments. Votre for each dans StartSearch ne garantit pas l'ordre des éléments. FindAll dans la méthode GetConnectedVertices non plus. Regardons cette ligne:

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

Vous devez ajouter un OrderBy() pour assurer l'ordre souhaité.

1
Adrian Carneiro

Ceci est mon implémentation, de la pile est assez bon. Une inversion est faite avant la boucle 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;
    }
0
Lepton

Les éléments seront sortis de la pile dans l'ordre inverse de la manière dont ils sont poussés dessus:

stach.Push () résulte en: 1 2 3 4 5

stack.pop () résulte en: 5 4 3 2 1 (donc: de droite à gauche)

0
Flawless

Vous pourriez apprécier ceci:

        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;
    }
0
Evan Machusak