web-dev-qa-db-ja.com

リストとスタックを使用して深さ優先検索をC#に実装する

ある程度成功している深さ優先検索を作成したいと思います。

これまでのコードは次のとおりです(コンストラクタを除いて、VertexクラスとEdgeクラスにはプロパティのみが含まれているため、ここに投稿することは重要ではありません)。

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

その鉱山は振り向いていて、右から左に始まっているようです。

何が原因か知っていますか? (また、私の実装に関するアドバイスもいただければ幸いです)

編集:私は私の答えを得ましたが、それでも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;
}
28
Dumpen

私の体は振り向いて、右から左に始まっているようです。何が原因か知っていますか?

他の人が指摘しているように、あなたは左から右への順序でスタックの次のノードをスタックにプッシュしています。これは、スタックが順序を逆にするため、右から左に飛び出すことを意味します。スタックは後入れ先出しです。

GetConnectedVerticesにリストではなくスタックを構築させることで問題を修正できます。このようにして、接続された頂点がtwice逆にされます。

また、私の実装に関するアドバイスは大歓迎です

実装は機能していると思いますが、根本的な問題が数多くあります。レビューのためにそのコードが提示された場合、ここに私が言うことは次のとおりです。

まず、このデータ構造の2つの深さ優先検索を同時に実行したいとします。複数のスレッドで実行していたか、内側のループが外側のループとは異なる要素に対してDFSを実行する入れ子になったループがあるためです。何が起こるのですか? 「State」フィールドと「VisitNumber」フィールドの両方を変更しようとするため、これらは互いに干渉します。検索のような「クリーン」な操作を実際に行うことは、実際にデータ構造を「ダーティ」にするのは非常に悪い考えです。

これを行うと、永続的な不変データを使用してグラフの冗長部分を表すことができなくなります。

また、クリーンアップするコードを省略していることにも気づきました。 「状態」が元の値に戻るのはいつですか? DFSを実行した場合はどうなりますか?ルートはすでにアクセスされているため、すぐに失敗します。

これらすべての理由から、より良い選択は、各頂点ではなく、独自のオブジェクトで「visited」状態を維持することです。

次に、すべての状態オブジェクトがクラスのプライベート変数であるのはなぜですか?これは単純なアルゴリズムです。クラス全体を作成する必要はありません。深さ優先検索アルゴリズムは、グラフをオブジェクトの状態としてではなく、仮パラメーターとして検索し、必要に応じて、フィールドではなくローカル変数で独自のローカル状態を維持する必要があります。

次に、グラフの抽象化は...まあ、それは抽象化ではありません。これは、1つの頂点と1つのエッジの2つのリストです。これら2つのリストが一貫していることをどのようにして知ることができますか?頂点リストにはないが、エッジリストにはある頂点があるとします。それをどうやって防ぐのですか?必要なのはグラフの抽象化です。グラフ抽象化の実装では、エッジを表現して近傍を見つける方法を心配します。

次に、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();

では、グラフの状態を壊すことなくトラバーサルを行うために、このメソッドをどのように実装するのでしょうか。独自の外部状態を維持します。

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検索に使用している場合、検索基準が満たされると反復が中止されます。結果が早期に見つかった場合、全探索を行う必要はありません。

54
Eric Lippert

私は@Ericのコードを一般化して、すべてのTのDFSトラバーサルを行い、子を持つすべてのタイプで機能するようにしました。共有したいと思いました。

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);
4
Adi Lester

問題は、要素を検索する順序にあります。 StartSearchの_for each_は、要素の順序を保証しません。 FindAllメソッドのGetConnectedVerticesも行いません。この行を見てみましょう:

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

OrderBy()を追加して、目的の順序を確保する必要があります。

1
Adrian Carneiro

これは私の実装であり、スタックは十分です。 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

アイテムは、プッシュされたときとは逆の順序でスタックからポップされます。

stach.Push()の結果:1 2 3 4 5

stack.pop()の結果:5 4 3 2 1(つまり:右から左)

0
Flawless

あなたはこれを楽しむかもしれません:

        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