유 방향 그래프에서 모든 사이클 찾기 그래프로 모든 사이클을 어떻게 찾을

주어진 노드에서 /로 지정된 그래프로 모든 사이클을 어떻게 찾을 수 있습니까?

예를 들어 다음과 같은 것을 원합니다.

A->B->A
A->B->C->A

B-> C-> B



답변

검색에서이 페이지를 찾았고 사이클이 강하게 연결된 구성 요소와 같지 않기 때문에 검색을 계속하고 마침내 유향 그래프의 모든 (기본) 사이클을 나열하는 효율적인 알고리즘을 찾았습니다. 이 문서는 Donald B. Johnson이 작성했으며 다음 링크에서 찾을 수 있습니다.

http://www.cs.tufts.edu/comp/150GA/homeworks/hw1/Johnson%2075.PDF

Java 구현은 다음에서 찾을 수 있습니다.

http://normalisiert.de/code/java/elementaryCycles.zip

Johnson의 알고리즘에 대한 Mathematica 데모는 여기 에서 찾을 수 있으며 , 구현은 오른쪽에서 다운로드 할 수 있습니다 ( “저자 코드 다운로드” ).

참고 : 실제로이 문제에 대한 많은 알고리즘이 있습니다. 이들 중 일부는이 기사에 나열되어 있습니다.

http://dx.doi.org/10.1137/0205007

기사에 따르면 Johnson의 알고리즘이 가장 빠릅니다.


답변

역 추적을 통한 깊이있는 첫 번째 검색이 여기에서 작동합니다. 이전에 노드를 방문했는지 여부를 추적하려면 부울 값 배열을 유지하십시오. 새 노드가 부족하여 (이미 노드를 치지 않은 상태로) 이동 한 경우 역 추적하고 다른 분기를 시도하십시오.

그래프를 나타내는 인접 목록이 있으면 DFS를 쉽게 구현할 수 있습니다. 예를 들어 adj [A] = {B, C}는 B와 C가 A의 자식임을 나타냅니다.

예를 들어, 아래의 의사 코드입니다. “start”는 시작한 노드입니다.

dfs(adj,node,visited):
  if (visited[node]):
    if (node == start):
      "found a path"
    return;
  visited[node]=YES;
  for child in adj[node]:
    dfs(adj,child,visited)
  visited[node]=NO;

시작 노드와 함께 위 함수를 호출하십시오.

visited = {}
dfs(adj,start,visited)

답변

우선-실제로 모든 사이클을 찾으려고하지는 않습니다 .1이 있으면 무한의 수가 있기 때문입니다. 예를 들어 ABA, ABABA 등. 또는 2 사이클을 8와 같은 사이클 등으로 결합하는 것이 가능할 수 있습니다. 의미있는 접근 방식은 소위 단순한 사이클을 찾는 것입니다. 시작 / 종료 지점에서. 그런 다음 원하는 경우 간단한 사이클 조합을 생성 할 수 있습니다.

유 방향 그래프에서 모든 단순 사이클을 찾기위한 기본 알고리즘 중 하나는 다음과 같습니다. 그래프에서 모든 단순 경로 (자체를 가로 지르지 않는 경로)의 깊이 우선 순회를 수행합니다. 현재 노드의 스택에 후속 노드가있을 때마다 간단한주기가 감지됩니다. 이는 식별 된 후속 작업으로 시작하여 스택의 상단으로 끝나는 스택의 요소로 구성됩니다. 모든 단순 경로의 깊이 우선 순회는 깊이 우선 검색과 유사하지만 현재 스택에있는 노드 이외의 방문한 노드를 중지 점으로 표시 / 기록하지 않습니다.

위의 무차별 대입 알고리즘은 매우 비효율적이며 사이클의 여러 복사본을 생성합니다. 그러나 성능을 향상시키고 사이클 복제를 피하기 위해 다양한 개선 사항을 적용하는 것은 여러 가지 실용적인 알고리즘의 출발점입니다. 나는이 알고리즘들이 교과서와 웹에서 쉽게 구할 수 없다는 것을 얼마 전에 알게 된 것에 놀랐다. 그래서 오픈 소스 Java 라이브러리 ( http://code.google.com/p/niographs/) 에서 무 방향 그래프로 4 개의 알고리즘과 1 사이클의 알고리즘을 구현했습니다 .

BTW, 나는 무향 그래프를 언급했기 때문에 : 그 알고리즘은 다릅니다. 스패닝 트리를 구축 한 다음 트리의 일부가 아닌 모든 에지는 트리의 일부 에지와 함께 간단한주기를 형성합니다. 사이클은 이런 방식으로 소위 사이클베이스를 형성합니다. 그런 다음 2 개 이상의 고유 한 기본주기를 결합하여 모든 단순주기를 찾을 수 있습니다. 자세한 내용은 예 : http://dspace.mit.edu/bitstream/handle/1721.1/68106/FTL_R_1982_07.pdf를 참조하십시오 .


답변

이 문제를 해결하기 위해 찾은 가장 간단한 선택은이라는 python lib를 사용하는 것 networkx입니다.

이 질문에 대한 최선의 답변에서 언급 한 Johnson의 알고리즘을 구현하지만 실행이 매우 간단합니다.

간단히 말해 다음이 필요합니다.

import networkx as nx
import matplotlib.pyplot as plt

# Create Directed Graph
G=nx.DiGraph()

# Add a list of nodes:
G.add_nodes_from(["a","b","c","d","e"])

# Add a list of edges:
G.add_edges_from([("a","b"),("b","c"), ("c","a"), ("b","d"), ("d","e"), ("e","a")])

#Return a list of cycles described as a list o nodes
list(nx.simple_cycles(G))

답변 : [[ ‘a’, ‘b’, ‘d’, ‘e’], [ ‘a’, ‘b’, ‘c’]]


답변

명확히하기 위해 :

  1. 강력하게 연결된 구성 요소는 그래프에서 가능한 모든주기가 아니라 하나 이상의주기를 갖는 모든 하위 그래프를 찾습니다. 예를 들어, 강력하게 연결된 모든 구성 요소를 가져 와서 각 구성 요소를 하나의 노드 (예 : 구성 요소 당 노드)로 축소 / 그룹화 / 병합하면 사이클이없는 트리가 생깁니다 (실제로 DAG). 각 구성 요소 (기본적으로 하나 이상의주기를 가진 하위 그래프)는 내부에 더 많은 가능한주기를 포함 할 수 있으므로 SCC는 가능한 모든주기를 찾지 못하고 하나 이상의주기를 가진 가능한 모든 그룹을 찾게됩니다. 그래프에 사이클이 없습니다.

  2. 그래프에서 모든 간단한 사이클 을 찾으려면 Johnson의 알고리즘이 후보입니다.


답변

나는 이것을 인터뷰 질문으로 한 번 받았는데, 이것이 당신에게 일어난 것으로 의심되며 당신은 여기에 도움을 요청하고 있습니다. 문제를 세 가지 질문으로 나누면 더 쉬워집니다.

  1. 다음 유효한 경로를 어떻게 결정합니까
  2. 포인트가 사용되었는지 어떻게 알 수 있습니까
  3. 같은 지점을 다시 교차하는 것을 피하는 방법

문제 1) 반복자 패턴을 사용하여 경로 결과를 반복하는 방법을 제공하십시오. 다음 경로를 얻기 위해 논리를 배치하기에 좋은 장소는 아마도 반복자의 “moveNext”일 것입니다. 유효한 경로를 찾으려면 데이터 구조에 따라 다릅니다. 나에게 그것은 유효한 경로 가능성으로 가득 찬 SQL 테이블이므로 소스가 주어진 유효한 목적지를 얻기 위해 쿼리를 작성해야했습니다.

문제 2) 각 노드를 찾을 때 컬렉션에 넣을 때마다 밀어 넣습니다. 즉, 작성중인 컬렉션을 심문하여 특정 지점에 대해 “두 배로 빠르게”돌아가는지 확인할 수 있습니다.

문제 3) 어느 시점에서든, 배가 두 배가되면 컬렉션에서 물건을 꺼내 “백업”할 수 있습니다. 그런 다음 해당 지점부터 다시 “앞으로 이동”하십시오.

해킹 : Sql Server 2008을 사용하는 경우 데이터를 트리에서 구조화하면이를 신속하게 해결하는 데 사용할 수있는 새로운 “계층 구조”가 있습니다.


답변

후면 모서리가있는 DFS 기반 변형은 실제로주기를 찾을 수 있지만 대부분의 경우 최소 가 아닙니다. 주기 . 일반적으로 DFS는주기가 있지만 실제로주기를 찾기에 충분하지 않다는 플래그를 제공합니다. 예를 들어, 두 개의 모서리를 공유하는 5 개의 다른 사이클을 상상해보십시오. 역 추적 변형을 포함하여 DFS 만 사용하여주기를 식별하는 간단한 방법은 없습니다.

Johnson의 알고리즘은 실제로 모든 고유 한 간단한주기를 제공하며 시간과 공간이 복잡합니다.

그러나 최소 사이클을 찾으려면 (정점을 통과하는 하나 이상의 사이클이 있고 최소 사이클을 찾는 데 관심이 있음을 의미 함) 그래프가 크지 않은 경우 아래의 간단한 방법을 사용해보십시오. Johnson에 비해 매우 간단하지만 다소 느립니다.

그래서, 하나 절대적으로 MINIMAL 사이클을 찾을 수있는 가장 쉬운 방법은 인접 행렬을 사용하여 모든 정점 사이의 최소한의 경로를 찾기 위해 플로이드의 알고리즘을 사용하는 것입니다. 이 알고리즘은 Johnson만큼 최적의 위치는 아니지만 너무 간단하고 내부 루프가 너무 작아서 작은 그래프 (<= 50-100 노드)의 경우에는 사용하는 것이 좋습니다. 부모 추적을 사용하는 경우 시간 복잡도는 O (n ^ 3), 공간 복잡도 O (n ^ 2), 그렇지 않은 경우 O (1)입니다. 우선주기가 있으면 질문에 대한 답을 찾으십시오. 알고리즘은 매우 간단합니다. 아래는 스칼라의 스 니펫입니다.

  val NO_EDGE = Integer.MAX_VALUE / 2

  def shortestPath(weights: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        weights(i)(j) = throughK
      }
    }
  }

원래이 알고리즘은 가중치 에지 그래프에서 작동하여 모든 노드 쌍 사이의 모든 최단 경로 (따라서 가중치 인수)를 찾습니다. 제대로 작동하려면 노드 사이에 방향이 지정된 가장자리가 있으면 1을 제공하고 그렇지 않으면 NO_EDGE를 제공해야합니다. 알고리즘이 실행 된 후이 노드가 값과 동일한 길이의주기에 참여하는 것보다 NO_EDGE보다 작은 값이 있으면 기본 대각선을 확인할 수 있습니다. 동일한주기의 다른 모든 노드는 동일한 값을 갖습니다 (주 대각선).

사이클 자체를 재구성하려면 부모 추적과 함께 약간 수정 된 알고리즘 버전을 사용해야합니다.

  def shortestPath(weights: Array[Array[Int]], parents: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        parents(i)(j) = k
        weights(i)(j) = throughK
      }
    }
  }

부모 행렬은 처음에 꼭짓점 사이에 가장자리가 있으면 가장자리 셀에 소스 꼭짓점 인덱스를 포함해야하며, 그렇지 않으면 -1입니다. 함수가 반환 된 후 각 모서리에 대해 최단 경로 트리에서 상위 노드를 참조하게됩니다. 그런 다음 실제주기를 쉽게 복구 할 수 있습니다.

우리는 모든 최소한의 사이클을 찾기 위해 다음과 같은 프로그램을 가지고 있습니다.

  val NO_EDGE = Integer.MAX_VALUE / 2;

  def shortestPathWithParentTracking(
         weights: Array[Array[Int]],
         parents: Array[Array[Int]]) = {
    for (k <- weights.indices;
         i <- weights.indices;
         j <- weights.indices) {
      val throughK = weights(i)(k) + weights(k)(j)
      if (throughK < weights(i)(j)) {
        parents(i)(j) = parents(i)(k)
        weights(i)(j) = throughK
      }
    }
  }

  def recoverCycles(
         cycleNodes: Seq[Int],
         parents: Array[Array[Int]]): Set[Seq[Int]] = {
    val res = new mutable.HashSet[Seq[Int]]()
    for (node <- cycleNodes) {
      var cycle = new mutable.ArrayBuffer[Int]()
      cycle += node
      var other = parents(node)(node)
      do {
        cycle += other
        other = parents(other)(node)
      } while(other != node)
      res += cycle.sorted
    }
    res.toSet
  }

결과를 테스트하는 작은 주요 방법

  def main(args: Array[String]): Unit = {
    val n = 3
    val weights = Array(Array(NO_EDGE, 1, NO_EDGE), Array(NO_EDGE, NO_EDGE, 1), Array(1, NO_EDGE, NO_EDGE))
    val parents = Array(Array(-1, 1, -1), Array(-1, -1, 2), Array(0, -1, -1))
    shortestPathWithParentTracking(weights, parents)
    val cycleNodes = parents.indices.filter(i => parents(i)(i) < NO_EDGE)
    val cycles: Set[Seq[Int]] = recoverCycles(cycleNodes, parents)
    println("The following minimal cycle found:")
    cycles.foreach(c => println(c.mkString))
    println(s"Total: ${cycles.size} cycle found")
  }

출력은

The following minimal cycle found:
012
Total: 1 cycle found