有向グラフが非巡回であるかどうかを確認するにはどうすればよいですか?


82

有向グラフが非巡回であるかどうかを確認するにはどうすればよいですか?そして、アルゴリズムはどのように呼び出されますか?参考にしていただければ幸いです。


SOの間違った答えを「修正」する何らかの方法を支持する別のケース。
スパー2009

2
それで、うーん、私はそれを見つけるのに必要な時間に主に興味があります。だから、私は抽象的なアルゴリズムが必要です。
nes1983 2009

すべてのエッジをトラバースし、すべての頂点をチェックして、下限がO(| V | + | E |)になるようにする必要があります。DFSとBFSは両方とも同じ複雑ですが、それは...あなたのためのスタックを管理してあなたは再帰を持っている場合、DFSは、コードに簡単です
ShuggyCoUk

DFSは同じ複雑さではありません。ノード{1 .. N}と、{(a、b)|の形式のエッジを持つグラフを考えてみましょう。a <b}。そのグラフは非周期的ですが、DFSはO(n!)になります
FryGuy 2009

1
DFSは決してO(n!)ではありません。各ノードに1回アクセスし、各エッジに最大2回アクセスします。したがって、O(| V | + | E |)またはO(n)です。
ジェイコンロッド

回答:


95

グラフをトポロジ的並べ替えようとしますが、それができない場合は、サイクルがあります。


2
どうしてこれに投票がなかったのですか?これはノード+エッジで線形であり、O(n ^ 2)ソリューションよりもはるかに優れています。
Loren Pechtel 2009

5
多くの場合、特にDFSを実行する必要がある場合は、DFS(J.Conrodの回答を参照)の方が簡単な場合があります。しかしもちろん、これは状況によって異なります。
sleske 2009

1
トポロジカル順序は無限ループになりますが、サイクルがどこで発生するかはわかりません...
Baradwaj Aryasomayajula 2015年

35

単純な深さ優先探索を実行するだけでは、サイクルを見つけるのに十分ではありません。サイクルが存在しなくても、DFSでノードに複数回アクセスすることができます。開始する場所によっては、グラフ全体にアクセスできない場合もあります。

グラフの連結成分のサイクルは、次のように確認できます。発信エッジのみを持つノードを見つけます。そのようなノードがない場合は、サイクルがあります。そのノードでDFSを開始します。各エッジをトラバースするときは、エッジがすでにスタック上にあるノードを指しているかどうかを確認してください。これは、サイクルの存在を示しています。そのようなエッジが見つからない場合、その連結成分にサイクルはありません。

Rutger Prinsが指摘しているように、グラフが接続されていない場合は、接続されている各コンポーネントで検索を繰り返す必要があります。

参考までに、タージャンの強連結成分アルゴリズムは密接に関連しています。また、サイクルが存在するかどうかを報告するだけでなく、サイクルを見つけるのにも役立ちます。


2
ところで:「すでにスタック上にあるノードを指す」エッジは、明らかな理由から、文献では「バックエッジ」と呼ばれることがよくあります。はい、これは、特にDFSを実行する必要がある場合は、グラフをトポロジ的に並べ替えるよりも簡単な場合があります。
sleske 2009

グラフを非巡回にするためには、接続された各コンポーネントに、出力エッジのみを持つノードが含まれている必要があると言います。メインアルゴリズムで使用するために、有向グラフの(「強く」接続されたコンポーネントではなく)接続されたコンポーネントを見つけるアルゴリズムを推奨できますか?
kostmo 2010

@kostmo、グラフに複数の連結成分がある場合、最初のDFSのすべてのノードにアクセスするわけではありません。訪問したノードを追跡し、すべてのノードに到達するまで、訪問していないノードでアルゴリズムを繰り返します。これは基本的に、連結成分アルゴリズムがとにかく機能する方法です。
ジェイコンロッド2010

6
この回答の意図は正しいですが、DFSのスタックベースの実装を使用している場合、回答は混乱します。DFSの実装に使用されるスタックには、テストする正しい要素が含まれていません。祖先ノードのセットを追跡するために使用されるアルゴリズムにスタックを追加する必要があります。
セオドアマードック2012

あなたの答えについて複数質問があります。私はそれらをここに掲載:stackoverflow.com/questions/37582599/...
アリ

14

Introduction to Algorithms(第2版)の補題22.11は、次のように述べています。

有向グラフGは、Gの深さ優先探索で後縁が得られない場合に限り、非巡回です。


1
これは基本的にJayConrodの答えの短縮版です:-)。
sleske 2009

同じ本の問題の1つは、| V |があることを示唆しているようです。時間アルゴリズム。それはここで答えられます:stackoverflow.com/questions/526331/…–
ジャスティン

9

Solution1サイクルをチェックするカーンアルゴリズム。主なアイデア:度数がゼロのノードがキューに追加されるキューを維持します。次に、キューが空になるまでノードを1つずつ剥がします。ノードのインエッジが存在するかどうかを確認します。

解決策2:強連結成分をチェックするTarjanアルゴリズム

Solution3DFS。整数配列を使用して、ノードの現在のステータスにタグを付けます。つまり、0-このノードが以前にアクセスされたことがないことを意味します。-1-このノードが訪問され、その子ノードが訪問されていることを意味します。1-このノードにアクセスし、完了したことを意味します。したがって、DFSの実行中にノードのステータスが-1の場合は、サイクルが存在している必要があることを意味します。


1

ShuggyCoUkが提供するソリューションは、すべてのノードをチェックしない可能性があるため、不完全です。


def isDAG(nodes V):
    while there is an unvisited node v in V:
        bool cycleFound = dfs(v)
        if cyclefound:
            return false
    return true

これには時間の複雑さがありますO(n + m)またはO(n ^ 2)


私のは確かに間違っています-私はそれを削除したので、あなたは今少し文脈から外れているようです
ShuggyCoUk 2009

3
O(n + m)<= O(n + n)= O(2n)、O(2n)!= O(n ^ 2)
Artru 2011

@Artru隣接行列を使用する場合はO(n ^ 2)、グラフを表すために隣接リストを使用する場合はO(n + m)。
0x450 2016年

ええと...m = O(n^2)完全グラフには正確にm=n^2エッジがあるからです。つまり、O(n+m) = O(n + n^2) = O(n^2)です。
Alex Reinking 2016年

1

これが古いトピックであることは知っていますが、将来の検索者のために、私が作成したC#実装があります(最も効率的であるとは言えません!)。これは、単純な整数を使用して各ノードを識別するように設計されています。ノードオブジェクトが適切にハッシュされ、等しい場合は、好きなように装飾できます。

非常に深いグラフの場合、これは各ノードの深さでハッシュセットを作成するため、オーバーヘッドが高くなる可能性があります(それらは広範囲にわたって破壊されます)。

検索するノードとそのノードへのパスを入力します。

  • 単一のルートノードを持つグラフの場合、そのノードと空のハッシュセットを送信します
  • 複数のルートノードを持つグラフの場合、これをそれらのノードのforeachでラップし、反復ごとに新しい空のハッシュセットを渡します
  • 特定のノードの下のサイクルをチェックするときは、空のハッシュセットと一緒にそのノードを渡すだけです。

    private bool FindCycle(int node, HashSet<int> path)
    {
    
        if (path.Contains(node))
            return true;
    
        var extendedPath = new HashSet<int>(path) {node};
    
        foreach (var child in GetChildren(node))
        {
            if (FindCycle(child, extendedPath))
                return true;
        }
    
        return false;
    }
    

1

DFSの実行中はバックエッジがないはずです。DFSの実行中にすでにアクセスしたノードを追跡し、現在のノードと既存のノードの間にエッジが発生した場合、グラフにサイクルがあります。


1

グラフにサイクルがあるかどうかを確認するための迅速なコードは次のとおりです。

func isCyclic(G : Dictionary<Int,Array<Int>>,root : Int , var visited : Array<Bool>,var breadCrumb : Array<Bool>)-> Bool
{

    if(breadCrumb[root] == true)
    {
        return true;
    }

    if(visited[root] == true)
    {
        return false;
    }

    visited[root] = true;

    breadCrumb[root] = true;

    if(G[root] != nil)
    {
        for child : Int in G[root]!
        {
            if(isCyclic(G,root : child,visited : visited,breadCrumb : breadCrumb))
            {
                return true;
            }
        }
    }

    breadCrumb[root] = false;
    return false;
}


let G = [0:[1,2,3],1:[4,5,6],2:[3,7,6],3:[5,7,8],5:[2]];

var visited = [false,false,false,false,false,false,false,false,false];
var breadCrumb = [false,false,false,false,false,false,false,false,false];




var isthereCycles = isCyclic(G,root : 0, visited : visited, breadCrumb : breadCrumb)

アイデアは次のようになります。訪問したノードを追跡する配列と、現在のノードにつながったノードのマーカーとして機能する追加の配列を備えた通常のdfsアルゴリズム。これにより、ノードに対してdfsを実行するたびにマーカー配列内の対応するアイテムをtrueに設定します。これにより、既にアクセスしたノードが検出されるたびに、マーカー配列内の対応するアイテムがtrueであるかどうかを確認し、trueの場合は、それ自体を許可するノードの1つをチェックします(したがって、サイクル)、そしてトリックは、ノードのdfsが戻るたびに、対応するマーカーをfalseに戻すことです。これにより、別のルートから再度アクセスした場合でも、だまされません。


0

これが、ピールオフリーフノードアルゴリズムのルビー実装です

def detect_cycles(initial_graph, number_of_iterations=-1)
    # If we keep peeling off leaf nodes, one of two things will happen
    # A) We will eventually peel off all nodes: The graph is acyclic.
    # B) We will get to a point where there is no leaf, yet the graph is not empty: The graph is cyclic.
    graph = initial_graph
    iteration = 0
    loop do
        iteration += 1
        if number_of_iterations > 0 && iteration > number_of_iterations
            raise "prevented infinite loop"
        end

        if graph.nodes.empty?
            #puts "the graph is without cycles"
            return false
        end

        leaf_nodes = graph.nodes.select { |node| node.leaving_edges.empty? }

        if leaf_nodes.empty?
            #puts "the graph contain cycles"
            return true
        end

        nodes2 = graph.nodes.reject { |node| leaf_nodes.member?(node) }
        edges2 = graph.edges.reject { |edge| leaf_nodes.member?(edge.destination) }
        graph = Graph.new(nodes2, edges2)
    end
    raise "should not happen"
end

0

Googleのインタビューでこの質問がありました。

トポロジカルソート

トポロジカルソートを試みることができます。これはO(V + E)です。ここで、Vは頂点の数、Eはエッジの数です。有向グラフは、これが実行できる場合に限り、非循環です。

再帰的な葉の除去

リーフノードがなくなるまで再帰的に削除します。ノードが複数残っている場合は、サイクルがあります。私が間違えない限り、これはO(V ^ 2 + VE)です。

DFSスタイル〜O(n + m)

ただし、効率的なDFS風のアルゴリズム、最悪の場合のO(V + E)は次のとおりです。

function isAcyclic (root) {
    const previous = new Set();

    function DFS (node) {
        previous.add(node);

        let isAcyclic = true;
        for (let child of children) {
            if (previous.has(node) || DFS(child)) {
                isAcyclic = false;
                break;
            }
        }

        previous.delete(node);

        return isAcyclic;
    }

    return DFS(root);
}

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.