web-dev-qa-db-ja.com

非再帰的アプローチでグラフの深さ優先検索を実装する方法

さて、私はこの問題に多くの時間を費やしました。ただし、ツリーの非再帰的メソッド ツリーの非再帰的 、またはグラフの再帰的メソッド グラフの再帰的 のソリューションのみを見つけることができます。

そして、多くのチュートリアル(ここではそれらのリンクを提供しません)はアプローチも提供しません。または、チュートリアルがまったく間違っています。私を助けてください。

更新:

説明するのは本当に難しいです:

無向グラフがある場合:

               1
             / |  \
            4  |   2
               3 /

1-- 2-- 3 --1はサイクルです。

ステップ:Push the neighbors of the popped vertex into the stack

WHAT'S THE ORDER OF THE VERTEXES SHOULD BE PUSHED?

プッシュされた順序が2 4 3の場合、スタックの頂点は次のとおりです。

| |
|3|
|4|
|2|    
 _

ノードをポップすると、1-> 3-> 2-> 4ではなく、1-> 3-> 4-> 2という結果が得られました。

それは間違っています。このシナリオを停止するために追加すべき条件は何ですか?

31
Alston

再帰のないDFSは基本的に [〜#〜] bfs [〜#〜] と同じですが、データ構造としてキューの代わりに stack を使用します。

スレッド 反復DFSと再帰DFSおよび異なる要素の順序 両方のアプローチとそれらの違いを処理します(そして、同じ順序でノードをトラバースしません!)

反復アプローチのアルゴリズムは基本的に次のとおりです。

DFS(source):
  s <- new stack
  visited <- {} // empty set
  s.Push(source)
  while (s is not empty):
    current <- s.pop()
    if (current is in visited):
        continue
    visited.add(current)
    // do something with current
    for each node v such that (current,v) is an Edge:
        s.Push(v)
45
amit

これは答えではなく、質問の現在のバージョンのグラフに対する@amitの答えのアルゴリズムの適用を示す拡張コメントです。1が開始ノードで、その隣接ノードが2、4の順序でプッシュされると仮定します3:

               1
             / |  \
            4  |   2
               3 /

Actions            Stack             Visited
=======            =====             =======
Push 1             [1]               {}
pop and visit 1    []                {1}
 Push 2, 4, 3      [2, 4, 3]         {1}
pop and visit 3    [2, 4]            {1, 3}
 Push 1, 2         [2, 4, 1, 2]      {1, 3}
pop and visit 2    [2, 4, 1]         {1, 3, 2}
 Push 1, 3         [2, 4, 1, 1, 3]   {1, 3, 2}
pop 3 (visited)    [2, 4, 1, 1]      {1, 3, 2}
pop 1 (visited)    [2, 4, 1]         {1, 3, 2}
pop 1 (visited)    [2, 4]            {1, 3, 2}
pop and visit 4    [2]               {1, 3, 2, 4}
  Push 1           [2, 1]            {1, 3, 2, 4}
pop 1 (visited)    [2]               {1, 3, 2, 4}
pop 2 (visited)    []                {1, 3, 2, 4}

したがって、1、2、3の順序で1のネイバーをプッシュするアルゴリズムを適用すると、訪問順序1、3、2、4になります。1のネイバーのプッシュ順序に関係なく、2と3は訪問順序が隣接するため訪問順序で隣接します最初に、まだアクセスされていないもう1つと、まだアクセスされていない1つをプッシュします。

16

DFSロジックは次のようになります。

1)現在のノードにアクセスしていない場合は、ノードにアクセスして、訪問済みとしてマークします
2)訪問されていないすべての近隣の場合、スタックにプッシュします

たとえば、JavaでGraphNodeクラスを定義しましょう。

class GraphNode {
    int index;
    ArrayList<GraphNode> neighbors;
}

そして、ここに再帰なしのDFSがあります:

void dfs(GraphNode node) {
    // sanity check
    if (node == null) {
        return;
    }

    // use a hash set to mark visited nodes
    Set<GraphNode> set = new HashSet<GraphNode>();

    // use a stack to help depth-first traversal
    Stack<GraphNode> stack = new Stack<GraphNode>();
    stack.Push(node);

    while (!stack.isEmpty()) {
        GraphNode curr = stack.pop();

        // current node has not been visited yet
        if (!set.contains(curr)) {
            // visit the node
            // ...

            // mark it as visited
            set.add(curr);
        }

        for (int i = 0; i < curr.neighbors.size(); i++) {
            GraphNode neighbor = curr.neighbors.get(i);

            // this neighbor has not been visited yet
            if (!set.contains(neighbor)) {
                stack.Push(neighbor);
            }
        }
    }
}

同じロジックを使用して、DFSを再帰的に実行したり、グラフを複製したりできます。

6
jeantimex

実際には、スタックは発見時間と終了時間をうまく処理できません。スタックでDFSを実装し、発見時間と終了時間を処理したい場合は、別のレコーダースタックに頼る必要があります、私の実装が示されています以下、テストが正しい、以下はケース1、ケース2、ケース3のグラフです。

case-1case-2case-3

from collections import defaultdict

class Graph(object):

    adj_list = defaultdict(list)

    def __init__(self, V):
        self.V = V

    def add_Edge(self,u,v):
        self.adj_list[u].append(v)

    def DFS(self):
        visited = []
        instack = []
        disc = []
        fini = []
        for t in range(self.V):
            visited.append(0)
            disc.append(0)
            fini.append(0)
            instack.append(0)

        time = 0
        for u_ in range(self.V):
            if (visited[u_] != 1):
                stack = []
                stack_recorder = []
                stack.append(u_)
                while stack:
                    u = stack.pop()
                    visited[u] = 1
                    time+=1
                    disc[u] = time
                    print(u)
                    stack_recorder.append(u)
                    flag = 0
                    for v in self.adj_list[u]:
                        if (visited[v] != 1):
                            flag = 1
                            if instack[v]==0:
                                stack.append(v)
                            instack[v]= 1



                    if flag == 0:
                        time+=1
                        temp = stack_recorder.pop()
                        fini[temp] = time
                while stack_recorder:
                    temp = stack_recorder.pop()
                    time+=1
                    fini[temp] = time
        print(disc)
        print(fini)

if __name__ == '__main__':

    V = 6
    G = Graph(V)

#==============================================================================
# #for case 1
#     G.add_Edge(0,1)
#     G.add_Edge(0,2)
#     G.add_Edge(1,3)
#     G.add_Edge(2,1)
#     G.add_Edge(3,2) 
#==============================================================================

#==============================================================================
# #for case 2
#     G.add_Edge(0,1)
#     G.add_Edge(0,2)
#     G.add_Edge(1,3)
#     G.add_Edge(3,2)  
#==============================================================================

#for case 3
    G.add_Edge(0,3)    
    G.add_Edge(0,1)

    G.add_Edge(1,4)
    G.add_Edge(2,4)
    G.add_Edge(2,5)
    G.add_Edge(3,1)
    G.add_Edge(4,3)
    G.add_Edge(5,5)    


    G.DFS()   
2
K.Wanter

はい。まだJavaコードを探している場合

dfs(Vertex start){
    Stack<Vertex> stack = new Stack<>(); // initialize a stack
    List<Vertex> visited = new ArrayList<>();//maintains order of visited nodes
    stack.Push(start); // Push the start
    while(!stack.isEmpty()){ //check if stack is empty
        Vertex popped = stack.pop(); // pop the top of the stack
        if(!visited.contains(popped)){ //backtrack if the vertex is already visited
            visited.add(popped); //mark it as visited as it is not yet visited
            for(Vertex adjacent: popped.getAdjacents()){ //get the adjacents of the vertex as add them to the stack
                    stack.add(adjacent);
            }
        }
    }

    for(Vertex v1 : visited){
        System.out.println(v1.getId());
    }
}
2
The New One

Pythonコード。時間の複雑さは[〜#〜] o [〜#〜][〜#〜] v [ 〜#〜]+[〜#〜] e [〜#〜])ここで[〜#〜] v [〜#〜]および[〜#〜] e [〜#〜]は、それぞれ頂点とエッジの数です。スペースの複雑さはO([〜#〜] v [〜#〜])です。これは、すべてのパスを含むパスがある最悪のケースのためです。バックトラッキングのない頂点(つまり、検索パスは 線形チェーン です)。

スタックはフォームのタプル(vertex、vertex_Edge_index)を格納するため、その頂点から処理された最後のEdgeの直後のEdgeの特定の頂点からDFSを再開できます(再帰的なDFSの関数呼び出しスタックと同様)。

サンプルコードでは complete digraph を使用します。この場合、すべての頂点が他のすべての頂点に接続されます。したがって、グラフはエッジリストであるため、ノードごとに明示的なエッジリストを保存する必要はありません(グラフ[〜#〜] g [〜#〜]すべての頂点が含まれます)。

numv = 1000
print('vertices =', numv)
G = [Vertex(i) for i in range(numv)]

def dfs(source):
  s = []
  visited = set()
  s.append((source,None))
  time = 1
  space = 0
  while s:
    time += 1
    current, index = s.pop()
    if index is None:
      visited.add(current)
      index = 0
    # vertex has all edges possible: G is a complete graph
    while index < len(G) and G[index] in visited:
      index += 1
    if index < len(G):
      s.append((current,index+1))
      s.append((G[index], None))
    space = max(space, len(s))
  print('time =', time, '\nspace =', space)

dfs(G[0])

出力:

time = 2000 
space = 1000

ここで、time[〜#〜] v [〜#〜]操作ではなく[〜#〜] e [〜#〜]。値はnumv* 2です。これは、すべての頂点が、ディスカバリー時と終了時の2回考慮されるためです。

2
bain

再帰は、コールスタックを使用してグラフトラバーサルの状態を保存する方法です。 std::stack型のローカル変数を使用することで、スタックを明示的に使用できます。DFSを実装するために再帰は必要なく、ループだけが必要です。

2
bobah

visited[n]ブール配列を使用して、現在のノードにアクセスしたかどうかを確認する必要があると思います。

1
Vikram Bhat

多くの人は、非再帰的なDFSはキューではなくスタックを持つ単なるBFSだと言うでしょう。それは正確ではありません。もう少し説明しましょう。

再帰的なDFS

再帰的なDFSは、呼び出しスタックを使用して状態を維持します。つまり、自分で別のスタックを管理することはありません。

ただし、大規模なグラフの場合、再帰的なDFS(または再帰関数)により深い再帰が発生し、スタックオーバーフローで問題がクラッシュする可能性があります(このWebサイトではなく 本物 )。

非再帰的なDFS

DFSはBFSと同じではありません。スペース使用率は異なりますが、BFSと同様に実装し、キューではなくスタックを使用すると、非再帰的なDFSよりも多くのスペースを使用します。

なぜもっとスペースがあるのですか?

このことを考慮:

// From non-recursive "DFS"
for (auto i&: adjacent) {
    if (!visited(i)) {
        stack.Push(i);
    }
}

そして、これと比較してください:

// From recursive DFS
for (auto i&: adjacent) {
    if (!visited(i)) {
        dfs(i);
    }
}

コードの最初の部分では、隣接するすべてのノードをスタックに配置してから、次の隣接する頂点を反復処理しますが、これにはスペースコストがかかります。グラフが大きい場合、大きな違いが生じる可能性があります。

次に何をしますか?

スタックをポップした後、隣接リストを繰り返してスペースの問題を解決することを決定した場合、時間の複雑さのコストが追加されます。

1つの解決策は、訪問時にアイテムを1つずつスタックに追加することです。これを実現するために、ポップした後に反復を再開するためにスタックにイテレーターを保存できます。

怠zyな方法

C/C++での怠compileなアプローチは、プログラムをより大きなスタックサイズでコンパイルし、ulimitを介してスタックサイズを増やすことですが、それは本当にお粗末です。 Javaでは、スタックサイズをJVMパラメーターとして設定できます。

1
arboreal84

Stackを使用して、再帰プロセスの呼び出しスタックで行われたように実装します-

アイデアは、スタック内の頂点をプッシュし、次に、頂点のインデックスで隣接リストに保存されている頂点に隣接する頂点をプッシュし、グラフ内でさらに移動できなくなるまでこのプロセスを続けることです。グラフ内を先に進むと、現在スタックの一番上にある頂点を削除します。これは、未訪問の頂点を取得できないためです。

現在、スタックを使用して、現在の頂点から探索できるすべての頂点が訪問されたときにのみスタックから頂点が削除される点に注意します。これは再帰プロセスによって自動的に実行されていました。

例-

グラフの例をご覧ください

(0(1(2(4 4)2)(3 3)1)0)(6(5 5)(7 7)6)

上記の括弧は、頂点がスタックに追加されてスタックから削除される順序を示しているため、頂点の括弧は、そこからアクセスできるすべての頂点が完了した場合にのみ閉じられます。

(ここでは、隣接リスト表現を使用し、C++ STLを使用してリストのベクトル(ベクトル> AdjList)として実装しました)

void DFSUsingStack() {
    /// we keep a check of the vertices visited, the vector is set to false for all vertices initially.
    vector<bool> visited(AdjList.size(), false);

    stack<int> st;

    for(int i=0 ; i<AdjList.size() ; i++){
        if(visited[i] == true){
            continue;
        }
        st.Push(i);
        cout << i << '\n';
        visited[i] = true;
        while(!st.empty()){
            int curr = st.top();
            for(list<int> :: iterator it = AdjList[curr].begin() ; it != AdjList[curr].end() ; it++){
                if(visited[*it] == false){
                    st.Push(*it);
                    cout << (*it) << '\n';
                    visited[*it] = true;
                    break;
                }
            }
            /// We can move ahead from current only if a new vertex has been added on the top of the stack.
            if(st.top() != curr){
                continue;
            }
            st.pop();
        }
    }
}
0
Aman

再帰アルゴリズムは、できるだけ深く突っ込もうとするため、DFSで非常にうまく機能します。未探索の頂点を見つけたらすぐに、その最初の未探索の頂点をすぐに探索します。最初の未探索の隣人を見つけたらすぐにforループから抜け出す必要があります。

for each neighbor w of v
   if w is not explored
       mark w as explored
       Push w onto the stack
       BREAK out of the for loop
0

次のJavaコードが便利です:-

private void DFS(int v,boolean[] visited){
    visited[v]=true;
    Stack<Integer> S = new Stack<Integer>();
    S.Push(v);
    while(!S.isEmpty()){
        int v1=S.pop();     
        System.out.println(adjLists.get(v1).name);
        for(Neighbor nbr=adjLists.get(v1).adjList; nbr != null; nbr=nbr.next){
             if (!visited[nbr.VertexNum]){
                 visited[nbr.VertexNum]=true;
                 S.Push(nbr.VertexNum);
             }
        }
    }
}
public void dfs() {
    boolean[] visited = new boolean[adjLists.size()];
    for (int v=0; v < visited.length; v++) {
        if (!visited[v])/*This condition is for Unconnected Vertices*/ {

            System.out.println("\nSTARTING AT " + adjLists.get(v).name);
            DFS(v, visited);
        }
    }
}
0
codeislife

私が間違っている場合、これはスペースを修正する私に関して最適化されたDFSだと思います。

s = stack
s.Push(initial node)
add initial node to visited
while s is not empty:
    v = s.peek()
    if for all E(v,u) there is one unvisited u:
        mark u as visited
        s.Push(u)
    else 
        s.pop
0
Bill Cheng