主にDFSは、BFSではなくグラフのサイクルを見つけるために使用されます。何か理由はありますか?どちらも、ツリー/グラフをトラバースしているときにノードがすでにアクセスされているかどうかを確認できます。
回答:
深さ優先探索は、より早くバックトラックできるため、幅優先探索よりもメモリ効率が高くなります。コールスタックを使用すると実装も簡単になりますが、これはスタックをオーバーフローしない最長のパスに依存します。
また、グラフが指示されている場合は、ノードにアクセスしたかどうかだけでなく、どのようにしてそこに到達したかを覚えておく必要があります。そうでなければ、あなたはあなたがサイクルを見つけたと思うかもしれませんが、実際にはあなたが持っているのは2つの別々のパスA-> Bだけですが、それはパスB-> Aがあるという意味ではありません。例えば、
から開始してBFSを実行0
すると、サイクルが存在することを検出しますが、実際にはサイクルはありません。
深さ優先探索を使用すると、下降時にノードを訪問済みとしてマークし、バックトラック時にノードのマークを解除できます。このアルゴリズムのパフォーマンスの向上については、コメントを参照してください。
有向グラフでサイクルを検出するための最良のアルゴリズムについては、Tarjanのアルゴリズムを参照してください。
グラフが無向である場合(有向グラフでサイクルを報告するBFSを使用した効率的なアルゴリズムを示すゲストになります!)、BFSは妥当である可能性があります。クロスエッジが{v1, v2}
であり、それらのノードを含むルート(BFSツリー内)がr
である場合、サイクルはr ~ v1 - v2 ~ r
(~
パス、-
単一エッジ)であり、DFSの場合とほぼ同じくらい簡単に報告できます。
BFSを使用する唯一の理由は、(無向の)グラフに長いパスと小さなパスカバー(つまり、深くて狭い)があることがわかっている場合です。その場合、BFSはDFSのスタックよりもキューに必要なメモリが比例して少なくなります(もちろんどちらも線形です)。
他のすべての場合、DFSが明らかに勝者です。有向グラフと無向グラフの両方で機能し、サイクルを報告するのは簡単です。祖先から子孫へのパスにバックエッジを連結するだけで、サイクルが得られます。全体として、この問題については、BFSよりもはるかに優れて実用的です。
BFSは、サイクルを見つける際の有向グラフでは機能しません。A-> BおよびA-> C-> Bを、グラフのAからBへのパスと見なします。BFSは、パスの1つに沿って進んだ後、Bが訪問されたと言います。次のパスを移動し続けると、マークされたノードBが再び見つかったと表示されます。したがって、サイクルがあります。明らかにここにはサイクルがありません。
なぜこのような古い質問がフィードに表示されたのかわかりませんが、以前の回答はすべて悪いので...
それはのでDFSは、有向グラフにおけるサイクルを見つけるために使用されて動作します。
DFSでは、すべての頂点が「訪問」されます。頂点への訪問とは、次のことを意味します。
その頂点から到達可能なサブグラフにアクセスします。これには、その頂点から到達可能なすべてのトレースされていないエッジのトレース、および到達可能なすべての未訪問の頂点へのアクセスが含まれます。
頂点が完成しました。
重要な機能は、頂点から到達可能なすべてのエッジが、頂点が終了する前にトレースされることです。これはDFSの機能ですが、BFSではありません。実際、これがDFSの定義です。
この機能により、サイクルの最初の頂点が開始されると、次のことがわかります。
したがって、サイクルがある場合は、開始されているが未完了の頂点へのエッジを見つけることが保証され(2)、そのようなエッジを見つけた場合は、サイクルがあることが保証されます(3)。
そのため、DFSは有向グラフのサイクルを見つけるために使用されます。
BFSはそのような保証を提供しないため、機能しません。(サブプロシージャとしてBFSまたは同様のものを含む完全に優れたサイクル検出アルゴリズムにもかかわらず)
一方、無向グラフには、頂点のペアの間に2つのパスがある場合、つまりツリーでない場合は常にサイクルがあります。これは、BFSまたはDFSのいずれかで簡単に検出できます。新しい頂点にトレースされたエッジはツリーを形成し、他のエッジはサイクルを示します。
ツリーのランダムな場所にサイクルを配置すると、DFSは、ツリーの約半分が覆われたときにサイクルにヒットする傾向があり、サイクルが進む場所をすでに通過した時間の半分と、通過しない時間の半分(そして、ツリーの残りの半分で平均してそれを見つけるでしょう)、それでそれはツリーの平均で約0.5 * 0.5 + 0.5 * 0.75 = 0.625を評価します。
ツリーのランダムな場所にサイクルを配置すると、BFSは、その深さでツリーのレイヤーを評価した場合にのみサイクルにヒットする傾向があります。したがって、通常、バランス二分木の葉を評価する必要があり、その結果、通常、より多くのツリーが評価されます。特に、3/4の時間、2つのリンクの少なくとも1つがツリーの葉に表示されます。そのような場合、ツリーの平均3/4(リンクが1つある場合)または7 /を評価する必要があります。ツリーの8(2つある場合)なので、1/2 * 3/4 + 1/4 * 7/8 =(7 + 12)/ 32 = 21/32 =を検索することを期待できます。リーフノードから離れて追加されたサイクルでツリーを検索するコストを追加することなく、ツリーの0.656...。
さらに、DFSはBFSよりも実装が簡単です。したがって、サイクルについて何かを知らない限り、これを使用します(たとえば、サイクルは検索元のルートの近くにある可能性が高く、その時点でBFSが有利になります)。
再帰的実装と反復的実装のどちらについて話しているかによって異なります。
再帰的-DFSはすべてのノードに2回アクセスします。反復-BFSはすべてのノードに1回アクセスします。
サイクルを検出する場合は、ノードを「開始」するときと「終了」するときの両方で、隣接ノードを追加する前と後の両方でノードを調査する必要があります。
これにはIterative-BFSでより多くの作業が必要になるため、ほとんどの人はRecursive-DFSを選択します。
たとえば、std :: stackを使用したIterative-DFSの単純な実装には、Iterative-BFSと同じ問題があることに注意してください。その場合、ノードでの作業を「終了」したときに追跡するために、ダミー要素をスタックに配置する必要があります。
Iterative-DFSがノードで「終了」するタイミングを決定するために追加の作業が必要になる方法の詳細については、この回答を参照してください(TopoSortのコンテキストで回答)。
うまくいけば、それが、ノードの処理を「終了」するタイミングを決定する必要がある問題に対して、人々がRecursive-DFSを好む理由を説明しています。