有向グラフでサイクルを検出するための最適なアルゴリズム


396

有向グラフ内のすべてのサイクルを検出するための最も効率的なアルゴリズムは何ですか?

実行する必要があるジョブのスケジュールを表す有向グラフがあります。ジョブはノードであり、依存関係はエッジです。このグラフ内の循環の依存関係につながるエラーのケースを検出する必要があります。


13
すべてのサイクルを検出したいとおっしゃっていますが、ユースケースでは、サイクルがあるかどうかを検出するだけで十分であることが示されています。
スティーブジェソップ

29
それはむしろ等のチェック、修正、チェック、修正するよりも、彼らは一度に固定することができるよう、すべてのサイクルを検出する方が良いだろう
Peauters

2
ドナルドB.ジョンソンの「有向グラフのすべての基本回路を見つける」という論文を読んでください。基本回路のみを検出しますが、これで十分です。そして、これがすぐに使用できるこのアルゴリズムのJava実装です:github.com/1123/johnson
user152468

アルゴリズムをさらに変更してDFSを実行します。アクセスした各ノードをマークします。すでにアクセスされているノードにアクセスすると、cicleが作成されます。パスから後退するときは、訪れたノードのマークを外してください。
Hesham Yassin 2016年

2
@HeshamYassin、すでにアクセスしたノードにアクセスしても、必ずしもループがあるとは限りません。私のコメントcs.stackexchange.com/questions/9676/…を読んでください。
マクシムドミトリエフ2017年

回答:


193

Tarjanの強連結コンポーネントアルゴリズムにO(|E| + |V|)時間の複雑さをいます。

他のアルゴリズムについては、ウィキペディアの強く接続されたコンポーネントを参照してください。


69
強連結成分を見つけると、グラフに存在するサイクルについてどのようにわかりますか?
Peter

4
誰かが確認できるかもしれませんが、Tarjanアルゴリズムは、A-> Aのように、自分自身を直接指すノードのサイクルをサポートしていません。
セドリックギエメット

24
@Cedrikそうですね、直接ではありません。これはTarjanのアルゴリズムの欠陥ではありませんが、この質問での使用方法です。Tarjanはサイクルを直接検出するのではなく、強く関連するコンポーネントを検出します。もちろん、1より大きいサイズのSCCはサイクルを意味します。非循環コンポーネントには、それ自体がシングルトンSCCがあります。問題は、セルフループも単独でSCCに入るということです。したがって、自己ループの個別のチェックが必要です。これはかなり簡単です。
mgiuca

13
(グラフ内のすべての強連結成分)!=(グラフ内のすべてのサイクル)
optimusfrenk

4
@aku:3色のDFSにも同じランタイムがありますO(|E| + |V|)。白(一度も訪れたことがない)、灰色(現在のノードは訪れたが到達可能なすべてのノードはまだ訪れていない)、および黒(すべての到達可能なノードは現在のノードと一緒に訪れた)を使用したカラーコーディング、灰色のノードが別の灰色のノードを見つけた場合veサイクル。[Cormenのアルゴリズムブックに掲載されているものはかなり多い]。「タージャンのアルゴリズム」がそのようなDFSよりも利点があるかどうか疑問に思います!!
KGhatak 2017年

73

これがジョブのスケジュールであることを考えると、ある時点でソートすることになると思います、それらを提案された実行順序する。

その場合、トポロジーソートの実装は、いずれにせよサイクルを検出する可能性があります。UNIXはtsort確かにそうです。したがって、個別のステップではなく、tsortingと同時にサイクルを検出する方が効率的であると思います。

したがって、「ループを最も効率的に検出する方法」ではなく、「どのようにして最も効率的にtsortするか」という疑問が生じる可能性があります。答えはおそらく「ライブラリを使用する」でしょうが、次のウィキペディアの記事は失敗しています:

http://en.wikipedia.org/wiki/Topological_sorting

あるアルゴリズムの疑似コードと、Tarjanによる別のアルゴリズムの簡単な説明があります。どちらもO(|V| + |E|)時間が複雑です。


トポロジカルソートは、深さ優先検索アルゴリズムに依存しているため、サイクルを検出できますが、実際にサイクルを検出するには、追加の簿記が必要です。カート・ピークの正解をご覧ください。
ルークハッチソン

33

それを行う最も簡単な方法は、グラフの深さ優先トラバーサル(DFT)を実行することですです。

グラフにn頂点がある場合、これはO(n)時間複雑性アルゴリズムです。おそらく各頂点からDFTを実行する必要があるため、全体の複雑度は次のようになります。O(n^2)ます。

現在の深度の最初のトラバーサルすべての頂点を含むスタックを維持する必要があります。その最初の要素はルートノードです。DFT中に既にスタックにある要素に遭遇した場合、サイクルがあります。


21
これは、「通常の」グラフのために真であるが、ために偽のでしょう有向グラフ。たとえば、4つのノードを持つ「ダイヤモンド依存図」を考えてみます。AがBとCを指し、それぞれがDを指すエッジを持っています。この図のAからのDFTトラバーサルは、「ループ」が実際にはサイクル-ループはありますが、矢印をたどって移動することができないため、ループではありません。
Peter

9
@ピーターは、AからのDFTがサイクルがあると誤って結論付ける方法を説明できますか?
Deepak 2012

10
@Deepak-実際、私は「physウィザード」からの答えを誤解しています。彼が「スタック内」に書き込んだところ、「すでに見つかっている」と思いました。DFTの実行中に「スタック内」の重複をチェックすることは、確かに(有向ループを検出するために)十分です。あなた一人一人に一つの賛成票。
ピーター

2
O(n)スタックをチェックして、すでに訪問済みのノードが含まれているかどうかを確認するよう提案しているのに、時間の複雑さはなぜですか?スタックO(n)をスキャンすると、新しいノードごとにスタックをスキャンする必要があるため、実行時間が長くなります。O(n)訪問したノードにマークを付けると達成できます
James Wierzba

ピーターが言ったように、これは有向グラフでは不完全です。カート・ピークの正解をご覧ください。
ルークハッチソン

32

コーメンらの補題22.11 Introduction to Algorithms(CLRS)によれば、

有向グラフGは、Gの深さ優先探索でバックエッジが生成されない場合にのみ非循環です。

これはいくつかの回答で言及されています。ここでは、CLRSの第22章に基づくコード例も提供します。グラフの例を以下に示します。

ここに画像の説明を入力してください

深さ優先検索読み取り用のCLRSの疑似コード:

ここに画像の説明を入力してください

CLRS図22.4の例では、グラフは2つのDFSツリーで構成されています。1つはノードuvx、およびyで構成され、もう1つはノードwおよびzで構成されています。各ツリーには1つのバックエッジが含まれています。1つはxからv、もう1つはzからzです。(自己ループ)。

キー実現が戻るときにエッジがに、遭遇されていることであるDFS-VISIT隣人を反復処理しながら、機能vu、ノードがで遭遇しましたGRAY色。

次のPythonコードは、CLRSの疑似コードに、ifサイクルを検出する句を追加したものです。

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 finished:
            dfs_visit(G, v, discovered, finished)

    discovered.remove(u)
    finished.add(u)

    return discovered, finished


if __name__ == "__main__":
    G = Graph([
        ('u', 'v'),
        ('u', 'x'),
        ('v', 'y'),
        ('w', 'y'),
        ('w', 'z'),
        ('x', 'v'),
        ('y', 'x'),
        ('z', 'z')])

    dfs(G)

この例では、 time、サイクルの検出のみに関心があるため、CLRSの疑似コードはキャプチャされない。エッジのリストからグラフの隣接リスト表現を構築するためのボイラープレートコードもあります。

このスクリプトを実行すると、次の出力が出力されます。

Cycle detected: found a back edge from x to v.
Cycle detected: found a back edge from z to z.

これらは、正確にCLRS図22.4の例のバックエッジです。


29

DFSから始めます。DFS 中にバックエッジが発見された場合にのみ、サイクルが存在します。これはホワイトパス定理の結果として証明されました。


3
はい、私は同じだと思うが、これは十分ではありません、私は私の道のcs.stackexchange.com/questions/7216/find-the-simple-cycles-in-a-directed-graphを投稿
jonaprieto

そうだね。Ajay Gargは、この質問の一部の回答である「サイクル」を見つける方法についてのみ述べています。あなたのリンクは、尋ねられた質問に従ってすべてのサイクルを見つけることについて話していますが、再びそれはAjay Gargと同じアプローチを使用しているように見えますが、すべての可能なdfsツリーも行います。
Manohar Reddy Poreddy 2016

これは有向グラフでは不完全です。カート・ピークの正解をご覧ください。
ルークハッチソン

26

私の意見では、有向グラフでサイクルを検出するための最も理解できるアルゴリズムは、グラフの色付けアルゴリズムです。

基本的に、グラフの色付けアルゴリズムは、DFS方式でグラフをウォークします(深さ優先検索、つまり、別のパスを探索する前にパスを完全に探索します)。バックエッジが見つかると、グラフにループが含まれているとマークされます。

グラフの色付けアルゴリズムの詳細については、次の記事を参照してください:http : //www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

また、私はJavaScript https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.jsでグラフの色付けの実装を提供します


8

「訪問済み」プロパティをノードに追加できない場合は、セット(またはマップ)を使用し、訪問済みのすべてのノードをセットに追加します。一意のキーまたはオブジェクトのアドレスを「キー」として使用します。

これは、ユーザーが問題を修正する必要があるときに役立つ循環依存関係の「ルート」ノードに関する情報も提供します。

別の解決策は、実行する次の依存関係を見つけようとすることです。そのためには、現在の場所と次に何をする必要があるかを思い出せるスタックが必要です。実行する前に、依存関係がこのスタックに既に存在するかどうかを確認してください。もしそうなら、あなたはサイクルを見つけました。

これはO(N * M)の複雑さを持っているように見えるかもしれませんが、スタックの深さは非常に制限されているため(Nは小さい)、Mは依存関係ごとに小さくなり、「実行された」プラスとして確認できます。葉を見つけたら検索を停止できます(したがって、すべてのノードをチェックする必要ありませ -> Mも小さくなります)。

MetaMakeでは、リストのリストとしてグラフを作成し、実行時にすべてのノードを削除して、自然に検索ボリュームを削減しました。実際に独立したチェックを実行する必要はありませんでした。すべて通常の実行中に自動的に行われました。

「テストのみ」モードが必要な場合は、実際のジョブの実行を無効にする「ドライラン」フラグを追加するだけです。


7

多項式時間の有向グラフですべてのサイクルを見つけることができるアルゴリズムはありません。有向グラフにn個のノードがあり、ノードのすべてのペアが相互に接続している、つまり完全なグラフがあると仮定します。したがって、これらのnノードの空でないサブセットはサイクルを示し、そのようなサブセットは2 ^ n-1個存在します。したがって、多項式時間アルゴリズムは存在しません。したがって、グラフの有向サイクル数を示すことができる効率的な(愚かではない)アルゴリズムがあるとします。まず、強い連結成分を見つけ、次にこれらの連結成分にアルゴリズムを適用します。サイクルはコンポーネント内にのみ存在し、コンポーネント間には存在しないため。


1
ノードの数が入力のサイズと見なされる場合はTrue。エッジの数やサイクル数、またはこれらの測定の組み合わせの観点から、ランタイムの複雑さを説明することもできます。Donald B. Johnsonによるアルゴリズム「有向グラフのすべての基本回路を見つける」には、O((n + e)(c + 1))で与えられる多項式の実行時間があります。ここで、nはノードの数、eはエッジの数です。 cはグラフの基本回路の数。そして、これがこのアルゴリズムのJava実装です:github.com/1123/johnson
user152468

4

私はこの問題をsml(命令型プログラミング)で実装しました。こちらが概要です。インディグリーまたはアウトディグリーが0であるすべてのノードを見つけます。このようなノードをサイクルの一部にすることはできません(したがって、それらを削除してください)。次に、そのようなノードからすべての入力エッジまたは出力エッジを削除します。このプロセスを結果のグラフに再帰的に適用します。最後にノードやエッジが残っていない場合、グラフにはサイクルがありません。


2

私が行う方法は、訪問した頂点の数を数えて、トポロジカルソートを行うことです。その数がDAG内の頂点の総数より少ない場合は、サイクルがあります。


4
それは意味がありません。グラフにサイクルがある場合、トポロジーソートはありません。つまり、トポロジーソートの正しいアルゴリズムはすべて中止されます。
sleske 2009

4
wikipediaから:トポロジーの順序が存在するための障害であるため、多くのトポロジーソートアルゴリズムもサイクルを検出します。
Oleg Mikheev

1
@OlegMikheevはい、しかしスティーブは「その数がDAGの頂点の総数よりも少ない場合、サイクルがある」と言っていますが、これは意味がありません。
nbro 2015

@nbroたぶん、それらはトポロジーソートが存在しない場合に中止されるトポロジーソートアルゴリズムのバリアントを意味します(その後、すべての頂点にアクセスしません)。
maaartinus 2017

サイクルのあるグラフでトポロジカルソートを行うと、不良エッジの数が最も少ない順序になります(順序番号>近傍の順序番号)。しかし、並べ替えを行う必要があると、これらの不良エッジを簡単に検出できるため、サイクルのあるグラフが検出されます
UGP

2

/mathpro/16393/finding-a-cycle-of-fixed-length私はこのソリューションが特に4長に最適です:)

また、physウィザードは、O(V ^ 2)を実行する必要があると言っています。私はO(V)/ O(V + E)だけが必要だと思います。グラフが接続されている場合、DFSはすべてのノードを訪問します。グラフにサブグラフが接続されている場合、このサブグラフの頂点でDFSを実行するたびに、接続された頂点が見つかり、DFSの次の実行でこれらを考慮する必要はありません。したがって、各頂点に対して実行される可能性は正しくありません。


1

DFSが既にアクセスした頂点を指すエッジを見つけた場合、そこにサイクルがあります。


1
1,2,3で失敗:1,2; 1,3; 2,3;
騒々しい猫

4
@JakeGreeneここを見てください:i.imgur.com/tEkM5xy.png理解するのに十分なほど単純です。0から開始するとします。次に、ノード1に移動します。そこからのパスはなくなり、再帰が戻ります。次に、すでに訪問した頂点1にエッジを持つノード2を訪問します。あなたの意見では、あなたはそのときサイクルを持っているでしょう-そしてあなたは本当にそれを持っていません
騒々しい猫

3
@kittyPLそのグラフにはサイクルが含まれていません。ウィキペディアから:「有向グラフの有向サイクルは、同じ頂点で開始および終了する一連の頂点であり、サイクルの連続する2つの頂点ごとに、前の頂点から後の頂点に向かうエッジが存在します」有向サイクルでVに戻るVからのパスをたどることができる必要があります。mafonyaの解決策は与えられた問題に対して有効です
ジェイクグリーン

2
@JakeGreeneもちろんそうではありません。アルゴリズムを使用して1から開始すると、とにかくサイクルを検出します...このアルゴリズムはただ悪いです...通常、訪問した頂点に遭遇したときはいつでも、逆方向に歩くだけで十分です。
騒々しい猫

6
@kittyPL DFSは、指定された開始ノードからのサイクルを検出するように機能します。ただし、DFSを実行するときは、訪問したノードを色分けして、クロスエッジとバックエッジを区別する必要があります。頂点に初めてアクセスしたときは灰色になり、すべてのエッジに移動したら黒に変わります。DFSを実行するときに灰色の頂点にぶつかった場合、その頂点は祖先です(つまり、サイクルがあります)。頂点が黒の場合、それは単なるクロスエッジです。
Kyrra 2014

0

あなたが言ったように、あなたはジョブのセットを持っています、それは特定の順序で実行される必要があります。Topological sortジョブのスケジューリングに必要な順序が与えられた場合(または、依存関係の問題の場合direct acyclic graph)。dfsリストを実行および保守し、リストの最初にノードを追加し始めます。すでにアクセスされているノードに遭遇した場合。次に、特定のグラフでサイクルを見つけました。


-11

グラフがこのプロパティを満たす場合

|e| > |v| - 1

次に、グラフには少なくともサイクルが含まれます。


10
これは無向グラフには当てはまるかもしれませんが、有向グラフには当てはまりません。
Hans-PeterStörr、2011年

6
反例は、A-> B、B-> C、A-> Cです。
user152468

すべての頂点にエッジがあるわけではありません。
Debanjan Dhar 2016
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.