最近、より複雑なアルゴリズム、正確には Tarjan のアルゴリズムの一部として非再帰的 DFS を実装する必要がありました。再帰的な実装は非常に洗練されていますが、大きなグラフには適していません。反復バージョンを実装したとき、それが最終的にいかに洗練されていないものになってしまったかにショックを受け、何か間違ったことをしたのではないかと思いました。
反復 DFS には 2 つの基本的なアプローチがあります。まず、ノードのすべての子を一度にスタックにプッシュできます (はるかに一般的なようです)。または、1 つだけプッシュすることもできます。誰もがそうしているように見えるので、最初のものに焦点を当てます。
このアルゴリズムにはさまざまな問題がありましたが、最終的には、効率的に行うには、1 つではなく 2 つではなく 3 つのブール値フラグが必要であることに気付きました (必ずしも 3 つの明示的なブール変数が必要であるとは限りません。情報を間接的に保存することもできます)。通常は整数である変数の特別な値を使用しますが、何らかの方法でこれら 3 つの情報にアクセスする必要があります.3 つのフラグは: 1) 訪問済みです。これは、子が非常に冗長にスタックにプッシュされるのを防ぐためでした。2) 完了。同一ノードの重複処理を防止する。3) 昇順/降順。子がすでにスタックにプッシュされているかどうかを示します。擬似コードは次のようになります。
while(S)
if S.peek().done == True
S.pop()
continue
S.peek().visited = True
if S.peek().descending == True
S.peek().descending = False
for c in S.peek().children
if c.visited == False
S.push(c)
doDescendingStuff()
else
w = S.pop()
w.done = True
doAscendingStuff()
いくつかのメモ: 1) 技術的に昇順/降順は必要ありません。子がすべて完了しているかどうかを確認するだけでよいからです。しかし、密なグラフではかなり非効率的です。
2) 主なキッカー: 訪問/完了は必要ないように見えるかもしれません。これが(私が思うに)あなたがそれを必要とする理由です。スタック上でそれらを訪問するまで、訪問済みのものをマークすることはできません。もしそうなら、物事を間違った順序で処理することができます。たとえば、A が B と C にリンクされ、B が D にリンクされ、D が C にリンクされているとします。次に、A から B と C をスタックにプッシュします。B から D をスタックにプッシュします。スタックにプッシュするときに訪問したものをマークしている場合は、ここで C をスタックにプッシュしません。しかし、これは間違っています。このグラフでは、A からではなく、D から C にアクセスする必要があります (A が C の前に B にアクセスすると仮定)。したがって、それらを処理するまで、訪問したものをマークしません。ただし、スタックに C が 2 回あります。したがって、完全に完了したことを示す別のフラグが必要なので、C を 2 回処理する必要はありません。
このすべてを回避して、巻き戻しと巻き戻しの両方のアクションをサポートする完全に正しい非再帰的 DFS を作成する方法がわかりません。しかし、本能的にそれは不器用に感じます。より良い方法はありますか?私がオンラインで調べたほとんどすべての場所で、非再帰的 DFS を実際に実装する方法について、実際に行うことができ、非常に基本的なアルゴリズムを提供していると述べています。アルゴリズムが正しい場合 (同じノードへの複数のパスを適切にサポートするという点で) はまれですが、ワインディングとアンワインドの両方で処理を適切にサポートすることはめったにありません。