유 방향 그래프가 비순환인지 어떻게 확인합니까?


82

유 방향 그래프가 비순환인지 어떻게 확인합니까? 그리고 알고리즘은 어떻게 호출됩니까? 참고로 부탁드립니다.


SO에 대한 잘못된 답변을 "수정"하는 방법을 선호하는 또 다른 사례입니다.
Sparr

2
그래서, 음, 나는 그것을 찾는 데 필요한 시간에 주로 관심이 있습니다. 그래서 저는 추상 알고리즘이 필요합니다.
nes1983 09

모든 모서리를 횡단하고 모든 정점을 확인하여 하한이 O (| V | + | E |)가되도록해야합니다. DFS와 BFS는 모두 같은 복잡하지만 그건 ... 당신을 위해 스택을 관리하는 당신이 재귀이있는 경우 DFS는 코드를 쉽게
ShuggyCoUk

DFS는 동일한 복잡성 이 아닙니다 . 노드 {1 .. N} 및 {(a, b) | 형식의 간선이있는 그래프를 고려하십시오. a <b}. 그 그래프는 비순환 적이지만 DFS는 O (n!)
FryGuy

1
DFS는 O (n!)가 아닙니다. 각 노드를 한 번 방문하고 각 에지를 최대 두 번 방문합니다. 그래서 O (| V | + | E |) 또는 O (n).
Jay Conrod

답변:


95

나는 그래프를 토폴로지 적 으로 정렬 하려고하는데 , 할 수 없다면 사이클이있는 것입니다.


2
어떻게 투표가 없었습니까 ?? 노드 + 에지에서 선형이며 O (n ^ 2) 솔루션보다 훨씬 우수합니다!
Loren Pechtel

5
많은 경우 DFS (J. Conrod의 답변 참조)가 더 쉬울 수 있습니다. 특히 DFS를 수행해야 할 경우 특히 그렇습니다. 그러나 물론 이것은 상황에 따라 다릅니다.
sleske

1
위상 순서는 무한 루프에있을 것입니다하지만주기가 ... 발생하는 곳은 우리를 언급하지 않았다
Baradwaj Aryasomayajula

35

단순한 깊이 우선 검색은 주기를 찾기에 충분 하지 않습니다 . 주기가없는 DFS에서 노드를 여러 번 방문 할 수 있습니다. 시작 위치에 따라 전체 그래프를 방문하지 못할 수도 있습니다.

다음과 같이 그래프의 연결된 구성 요소에서주기를 확인할 수 있습니다. 나가는 가장자리 만있는 노드를 찾습니다. 그러한 노드가 없으면주기가 있습니다. 해당 노드에서 DFS를 시작합니다. 각 에지를 순회 할 때 에지가 이미 스택에있는 노드를 가리키는 지 확인하십시오. 이것은주기의 존재를 나타냅니다. 그러한 에지를 찾지 못하면 연결된 구성 요소에 사이클이없는 것입니다.

Rutger Prins가 지적했듯이 그래프가 연결되지 않은 경우 연결된 각 구성 요소에 대해 검색을 반복해야합니다.

참고로 Tarjan의 강력한 연결 구성 요소 알고리즘 은 밀접한 관련이 있습니다. 또한주기의 존재 여부를보고하는 것이 아니라주기를 찾는 데 도움이됩니다.


2
BTW : "이미 스택에있는 노드를 다시 가리키는"에지는 명백한 이유로 문헌에서 종종 "백 에지"라고합니다. 그리고 예, 이것은 그래프를 토폴로지 적으로 정렬하는 것보다 간단 할 수 있습니다. 특히 어쨌든 DFS를 수행해야하는 경우 더욱 그렇습니다.
sleske

그래프가 비순환이 되려면 연결된 각 구성 요소에 나가는 간선 만있는 노드가 있어야한다고 말합니다. 주 알고리즘에서 사용하기 위해 유 방향 그래프의 연결된 구성 요소 ( "강하게"연결된 구성 요소와 반대)를 찾는 알고리즘을 권장 할 수 있습니까?
kostmo

@kostmo, 그래프에 둘 이상의 연결된 구성 요소가있는 경우 첫 번째 DFS의 모든 노드를 방문하지 않습니다. 방문한 노드를 추적하고 모든 노드에 도달 할 때까지 방문하지 않은 노드로 알고리즘을 반복합니다. 이것은 기본적으로 연결된 구성 요소 알고리즘이 작동하는 방식입니다.
Jay Conrod

6
이 답변의 의도는 정확하지만 DFS의 스택 기반 구현을 사용하는 경우 대답은 혼란 스럽습니다. DFS를 구현하는 데 사용되는 스택에는 테스트 할 올바른 요소가 포함되지 않습니다. 상위 노드 집합을 추적하는 데 사용되는 알고리즘에 추가 스택을 추가해야합니다.
Theodore Murdock 2012

귀하의 답변에 대해 여러 가지 질문이 있습니다. 나는 그것들을 여기에 올렸다 : stackoverflow.com/questions/37582599/…
Ari

14

Introduction to Algorithms(제 2 판) 의 Lemma 22.11은 다음과 같이 설명합니다.

유 방향 그래프 G는 G의 깊이 우선 검색이 뒷 모서리를 생성하지 않는 경우에만 비순환입니다.


1
이것은 기본적으로 Jay Conrod의 답변 :-)의 축약 버전입니다.
sleske

같은 책의 문제 중 하나가 | V | 시간 알고리즘. 그것은 여기에 대한 답변 : stackoverflow.com/questions/526331/...
저스틴

9

Solution1Kahn 알고리즘은주기를 확인합니다 . 주요 아이디어 : 정도가 0 인 노드가 대기열에 추가되는 대기열을 유지합니다. 그런 다음 대기열이 비워 질 때까지 노드를 하나씩 떼어냅니다. 노드의 인에지가 존재하는지 확인하십시오.

Solution2 : 강력한 연결 구성 요소를 확인하는 Tarjan 알고리즘 .

솔루션 3 : DFS . 정수 배열을 사용하여 노드의 현재 상태에 태그를 지정합니다. 즉 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

3
O (N + m) <= O (N + N) = O (2N), O (2N) = O (N ^ 2)!
Artru

@Artru O (n ^ 2)는 인접 행렬을 사용할 때, O (n + m)는 그래프를 표현하기 위해 인접 목록을 사용할 때입니다.
0x450

음 .. m = O(n^2)전체 그래프에는 정확히 m=n^2모서리 가 있기 때문 입니다. 그래서 그것은 O(n+m) = O(n + n^2) = O(n^2).
Alex Reinking

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인지 확인하고 참이면 자신에게 허용 된 노드 중 하나를 확인합니다 (따라서 그리고 트릭은 노드의 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-esque 알고리즘, 최악의 경우 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.