Tìm tất cả các chu kỳ trong đồ thị có hướng


198

Làm cách nào tôi có thể tìm thấy (lặp đi lặp lại) TẤT CẢ các chu kỳ trong đồ thị có hướng từ / đến một nút cho trước?

Ví dụ, tôi muốn một cái gì đó như thế này:

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

nhưng không phải: B-> C-> B


1
Bài tập về nhà tôi giả sử? me.utexas.edu/~bard/IP/Handouts/ Motorcycle.pdf không phải đó không phải là một câu hỏi hợp lệ :)
ShuggyCoUk

5
Lưu ý rằng đây là ít nhất NP Hard. Có thể là PSPACE, tôi phải suy nghĩ về nó, nhưng quá sớm vào buổi sáng vì lý thuyết phức tạp B-)
Brian Postow

2
Nếu đồ thị đầu vào của bạn có v đỉnh và cạnh e thì có 2 ^ (e - v +1) -1 chu kỳ khác nhau (mặc dù không phải tất cả đều có thể là chu kỳ đơn giản). Đó là khá nhiều - bạn có thể không muốn viết rõ ràng tất cả chúng. Ngoài ra, vì kích thước đầu ra là hàm mũ, độ phức tạp của thuật toán không thể là đa thức. Tôi nghĩ rằng vẫn không có câu trả lời cho câu hỏi này.
CygnusX1

1
Lựa chọn tốt nhất của tôi đối với tôi là thế này: personal.kent.edu/~rmuhamma/Algorithms/MyAlgorithms/GraphAlgor/...
Melsi

Câu trả lời:


105

Tôi tìm thấy trang này trong tìm kiếm của mình và vì các chu kỳ không giống như các thành phần được kết nối mạnh mẽ, tôi tiếp tục tìm kiếm và cuối cùng, tôi đã tìm thấy một thuật toán hiệu quả liệt kê tất cả các chu kỳ (cơ bản) của đồ thị có hướng. Đó là từ Donald B. Johnson và bài báo có thể được tìm thấy trong liên kết sau:

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

Một triển khai java có thể được tìm thấy trong:

http: // n normalisiert.de/code/java/elementaryC đua.zip

Một Mathematica cuộc biểu tình của thuật toán Johnson có thể tìm thấy ở đây , thực hiện có thể được tải về từ bên phải ( "Tải về tác giả mã" ).

Lưu ý: Trên thực tế, có nhiều thuật toán cho vấn đề này. Một số trong số họ được liệt kê trong bài viết này:

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

Theo bài báo, thuật toán của Johnson là thuật toán nhanh nhất.


1
Tôi thấy nó thật rắc rối khi thực hiện từ bài báo, và cuối cùng thì phần mềm này vẫn đòi hỏi phải thực hiện Tarjan. Và mã Java cũng gớm ghiếc. :(
Gleno

7
@Gleno Vâng, nếu bạn có nghĩa là bạn có thể sử dụng Tarjan để tìm tất cả các chu kỳ trong biểu đồ thay vì thực hiện phần còn lại, bạn đã sai. Tại đây , bạn có thể thấy sự khác biệt giữa các thành phần được kết nối mạnh và tất cả các chu kỳ (Các chu kỳ cd và gh sẽ không được trả về bởi alg của Tarjan) (@ batbrat Câu trả lời về sự nhầm lẫn của bạn cũng được ẩn ở đây: Tất cả các chu kỳ có thể không được Tarjan trả lại alg, vì vậy độ phức tạp của nó có thể nhỏ hơn hàm mũ). Mã Java có thể tốt hơn, nhưng nó đã tiết kiệm cho tôi nỗ lực triển khai từ bài báo.
eminsenay

4
Câu trả lời này tốt hơn nhiều so với câu trả lời được chọn. Tôi đã vật lộn khá lâu trong khi cố gắng tìm ra cách để có được tất cả các chu kỳ đơn giản từ các thành phần được kết nối mạnh mẽ. Hóa ra điều này là không tầm thường. Bài báo của Johnson chứa một thuật toán tuyệt vời, nhưng hơi khó khăn để lội qua. Tôi đã xem triển khai Java và tự triển khai trong Matlab. Mã này có sẵn tại gist.github.com/1260153 .
codehippo

5
@moteutsch: Có thể tôi đang thiếu một cái gì đó, nhưng theo bài báo của Johnson (và các nguồn khác), một chu kỳ là cơ bản nếu không có đỉnh (ngoài bắt đầu / kết thúc) xuất hiện nhiều hơn một lần. Theo định nghĩa đó, không phải là A->B->C->Atiểu học quá?
psmears

9
Lưu ý cho bất cứ ai sử dụng python cho việc này: thuật toán Johnson được triển khai như simple_cycletrong networkx.
Joel

35

Độ sâu tìm kiếm đầu tiên với quay lui nên hoạt động ở đây. Giữ một mảng các giá trị boolean để theo dõi xem bạn đã truy cập một nút trước đó chưa. Nếu bạn hết các nút mới để đi đến (mà không nhấn một nút bạn đã có), thì chỉ cần quay lại và thử một nhánh khác.

DFS rất dễ thực hiện nếu bạn có một danh sách kề để thể hiện biểu đồ. Ví dụ: adj [A] = {B, C} chỉ ra rằng B và C là con của A.

Ví dụ, mã giả dưới đây. "bắt đầu" là nút bạn bắt đầu từ.

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;

Gọi hàm trên với nút bắt đầu:

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

2
Cảm ơn. Tôi thích cách tiếp cận này với một số phương pháp khác được lưu ý ở đây vì nó đơn giản (r) để hiểu và có độ phức tạp thời gian hợp lý, mặc dù có lẽ không tối ưu.
redcalx

1
Làm thế nào để tìm thấy tất cả các chu kỳ?
bão não

3
if (node == start): - những gì node and starttrong cuộc gọi đầu tiên
cơn bão não

2
@ user1988876 Điều này dường như tìm thấy tất cả các chu kỳ liên quan đến một đỉnh đã cho (sẽ là start). Nó bắt đầu từ đỉnh đó và thực hiện DFS cho đến khi nó quay trở lại đỉnh đó một lần nữa, sau đó nó biết nó đã tìm thấy một chu kỳ. Nhưng nó không thực sự tạo ra các chu kỳ, chỉ là một số trong số chúng (nhưng sửa đổi nó để làm điều đó thay vào đó không quá khó khăn).
Bernhard Barker

1
@ user1988876 Chà, nó chỉ in "tìm thấy một đường dẫn" một số lần bằng với số chu kỳ được tìm thấy (điều này có thể dễ dàng được thay thế bằng số đếm). Có, nó sẽ phát hiện chu kỳ chỉ từ start. Bạn không thực sự cần phải xóa các cờ đã truy cập vì mỗi cờ đã truy cập sẽ bị xóa do visited[node]=NO;. Nhưng hãy nhớ rằng nếu bạn có một chu kỳ A->B->C->A, bạn sẽ phát hiện ra 3 lần, như startcó thể là 3 trong số đó. Một ý tưởng để ngăn chặn điều này là có một mảng được truy cập khác trong đó mọi nút đã là startnút tại một thời điểm nào đó được đặt và sau đó bạn không truy cập lại các mảng này.
Bernhard Barker

23

Trước hết - bạn không thực sự muốn thử tìm tất cả các chu kỳ theo nghĩa đen bởi vì nếu có 1 thì có vô số các chu kỳ đó. Ví dụ: ABA, ABABA, v.v. Hoặc có thể kết hợp 2 chu kỳ thành một chu kỳ giống như 8, v.v ... Cách tiếp cận có ý nghĩa là tìm kiếm tất cả những cái gọi là chu kỳ đơn giản - những chu trình không tự vượt qua ngoại trừ ở điểm bắt đầu / kết thúc. Sau đó, nếu bạn muốn bạn có thể tạo ra sự kết hợp của các chu kỳ đơn giản.

Một trong những thuật toán cơ bản để tìm tất cả các chu trình đơn giản trong đồ thị có hướng là: Thực hiện một giao dịch theo chiều sâu của tất cả các đường dẫn đơn giản (những đường không tự đi qua) trong biểu đồ. Mỗi khi nút hiện tại có một người kế thừa trên ngăn xếp, một chu kỳ đơn giản được phát hiện. Nó bao gồm các phần tử trên ngăn xếp bắt đầu bằng phần kế thừa đã xác định và kết thúc bằng phần trên cùng của ngăn xếp. Độ sâu ngang qua đầu tiên của tất cả các đường dẫn đơn giản tương tự như tìm kiếm đầu tiên theo chiều sâu nhưng bạn không đánh dấu / ghi lại các nút đã truy cập ngoài các nút hiện tại trên ngăn xếp làm điểm dừng.

Thuật toán brute force ở trên là không hiệu quả khủng khiếp và thêm vào đó tạo ra nhiều bản sao của các chu kỳ. Tuy nhiên, đây là điểm khởi đầu của nhiều thuật toán thực tế áp dụng các cải tiến khác nhau để cải thiện hiệu suất và tránh trùng lặp chu kỳ. Tôi đã rất ngạc nhiên khi phát hiện ra rằng các thuật toán này không có sẵn trong sách giáo khoa và trên web. Vì vậy, tôi đã thực hiện một số nghiên cứu và triển khai 4 thuật toán như vậy và 1 thuật toán cho các chu kỳ trong các đồ thị không bị chặn trong thư viện Java mã nguồn mở tại đây: http://code.google.com.vn/p/niographs/ .

BTW, vì tôi đã đề cập đến các đồ thị vô hướng: Thuật toán cho những đồ thị này là khác nhau. Xây dựng một cây bao trùm và sau đó mọi cạnh không phải là một phần của cây tạo thành một chu kỳ đơn giản cùng với một số cạnh trong cây. Các chu trình được tìm thấy theo cách này tạo thành một cái gọi là cơ sở chu kỳ. Tất cả các chu trình đơn giản sau đó có thể được tìm thấy bằng cách kết hợp 2 hoặc nhiều chu kỳ cơ sở khác biệt. Để biết thêm chi tiết, hãy xem ví dụ này: http://dspace.mit.edu/bitstream/handle/1721.1/68106/FTL_R_1982_07.pdf .


Như một ví dụ làm thế nào để sử dụng jgraphtđược sử dụng trong http://code.google.com/p/niographs/bạn có thể lấy ví dụ từ github.com/jgrapht/jgrapht/wiki/DirectedGraphDemo
Vishrant

19

Sự lựa chọn đơn giản nhất mà tôi tìm thấy để giải quyết vấn đề này là sử dụng lib python được gọi networkx.

Nó thực hiện thuật toán của Johnson được đề cập trong câu trả lời hay nhất của câu hỏi này nhưng nó khá đơn giản để thực hiện.

Nói tóm lại, bạn cần những điều sau đây:

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))

Trả lời: [['a', 'b', 'd', 'e'], ['a', 'b', 'c']]

nhập mô tả hình ảnh ở đây


1
Bạn cũng có thể chuyển từ điển sang biểu đồ mạng:nx.DiGraph({'a': ['b'], 'b': ['c','d'], 'c': ['a'], 'd': ['e'], 'e':['a']})
Luke Miles

Làm thế nào để tôi xác định một đỉnh bắt đầu?
nosense

5

Làm rõ:

  1. Các thành phần được kết nối mạnh mẽ sẽ tìm thấy tất cả các sơ đồ con có ít nhất một chu kỳ trong đó, không phải tất cả các chu kỳ có thể có trong biểu đồ. ví dụ: nếu bạn lấy tất cả các thành phần được kết nối mạnh và thu gọn / nhóm / hợp nhất mỗi một trong số chúng thành một nút (tức là một nút cho mỗi thành phần), bạn sẽ nhận được một cây không có chu kỳ (thực tế là DAG). Mỗi thành phần (về cơ bản là một sơ đồ con có ít nhất một chu kỳ trong đó) có thể chứa nhiều chu kỳ có thể hơn trong nội bộ, vì vậy SCC sẽ KHÔNG tìm thấy tất cả các chu kỳ có thể, nó sẽ tìm tất cả các nhóm có thể có ít nhất một chu kỳ và nếu bạn nhóm chúng, sau đó đồ thị sẽ không có chu kỳ.

  2. để tìm tất cả các chu trình đơn giản trong biểu đồ, như những người khác đã đề cập, thuật toán của Johnson là một ứng cử viên.


3

Tôi đã được đưa ra điều này như một câu hỏi phỏng vấn một lần, tôi nghi ngờ điều này đã xảy ra với bạn và bạn đang đến đây để được giúp đỡ. Chia vấn đề thành ba câu hỏi và nó trở nên dễ dàng hơn.

  1. Làm thế nào để bạn xác định tuyến hợp lệ tiếp theo
  2. Làm thế nào để bạn xác định nếu một điểm đã được sử dụng
  3. Làm thế nào để bạn tránh vượt qua cùng một điểm một lần nữa

Vấn đề 1) Sử dụng mẫu lặp để cung cấp cách lặp kết quả tuyến đường. Một nơi tốt để đặt logic để có được tuyến đường tiếp theo có lẽ là "moveNext" của trình lặp của bạn. Để tìm một tuyến hợp lệ, nó phụ thuộc vào cấu trúc dữ liệu của bạn. Đối với tôi đó là một bảng sql đầy đủ các khả năng tuyến hợp lệ vì vậy tôi phải xây dựng một truy vấn để có được các đích hợp lệ được cung cấp một nguồn.

Vấn đề 2) Đẩy từng nút khi bạn tìm thấy chúng vào một bộ sưu tập khi bạn nhận được chúng, điều này có nghĩa là bạn có thể thấy nếu bạn đang "nhân đôi" lại một điểm rất dễ dàng bằng cách thẩm vấn bộ sưu tập bạn đang xây dựng.

Vấn đề 3) Nếu tại bất kỳ thời điểm nào bạn thấy bạn đang nhân đôi trở lại, bạn có thể bật mọi thứ ra khỏi bộ sưu tập và "sao lưu". Sau đó, từ đó cố gắng "tiến về phía trước" một lần nữa.

Hack: nếu bạn đang sử dụng Sql Server 2008, có một số thứ "phân cấp" mới mà bạn có thể sử dụng để giải quyết nhanh vấn đề này nếu bạn cấu trúc dữ liệu của mình trong một cây.


3

Các biến thể dựa trên DFS có cạnh sau sẽ tìm thấy chu kỳ thực sự, nhưng trong nhiều trường hợp, nó sẽ KHÔNG phải là chu kỳ tối thiểu . Nói chung DFS cung cấp cho bạn cờ rằng có một chu kỳ nhưng nó không đủ tốt để thực sự tìm thấy các chu kỳ. Ví dụ, hãy tưởng tượng 5 chu kỳ khác nhau chia sẻ hai cạnh. Không có cách đơn giản nào để xác định các chu kỳ chỉ sử dụng DFS (bao gồm các biến thể quay lui).

Thuật toán của Johnson thực sự mang lại tất cả các chu kỳ đơn giản độc đáo và có độ phức tạp không gian và thời gian tốt.

Nhưng nếu bạn chỉ muốn tìm chu kỳ TỐI THIỂU (có nghĩa là có thể có nhiều hơn một chu kỳ đi qua bất kỳ đỉnh nào và chúng tôi quan tâm đến việc tìm các chu kỳ tối thiểu) VÀ biểu đồ của bạn không lớn lắm, bạn có thể thử sử dụng phương pháp đơn giản dưới đây. Nó rất đơn giản nhưng khá chậm so với Johnson.

Vì vậy, một trong những cách hoàn toàn dễ dàng nhất để tìm chu kỳ TỐI THIỂU là sử dụng thuật toán của Floyd để tìm các đường dẫn tối thiểu giữa tất cả các đỉnh bằng ma trận kề. Thuật toán này không ở đâu tối ưu như của Johnson, nhưng nó rất đơn giản và vòng lặp bên trong của nó chặt chẽ đến mức đối với các đồ thị nhỏ hơn (<= 50-100 nút) hoàn toàn có ý nghĩa khi sử dụng nó. Độ phức tạp thời gian là O (n ^ 3), độ phức tạp không gian O (n ^ 2) nếu bạn sử dụng theo dõi cha mẹ và O (1) nếu bạn không. Trước hết, hãy tìm câu trả lời cho câu hỏi nếu có một chu kỳ. Thuật toán rất đơn giản. Dưới đây là đoạn trích trong Scala.

  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
      }
    }
  }

Ban đầu thuật toán này hoạt động trên biểu đồ cạnh có trọng số để tìm tất cả các đường dẫn ngắn nhất giữa tất cả các cặp nút (do đó là đối số trọng số). Để nó hoạt động chính xác, bạn cần cung cấp 1 nếu có một cạnh được định hướng giữa các nút hoặc NO_EDGE nếu không. Sau khi thực hiện thuật toán, bạn có thể kiểm tra đường chéo chính, nếu có giá trị nhỏ hơn NO_EDGE so với nút này tham gia vào một chu kỳ có độ dài bằng giá trị. Mọi nút khác của cùng một chu kỳ sẽ có cùng giá trị (trên đường chéo chính).

Để tự xây dựng lại chu trình, chúng ta cần sử dụng phiên bản thuật toán được sửa đổi một chút với theo dõi cha mẹ.

  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
      }
    }
  }

Ma trận cha mẹ ban đầu nên chứa chỉ số đỉnh nguồn trong một ô cạnh nếu có một cạnh giữa các đỉnh và -1 nếu không. Sau khi hàm trả về, với mỗi cạnh bạn sẽ có tham chiếu đến nút cha trong cây đường dẫn ngắn nhất. Và sau đó thật dễ dàng để phục hồi chu kỳ thực tế.

Tất cả trong tất cả chúng ta có chương trình sau đây để tìm tất cả các chu kỳ tối thiểu

  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
  }

và một phương pháp chính nhỏ chỉ để kiểm tra kết quả

  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")
  }

và đầu ra là

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

2

Trong trường hợp đồ thị không có hướng , một bài báo được xuất bản gần đây ( Danh sách tối ưu các chu trình và đường đi trong đồ thị không có hướng ) cung cấp một giải pháp tối ưu không có triệu chứng. Bạn có thể đọc nó ở đây http://arxiv.org/abs/1205.2766 hoặc tại đây http://dl.acm.org/citation.cfm?id=2627951 Tôi biết nó không trả lời câu hỏi của bạn, nhưng vì tiêu đề của Câu hỏi của bạn không đề cập đến hướng, nó vẫn có thể hữu ích cho tìm kiếm của Google


1

Bắt đầu tại nút X và kiểm tra tất cả các nút con (nút cha và nút con là tương đương nếu không bị chặn). Đánh dấu các nút con đó là con của X. Từ bất kỳ nút con nào như vậy A, đánh dấu các con của chúng là A, X ', trong đó X' được đánh dấu là cách 2 bước.). Nếu sau đó bạn nhấn X và đánh dấu nó là con của X '', điều đó có nghĩa là X đang ở trong chu kỳ 3 nút. Quay lại với cha mẹ thật dễ dàng (vì thực tế, thuật toán không hỗ trợ cho việc này vì vậy bạn sẽ tìm thấy cha mẹ nào có X ').

Lưu ý: Nếu đồ thị không có hướng hoặc có bất kỳ cạnh hai chiều, thuật toán này sẽ phức tạp hơn, giả sử bạn không muốn đi qua cùng một cạnh hai lần trong một chu kỳ.


1

Nếu điều bạn muốn là tìm tất cả các mạch cơ bản trong biểu đồ, bạn có thể sử dụng thuật toán EC, của JAMES C. TIERNAN, được tìm thấy trên một tờ giấy từ năm 1970.

Các rất ban đầu thuật toán EC như tôi cố gắng thực hiện nó trong php (hy vọng không có lỗi được hiển thị bên dưới). Nó có thể tìm thấy các vòng lặp quá nếu có bất kỳ. Các mạch trong triển khai này (cố gắng sao chép bản gốc) là các phần tử khác không. Không ở đây là viết tắt của không tồn tại (null như chúng ta biết).

Ngoài ra, bên dưới còn có một triển khai khác giúp thuật toán độc lập hơn, điều này có nghĩa là các nút có thể bắt đầu từ bất cứ đâu ngay cả từ các số âm, ví dụ -4, -3, -2, .. vv

Trong cả hai trường hợp, các nút là tuần tự.

Bạn có thể cần nghiên cứu bài báo gốc, Thuật toán mạch tiểu học James C. Tiernan

<?php
echo  "<pre><br><br>";

$G = array(
        1=>array(1,2,3),
        2=>array(1,2,3),
        3=>array(1,2,3)
);


define('N',key(array_slice($G, -1, 1, true)));
$P = array(1=>0,2=>0,3=>0,4=>0,5=>0);
$H = array(1=>$P, 2=>$P, 3=>$P, 4=>$P, 5=>$P );
$k = 1;
$P[$k] = key($G);
$Circ = array();


#[Path Extension]
EC2_Path_Extension:
foreach($G[$P[$k]] as $j => $child ){
    if( $child>$P[1] and in_array($child, $P)===false and in_array($child, $H[$P[$k]])===false ){
    $k++;
    $P[$k] = $child;
    goto EC2_Path_Extension;
}   }

#[EC3 Circuit Confirmation]
if( in_array($P[1], $G[$P[$k]])===true ){//if PATH[1] is not child of PATH[current] then don't have a cycle
    $Circ[] = $P;
}

#[EC4 Vertex Closure]
if($k===1){
    goto EC5_Advance_Initial_Vertex;
}
//afou den ksana theoreitai einai asfales na svisoume
for( $m=1; $m<=N; $m++){//H[P[k], m] <- O, m = 1, 2, . . . , N
    if( $H[$P[$k-1]][$m]===0 ){
        $H[$P[$k-1]][$m]=$P[$k];
        break(1);
    }
}
for( $m=1; $m<=N; $m++ ){//H[P[k], m] <- O, m = 1, 2, . . . , N
    $H[$P[$k]][$m]=0;
}
$P[$k]=0;
$k--;
goto EC2_Path_Extension;

#[EC5 Advance Initial Vertex]
EC5_Advance_Initial_Vertex:
if($P[1] === N){
    goto EC6_Terminate;
}
$P[1]++;
$k=1;
$H=array(
        1=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        2=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        3=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        4=>array(1=>0,2=>0,3=>0,4=>0,5=>0),
        5=>array(1=>0,2=>0,3=>0,4=>0,5=>0)
);
goto EC2_Path_Extension;

#[EC5 Advance Initial Vertex]
EC6_Terminate:
print_r($Circ);
?>

Sau đó, đây là cách triển khai khác, độc lập hơn với biểu đồ, không có goto và không có giá trị mảng, thay vào đó, nó sử dụng các khóa mảng, đường dẫn, biểu đồ và mạch được lưu trữ dưới dạng các khóa mảng (sử dụng các giá trị mảng nếu bạn muốn, chỉ cần thay đổi yêu cầu dòng). Biểu đồ ví dụ bắt đầu từ -4 để thể hiện tính độc lập của nó.

<?php

$G = array(
        -4=>array(-4=>true,-3=>true,-2=>true),
        -3=>array(-4=>true,-3=>true,-2=>true),
        -2=>array(-4=>true,-3=>true,-2=>true)
);


$C = array();


EC($G,$C);
echo "<pre>";
print_r($C);
function EC($G, &$C){

    $CNST_not_closed =  false;                          // this flag indicates no closure
    $CNST_closed        = true;                         // this flag indicates closure
    // define the state where there is no closures for some node
    $tmp_first_node  =  key($G);                        // first node = first key
    $tmp_last_node  =   $tmp_first_node-1+count($G);    // last node  = last  key
    $CNST_closure_reset = array();
    for($k=$tmp_first_node; $k<=$tmp_last_node; $k++){
        $CNST_closure_reset[$k] = $CNST_not_closed;
    }
    // define the state where there is no closure for all nodes
    for($k=$tmp_first_node; $k<=$tmp_last_node; $k++){
        $H[$k] = $CNST_closure_reset;   // Key in the closure arrays represent nodes
    }
    unset($tmp_first_node);
    unset($tmp_last_node);


    # Start algorithm
    foreach($G as $init_node => $children){#[Jump to initial node set]
        #[Initial Node Set]
        $P = array();                   // declare at starup, remove the old $init_node from path on loop
        $P[$init_node]=true;            // the first key in P is always the new initial node
        $k=$init_node;                  // update the current node
                                        // On loop H[old_init_node] is not cleared cause is never checked again
        do{#Path 1,3,7,4 jump here to extend father 7
            do{#Path from 1,3,8,5 became 2,4,8,5,6 jump here to extend child 6
                $new_expansion = false;
                foreach( $G[$k] as $child => $foo ){#Consider each child of 7 or 6
                    if( $child>$init_node and isset($P[$child])===false and $H[$k][$child]===$CNST_not_closed ){
                        $P[$child]=true;    // add this child to the path
                        $k = $child;        // update the current node
                        $new_expansion=true;// set the flag for expanding the child of k
                        break(1);           // we are done, one child at a time
            }   }   }while(($new_expansion===true));// Do while a new child has been added to the path

            # If the first node is child of the last we have a circuit
            if( isset($G[$k][$init_node])===true ){
                $C[] = $P;  // Leaving this out of closure will catch loops to
            }

            # Closure
            if($k>$init_node){                  //if k>init_node then alwaya count(P)>1, so proceed to closure
                $new_expansion=true;            // $new_expansion is never true, set true to expand father of k
                unset($P[$k]);                  // remove k from path
                end($P); $k_father = key($P);   // get father of k
                $H[$k_father][$k]=$CNST_closed; // mark k as closed
                $H[$k] = $CNST_closure_reset;   // reset k closure
                $k = $k_father;                 // update k
        }   } while($new_expansion===true);//if we don't wnter the if block m has the old k$k_father_old = $k;
        // Advance Initial Vertex Context
    }//foreach initial


}//function

?>

Tôi đã phân tích và ghi lại EC nhưng không may là tài liệu này bằng tiếng Hy Lạp.


1

Có hai bước (thuật toán) liên quan đến việc tìm kiếm tất cả các chu kỳ trong DAG.

Bước đầu tiên là sử dụng thuật toán của Tarjan để tìm tập hợp các thành phần được kết nối mạnh.

  1. Bắt đầu từ bất kỳ đỉnh tùy ý.
  2. DFS từ đỉnh đó. Đối với mỗi nút x, giữ hai số, dfs_index [x] và dfs_lowval [x]. dfs_index [x] lưu trữ khi nút đó được truy cập, trong khi dfs_lowval [x] = min (dfs_low [k]) trong đó k là tất cả các con của x không phải là cha mẹ trực tiếp của x trong cây bao trùm dfs.
  3. Tất cả các nút có cùng dfs_lowval [x] đều nằm trong cùng một thành phần được kết nối mạnh.

Bước thứ hai là tìm chu kỳ (đường dẫn) trong các thành phần được kết nối. Đề nghị của tôi là sử dụng một phiên bản sửa đổi của thuật toán Hierholzer.

Ý tưởng là:

  1. Chọn bất kỳ đỉnh bắt đầu v và đi theo vệt cạnh từ đỉnh đó cho đến khi bạn quay lại v. Không thể bị kẹt ở bất kỳ đỉnh nào ngoài v, bởi vì mức độ chẵn của tất cả các đỉnh đảm bảo rằng, khi đường mòn đi vào một đỉnh khác đỉnh w phải có cạnh không sử dụng để lại w. Chuyến tham quan được hình thành theo cách này là một chuyến tham quan kín, nhưng có thể không bao gồm tất cả các đỉnh và cạnh của biểu đồ ban đầu.
  2. Chừng nào còn tồn tại một đỉnh v thuộc về tour hiện tại nhưng có các cạnh liền kề không phải là một phần của chuyến tham quan, hãy bắt đầu một đường khác từ v, theo các cạnh không được sử dụng cho đến khi bạn quay lại v và tham gia chuyến tham quan được hình thành theo cách này đến tour trước.

Đây là liên kết đến một triển khai Java với một trường hợp thử nghiệm:

http://stones333.blogspot.com/2013/12/find-airs-in-directed-graph-dag.html


16
Làm thế nào một chu kỳ có thể tồn tại trong một DAG (Đồ thị theo chu kỳ có hướng)?
sky_coder123

Điều này không tìm thấy tất cả các chu kỳ.
Vishwa Ratna


0

Tôi tình cờ tìm thấy thuật toán sau có vẻ hiệu quả hơn thuật toán của Johnson (ít nhất là đối với các biểu đồ lớn hơn). Tuy nhiên tôi không chắc về hiệu suất của nó so với thuật toán của Tarjan.
Ngoài ra, tôi chỉ kiểm tra nó cho hình tam giác cho đến nay. Nếu quan tâm, vui lòng xem "Thuật toán liệt kê và biểu đồ con" của Norishige Chiba và Takao Nishizeki ( http://dx.doi.org/10.1137/0214017 )


0

Giải pháp Javascript sử dụng các danh sách liên kết rời rạc. Có thể được nâng cấp để tách rời các khu rừng để có thời gian chạy nhanh hơn.

var input = '5\nYYNNN\nYYYNN\nNYYNN\nNNNYN\nNNNNY'
console.log(input);
//above solution should be 3 because the components are
//{0,1,2}, because {0,1} and {1,2} therefore {0,1,2}
//{3}
//{4}

//MIT license, authored by Ling Qing Meng

//'4\nYYNN\nYYYN\nNYYN\nNNNY'

//Read Input, preformatting
var reformat = input.split(/\n/);
var N = reformat[0];
var adjMatrix = [];
for (var i = 1; i < reformat.length; i++) {
    adjMatrix.push(reformat[i]);
}

//for (each person x from 1 to N) CREATE-SET(x)
var sets = [];
for (var i = 0; i < N; i++) {
    var s = new LinkedList();
    s.add(i);
    sets.push(s);
}

//populate friend potentials using combinatorics, then filters
var people =  [];
var friends = [];
for (var i = 0; i < N; i++) {
    people.push(i);
}
var potentialFriends = k_combinations(people,2);
for (var i = 0; i < potentialFriends.length; i++){
    if (isFriend(adjMatrix,potentialFriends[i]) === 'Y'){
        friends.push(potentialFriends[i]);
    }
}


//for (each pair of friends (x y) ) if (FIND-SET(x) != FIND-SET(y)) MERGE-SETS(x, y)
for (var i = 0; i < friends.length; i++) {
    var x = friends[i][0];
    var y = friends[i][1];
    if (FindSet(x) != FindSet(y)) {
        sets.push(MergeSet(x,y));
    }
}


for (var i = 0; i < sets.length; i++) {
    //sets[i].traverse();
}
console.log('How many distinct connected components?',sets.length);



//Linked List data structures neccesary for above to work
function Node(){
    this.data = null;
    this.next = null;
}

function LinkedList(){
    this.head = null;
    this.tail = null;
    this.size = 0;

    // Add node to the end
    this.add = function(data){
        var node = new Node();
        node.data = data;
        if (this.head == null){
            this.head = node;
            this.tail = node;
        } else {
            this.tail.next = node;
            this.tail = node;
        }
        this.size++;
    };


    this.contains = function(data) {
        if (this.head.data === data) 
            return this;
        var next = this.head.next;
        while (next !== null) {
            if (next.data === data) {
                return this;
            }
            next = next.next;
        }
        return null;
    };

    this.traverse = function() {
        var current = this.head;
        var toPrint = '';
        while (current !== null) {
            //callback.call(this, current); put callback as an argument to top function
            toPrint += current.data.toString() + ' ';
            current = current.next; 
        }
        console.log('list data: ',toPrint);
    }

    this.merge = function(list) {
        var current = this.head;
        var next = current.next;
        while (next !== null) {
            current = next;
            next = next.next;
        }
        current.next = list.head;
        this.size += list.size;
        return this;
    };

    this.reverse = function() {
      if (this.head == null) 
        return;
      if (this.head.next == null) 
        return;

      var currentNode = this.head;
      var nextNode = this.head.next;
      var prevNode = this.head;
      this.head.next = null;
      while (nextNode != null) {
        currentNode = nextNode;
        nextNode = currentNode.next;
        currentNode.next = prevNode;
        prevNode = currentNode;
      }
      this.head = currentNode;
      return this;
    }


}


/**
 * GENERAL HELPER FUNCTIONS
 */

function FindSet(x) {
    for (var i = 0; i < sets.length; i++){
        if (sets[i].contains(x) != null) {
            return sets[i].contains(x);
        }
    }
    return null;
}

function MergeSet(x,y) {
    var listA,listB;
    for (var i = 0; i < sets.length; i++){
        if (sets[i].contains(x) != null) {
            listA = sets[i].contains(x);
            sets.splice(i,1);
        }
    }
    for (var i = 0; i < sets.length; i++) {
        if (sets[i].contains(y) != null) {
            listB = sets[i].contains(y);
            sets.splice(i,1);
        }
    }
    var res = MergeLists(listA,listB);
    return res;

}


function MergeLists(listA, listB) {
    var listC = new LinkedList();
    listA.merge(listB);
    listC = listA;
    return listC;
}

//access matrix by i,j -> returns 'Y' or 'N'
function isFriend(matrix, pair){
    return matrix[pair[0]].charAt(pair[1]);
}

function k_combinations(set, k) {
    var i, j, combs, head, tailcombs;
    if (k > set.length || k <= 0) {
        return [];
    }
    if (k == set.length) {
        return [set];
    }
    if (k == 1) {
        combs = [];
        for (i = 0; i < set.length; i++) {
            combs.push([set[i]]);
        }
        return combs;
    }
    // Assert {1 < k < set.length}
    combs = [];
    for (i = 0; i < set.length - k + 1; i++) {
        head = set.slice(i, i+1);
        tailcombs = k_combinations(set.slice(i + 1), k - 1);
        for (j = 0; j < tailcombs.length; j++) {
            combs.push(head.concat(tailcombs[j]));
        }
    }
    return combs;
}

0

DFS từ nút bắt đầu s, theo dõi đường dẫn DFS trong quá trình truyền tải và ghi lại đường dẫn nếu bạn tìm thấy một cạnh từ nút v trong đường dẫn đến s. (v, s) là một mặt sau trong cây DFS và do đó chỉ ra một chu kỳ có chứa s.


Tốt, nhưng đây không phải là điều OP đang tìm kiếm: tìm tất cả chu kỳ, có thể là tối thiểu.
Sean L

0

Liên quan đến câu hỏi của bạn về Chu kỳ hoán vị , đọc thêm tại đây: https://www.codechef.com/probols/PCYCLE

Bạn có thể thử mã này (nhập kích thước và số chữ số):

# include<cstdio>
using namespace std;

int main()
{
    int n;
    scanf("%d",&n);

    int num[1000];
    int visited[1000]={0};
    int vindex[2000];
    for(int i=1;i<=n;i++)
        scanf("%d",&num[i]);

    int t_visited=0;
    int cycles=0;
    int start=0, index;

    while(t_visited < n)
    {
        for(int i=1;i<=n;i++)
        {
            if(visited[i]==0)
            {
                vindex[start]=i;
                visited[i]=1;
                t_visited++;
                index=start;
                break;
            }
        }
        while(true)
        {
            index++;
            vindex[index]=num[vindex[index-1]];

            if(vindex[index]==vindex[start])
                break;
            visited[vindex[index]]=1;
            t_visited++;
        }
        vindex[++index]=0;
        start=index+1;
        cycles++;
    }

    printf("%d\n",cycles,vindex[0]);

    for(int i=0;i<(n+2*cycles);i++)
    {
        if(vindex[i]==0)
            printf("\n");
        else
            printf("%d ",vindex[i]);
    }
}

0

Phiên bản DFS c ++ cho mã giả trong câu trả lời của tầng hai:

void findCircleUnit(int start, int v, bool* visited, vector<int>& path) {
    if(visited[v]) {
        if(v == start) {
            for(auto c : path)
                cout << c << " ";
            cout << endl;
            return;
        }
        else 
            return;
    }
    visited[v] = true;
    path.push_back(v);
    for(auto i : G[v])
        findCircleUnit(start, i, visited, path);
    visited[v] = false;
    path.pop_back();
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.