有向グラフ内のすべてのサイクルを検出するための最も効率的なアルゴリズムは何ですか?
実行する必要があるジョブのスケジュールを表す有向グラフがあります。ジョブはノードであり、依存関係はEdgeです。このグラフの中で循環依存性につながるサイクルのエラーケースを検出する必要があります。
Tarjanの強連結成分アルゴリズム はO(|E| + |V|)
時間計算量を持ちます。
他のアルゴリズムについては、ウィキペディアの 強連結成分 を参照してください。
これがジョブのスケジュールであることを考えると、ある時点であなたはそれらを提案された実行順序にソートすることになると思います。
その場合、 トポロジソート の実装では、どのような場合でもサイクルを検出できます。 UNIXのtsort
は確かにそうです。したがって、別のステップではなく、歪曲と同時にサイクルを検出する方が効率的であると考えられます。
そのため、「ループを最も効率的に検出する方法」ではなく、「どのようにして最も効率的にtsortを実行するのか」という質問になる可能性があります。その答えはおそらく「ライブラリを使う」であるが、それに失敗したのは次のウィキペディアの記事です。
あるアルゴリズムの擬似コードと、Tarjanによる別のアルゴリズムの簡単な説明があります。どちらもO(|V| + |E|)
時間の複雑さを持っています。
DFSから始めます。back-EdgeがDFSの間に検出された場合に限り、サイクルが存在します。これはホワイトパス定理の結果として証明されています。
これを行う最も簡単な方法はグラフの深さ優先のトラバース(DFT)を行うです。
グラフがn
の頂点を持つ場合、これはO(n)
時間複雑度アルゴリズムです。おそらく各頂点からDFTを実行しなければならないので、全体の複雑さはO(n^2)
になります。
最初の要素がルートノードになるように、現在の深さの最初のトラバーサルにあるすべての頂点を含むスタックを維持する必要があります。あなたがDFTの間にすでにスタックにある要素に出くわしたならば、あなたはサイクルを持っています。
私の考えでは、有向グラフの周期を検出するための最も理解しやすいアルゴリズムは、グラフ着色アルゴリズムです。
基本的に、グラフの色付けアルゴリズムはDFSの方法でグラフをたどります(Depth First Search、つまり別のパスを探索する前にパスを完全に探索することを意味します)。後方エッジが見つかると、ループを含むものとしてグラフにマークを付けます。
グラフ彩色アルゴリズムの詳細な説明については、この記事を読んでください: http://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/
また、私はJavaScriptでグラフカラーリングの実装を提供します https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js
ノードに「visited」プロパティを追加できない場合は、セット(またはマップ)を使用し、すでにセットに含まれていない限り、訪問したすべてのノードをセットに追加します。 「キー」として、一意のキーまたはオブジェクトのアドレスを使用してください。
これはまた、循環的な依存関係の「ルート」ノードに関する情報を提供します。これは、ユーザーが問題を解決する必要があるときに役立ちます。
もう1つの解決策は、次に実行する依存関係を見つけようとすることです。そのためには、自分が今どこにいるのか、次に何をする必要があるのかを思い出すことができるスタックが必要です。実行する前に、依存関係がすでにこのスタックにあるかどうかを確認してください。もしそうなら、あなたはサイクルを見つけました。
これはO(N * M)の複雑さを持っているように思われるかもしれませんが、スタックは非常に限られた深さしか持たず(したがってNは小さい)、Mは "実行"としてチェックすることができるあなたが葉を見つけたときに検索を止めることができます(だから決してすべてのノードをチェックする必要はありません - > Mも小さくなります)。
MetaMakeでは、リストのリストとしてグラフを作成してから、ノードを実行するたびにすべてのノードを削除したため、検索ボリュームが自然に削減されました。私は実際に独立したチェックを実行する必要はありませんでした、それはすべて通常の実行中に自動的に行われました。
「テスト専用」モードが必要な場合は、実際のジョブの実行を無効にする「ドライラン」フラグを追加するだけです。
多項式時間で有向グラフのすべてのサイクルを見つけることができるアルゴリズムはありません。有向グラフがn個のノードを持ち、ノードの各ペアが相互に接続していると仮定すると、完全なグラフが得られます。したがって、これらのn個のノードの空でないサブセットはサイクルを示し、そのようなサブセットは2 ^ n-1個あります。そのため、多項式時間アルゴリズムは存在しません。グラフの有向サイクル数を見分けることができる効率的な(バカでない)アルゴリズムがあるとしましょう。最初に強い連結成分を見つけ、次にこれらの連結成分にアルゴリズムを適用します。サイクルはコンポーネント内にのみ存在し、コンポーネント間には存在しないためです。
Cormen他の補題22.11によると、 アルゴリズムの紹介 (CLRS):
有向グラフGは、Gの深さ優先探索が後縁をもたらさない場合に限り、非巡回的である。
これはいくつかの回答で言及されています。ここで私はまたCLRSの第22章に基づくコード例を提供します。グラフの例を以下に示します。
深さ優先探索のためのCLRSの擬似コードは次のようになります。
図22.4のCLRSの例では、グラフは2つのDFSツリーで構成されています。1つはノード u 、 v 、 x 、および y 、もう一方はノードです。 w および z 。それぞれの木は一つの後方Edgeを含みます:一つは x から v へ、そしてもう一つは z から z へ(自己ループ)。
重要な認識は、DFS-VISIT
関数内で、v
の隣のu
を反復処理しているときに、GRAY
カラーでノードに遭遇したときに後方エッジに遭遇することです。
次のPythonコードは、サイクルを検出するif
節が追加されたCLRSの擬似コードを改変したものです。
import collections
class Graph(object):
def __init__(self, edges):
self.edges = edges
self.adj = Graph._build_adjacency_list(edges)
@staticmethod
def _build_adjacency_list(edges):
adj = collections.defaultdict(list)
for Edge in edges:
adj[Edge[0]].append(Edge[1])
return adj
def dfs(G):
discovered = set()
finished = set()
for u in G.adj:
if u not in discovered and u not in finished:
discovered, finished = dfs_visit(G, u, discovered, finished)
def dfs_visit(G, u, discovered, finished):
discovered.add(u)
for v in G.adj[u]:
# Detect cycles
if v in discovered:
print(f"Cycle detected: found a back Edge from {u} to {v}.")
# Recurse into DFS tree
if v not in discovered and v not in finished:
dfs_visit(G, v, discovered, finished)
discovered.remove(u)
finished.add(u)
return discovered, finished
if __== "__main__":
G = Graph([
('u', 'v'),
('u', 'x'),
('v', 'y'),
('w', 'y'),
('w', 'z'),
('x', 'v'),
('y', 'x'),
('z', 'z')])
dfs(G)
この例では、CLRSの疑似コード内のtime
は、サイクルの検出にのみ関心があるため、キャプチャーされません。辺のリストからグラフの隣接リスト表現を構築するための定型コードもあります。
このスクリプトが実行されると、次の出力を表示します。
Cycle detected: found a back Edge from x to v.
Cycle detected: found a back Edge from z to z.
図22.4の例では、これらはまさにバックエッジです。
DFSが既に訪れた頂点を指すEdgeを見つけると、そこにサイクルがあります。
私がそうする方法は、訪れた頂点の数を数えて、トポロジカルソートをすることです。その数がDAG内の頂点の総数より少ない場合は、周期があります。
私はこの問題をsml(命令型プログラミング)で実装しました。これがその概要です。次数が0またはoutgreeが0のすべてのノードを見つけます。そのようなノードはサイクルの一部になることはできません(したがってそれらを削除してください)。次に、そのようなノードからすべての着信エッジまたは発信エッジを削除します。このプロセスを結果のグラフに再帰的に適用します。最後にノードやエッジが残っていない場合、グラフには周期がありません。それ以外の場合はグラフにはありません。
https://mathoverflow.net/questions/16393/finding-a-cycle-of-fixed-length 私はこのソリューションが4のために特別に最も好き長さ:)
また、phys wizardは、u(V ^ 2)を実行する必要があると言っています。私たちはO(V)/ O(V + E)だけを必要としていると思います。グラフが接続されている場合、DFSはすべてのノードにアクセスします。グラフにサブグラフが接続されている場合、このサブグラフの頂点でDFSを実行するたびに、接続されている頂点が見つかり、次回のDFSの実行でこれらを考慮する必要はありません。したがって、各頂点に対して実行する可能性は正しくありません。
あなたが言ったように、あなたは一連の仕事を持っています、それはある順番で実行される必要があります。 Topological sort
は、ジョブのスケジュール(またはそれがdirect acyclic graph
の場合は依存関係の問題)の順序付けを要求したことを示しています。 dfs
を実行してリストを保守し、リストの先頭にノードを追加し始めます。すでに訪れたことがあるノードに遭遇した場合は。それからあなたは与えられたグラフでサイクルを見つけました。