Thuật toán tốt nhất để phát hiện các chu kỳ trong đồ thị có hướng


396

Thuật toán hiệu quả nhất để phát hiện tất cả các chu kỳ trong đồ thị có hướng là gì?

Tôi có một biểu đồ được định hướng đại diện cho một lịch trình các công việc cần được thực hiện, một công việc là một nút và một phụ thuộc là một cạnh. Tôi cần phát hiện trường hợp lỗi của một chu kỳ trong biểu đồ này dẫn đến sự phụ thuộc theo chu kỳ.


13
Bạn nói rằng bạn muốn phát hiện tất cả các chu kỳ, nhưng trường hợp sử dụng của bạn cho thấy rằng sẽ đủ để phát hiện xem có bất kỳ chu kỳ nào không.
Steve Jessop

29
Sẽ tốt hơn nếu phát hiện tất cả các chu kỳ để chúng có thể được sửa trong một lần, thay vì kiểm tra, sửa chữa, kiểm tra, sửa chữa, v.v.
Peauters

2
Bạn nên đọc bài báo "Tìm tất cả các mạch cơ bản của đồ thị có hướng" của Donald B. Johnson. Nó sẽ chỉ tìm thấy các mạch cơ bản, nhưng điều này là đủ cho trường hợp của bạn. Và đây là triển khai Java của tôi về thuật toán này đã sẵn sàng để sử dụng: github.com/1123/johnson
user52468 14/2/2015

Chạy DFS với sửa đổi bổ sung cho thuật toán: đánh dấu từng nút bạn đã truy cập. nếu bạn truy cập một nút đã được truy cập, thì bạn có một cicle. khi bạn rút lui khỏi một đường dẫn, bỏ đánh dấu các nút được truy cập.
Hesham Yassin

2
@HeshamYassin, nếu bạn truy cập một nút mà bạn đã truy cập, điều đó không nhất thiết có nghĩa là có một vòng lặp. Vui lòng đọc nhận xét của tôi cs.stackexchange.com/questions/9676/ .
Maksim Dmitriev

Câu trả lời:


193

Thuật toán các thành phần được kết nối mạnh mẽ của TarjanO(|E| + |V|)độ phức tạp về thời gian.

Đối với các thuật toán khác, xem các thành phần được kết nối mạnh mẽ trên Wikipedia.


69
Làm thế nào để tìm các thành phần được kết nối mạnh cho bạn biết về các chu kỳ tồn tại trong biểu đồ?
Peter

4
Có thể ai đó có thể xác nhận nhưng thuật toán Tarjan không hỗ trợ chu kỳ của các nút trỏ trực tiếp vào chính họ, như A-> A.
Cédric Guillemette

24
@Cedrik Phải, không trực tiếp. Đây không phải là một lỗ hổng trong thuật toán của Tarjan, nhưng cách nó được sử dụng cho câu hỏi này. Tarjan không trực tiếp tìm chu kỳ , nó tìm thấy các thành phần được kết nối mạnh mẽ. Tất nhiên, bất kỳ SCC nào có kích thước lớn hơn 1 đều ngụ ý một chu kỳ. Các thành phần không theo chu kỳ có một SCC đơn lẻ. Vấn đề là một vòng lặp tự cũng sẽ tự đi vào SCC. Vì vậy, bạn cần một kiểm tra riêng cho các vòng lặp tự, đó là khá nhỏ.
mgiuca

13
(tất cả các thành phần được kết nối mạnh trong biểu đồ)! = (tất cả các chu kỳ trong biểu đồ)
Optimusfrenk 16/2/2015

4
@ aku: Một DFS ba màu cũng có thời gian chạy giống nhau O(|E| + |V|). Sử dụng màu trắng (không bao giờ truy cập), màu xám (nút hiện tại được truy cập nhưng tất cả các nút có thể truy cập chưa được truy cập) và màu đen (tất cả các nút có thể truy cập được truy cập cùng với mã màu hiện tại), nếu một nút màu xám tìm thấy nút màu xám khác thì chúng ta ' có một chu kỳ. [Khá nhiều những gì chúng ta có trong cuốn sách thuật toán của Cormen]. Tự hỏi liệu 'thuật toán của Tarjan' có lợi ích gì đối với DFS đó không !!
KGhatak

73

Cho rằng đây là một lịch trình công việc, tôi nghi ngờ rằng đến một lúc nào đó bạn sẽ sắp xếp chúng vào một trật tự thực hiện được đề xuất.

Nếu đó là trường hợp, thì việc thực hiện sắp xếp theo cấu trúc liên kết trong mọi trường hợp có thể phát hiện chu kỳ. UNIX tsortchắc chắn làm được. Tôi nghĩ rằng có khả năng là hiệu quả hơn để phát hiện các chu kỳ cùng lúc với tsorting, hơn là trong một bước riêng biệt.

Vì vậy, câu hỏi có thể trở thành, "làm thế nào để tôi tsort hiệu quả nhất", thay vì "làm thế nào để tôi phát hiện các vòng lặp hiệu quả nhất". Câu trả lời có lẽ là "sử dụng thư viện", nhưng không thành công trong bài viết Wikipedia sau:

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

có mã giả cho một thuật toán và mô tả ngắn gọn về một thuật toán khác từ Tarjan. Cả hai đều có O(|V| + |E|)thời gian phức tạp.


Một loại cấu trúc liên kết có thể phát hiện các chu kỳ, vì nó dựa trên thuật toán tìm kiếm theo chiều sâu, nhưng bạn cần có sổ sách bổ sung để thực sự phát hiện các chu kỳ. Xem câu trả lời đúng của Kurt Peek.
Luke Hutchison

33

Cách đơn giản nhất để làm điều đó là thực hiện một chiều sâu trước tiên (DFT) của biểu đồ .

Nếu đồ thị có ncác đỉnh, đây là O(n)thuật toán phức tạp thời gian. Vì bạn có thể sẽ phải thực hiện một DFT bắt đầu từ mỗi đỉnh, nên tổng độ phức tạp trở thành O(n^2).

Bạn phải duy trì một ngăn xếp chứa tất cả các đỉnh trong chiều ngang hiện tại đầu tiên , với phần tử đầu tiên của nó là nút gốc. Nếu bạn gặp một phần tử đã có trong ngăn xếp trong DFT, thì bạn có một chu kỳ.


21
Điều này sẽ đúng với biểu đồ "thông thường", nhưng sai với biểu đồ có hướng . Ví dụ, hãy xem xét "sơ đồ phụ thuộc kim cương" với bốn nút: A có các cạnh chỉ vào B và C, mỗi nút có cạnh chỉ vào D. Chuyển động DFT của sơ đồ này từ A sẽ kết luận không chính xác rằng "vòng lặp" là thực sự là một chu trình - mặc dù có một vòng lặp, nó không phải là một chu trình bởi vì nó không thể đi qua được bằng cách đi theo các mũi tên.
Peter

9
@peter bạn có thể vui lòng giải thích làm thế nào DFT từ A sẽ kết luận không chính xác rằng có một chu kỳ?
Deepak

10
@Deepak - Trên thực tế, tôi đã đọc sai câu trả lời từ "thuật sĩ vật lý": nơi anh ấy viết "trong ngăn xếp" Tôi nghĩ rằng "đã được tìm thấy". Nó thực sự là đủ (để phát hiện một vòng lặp có hướng) để kiểm tra các bản sao "trong ngăn xếp" trong quá trình thực hiện DFT. Một upvote cho mỗi bạn.
Peter

2
Tại sao bạn nói độ phức tạp thời gian là O(n)trong khi bạn đề nghị kiểm tra ngăn xếp để xem nó có chứa nút truy cập không? Quét ngăn xếp thêm thời gian vào O(n)thời gian chạy vì nó phải quét ngăn xếp trên mỗi nút mới. Bạn có thể đạt đượcO(n) nếu bạn đánh dấu các nút được truy cập
James Wierzba

Như Peter đã nói, điều này là không đầy đủ cho các đồ thị có hướng. Xem câu trả lời đúng của Kurt Peek.
Luke Hutchison

32

Theo Bổ đề 22.11 của Cormen và cộng sự, Giới thiệu về Thuật toán (CLRS):

Đồ thị có hướng G là theo chu kỳ khi và chỉ khi tìm kiếm theo chiều sâu của G không có cạnh sau.

Điều này đã được đề cập trong một số câu trả lời; ở đây tôi cũng sẽ cung cấp một ví dụ mã dựa trên chương 22 của CLRS. Biểu đồ ví dụ được minh họa dưới đây.

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

Mã giả của CLRS cho tìm kiếm theo chiều sâu đọc:

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

Trong ví dụ trong CLRS Hình 22.4, biểu đồ bao gồm hai cây DFS: một bao gồm các nút u , v , xy và một nút khác của các nút w z . Mỗi cây chứa một cạnh sau: một từ x đến v và một cây khác từ z đến z (một vòng lặp tự).

Nhận thức chính là một cạnh sau gặp phải khi, trong DFS-VISIThàm, trong khi lặp qua các hàng xóm vcủa u, một nút được bắt gặp với GRAYmàu.

Mã Python sau đây là sự điều chỉnh mã giả của CLRS với một ifmệnh đề được thêm vào để phát hiện các chu kỳ:

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)

Lưu ý rằng trong ví dụ này, timemã giả trong CLRS không bị bắt vì chúng tôi chỉ quan tâm đến việc phát hiện các chu kỳ. Ngoài ra còn có một số mã soạn sẵn để xây dựng biểu diễn danh sách kề của biểu đồ từ danh sách các cạnh.

Khi đoạn script này được thực thi, nó sẽ in ra kết quả sau:

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

Đây chính xác là các cạnh sau trong ví dụ trong CLRS Hình 22.4.


29

Bắt đầu với DFS: một chu kỳ tồn tại khi và chỉ khi một cạnh sau được phát hiện trong DFS . Điều này được chứng minh là kết quả của lý thuyết đường trắng.


3
Vâng, tôi cũng nghĩ như vậy, nhưng điều này là không đủ, tôi đăng bài của mình cs.stackexchange.com/questions/7216/find-the-simple- Motorcycle
in

Thật. Ajay Garg chỉ nói về cách tìm "một chu kỳ", đó là một câu trả lời cho câu hỏi này. Liên kết của bạn nói về việc tìm tất cả các chu kỳ theo câu hỏi được hỏi, nhưng một lần nữa có vẻ như nó sử dụng cách tiếp cận tương tự như Ajay Garg, nhưng cũng có thể thực hiện tất cả các cây dfs.
Manohar Reddy Poreddy

Điều này là không đầy đủ cho đồ thị có hướng. Xem câu trả lời đúng của Kurt Peek.
Luke Hutchison

26

Theo tôi, thuật toán dễ hiểu nhất để phát hiện chu kỳ trong đồ thị có hướng là thuật toán tô màu đồ thị.

Về cơ bản, thuật toán tô màu biểu đồ đi theo biểu đồ theo cách DFS (Depth First Search, có nghĩa là nó khám phá một đường dẫn hoàn toàn trước khi khám phá một đường dẫn khác). Khi tìm thấy cạnh sau, nó đánh dấu biểu đồ là chứa một vòng lặp.

Để được giải thích sâu hơn về thuật toán tô màu đồ thị, vui lòng đọc bài viết này: http : //www.geekforgeek.org/detect- Motorcycle-direct-graph-USE-colors/

Ngoài ra, tôi cung cấp triển khai tô màu đồ thị trong JavaScript https://github.com/dexcodeinc/graph_alerskym.js/blob/master/graph_alerskym.js


8

Nếu bạn không thể thêm thuộc tính "đã truy cập" vào các nút, hãy sử dụng một tập hợp (hoặc bản đồ) và chỉ cần thêm tất cả các nút đã truy cập vào tập hợp trừ khi chúng đã có trong tập hợp. Sử dụng một khóa duy nhất hoặc địa chỉ của các đối tượng làm "khóa".

Điều này cũng cung cấp cho bạn thông tin về nút "gốc" của phụ thuộc theo chu kỳ sẽ có ích khi người dùng phải khắc phục sự cố.

Một giải pháp khác là cố gắng tìm sự phụ thuộc tiếp theo để thực thi. Đối với điều này, bạn phải có một số ngăn xếp nơi bạn có thể nhớ bạn đang ở đâu và bạn cần làm gì tiếp theo. Kiểm tra xem một phụ thuộc đã có trên ngăn xếp này trước khi bạn thực hiện nó. Nếu có, bạn đã tìm thấy một chu kỳ.

Mặc dù điều này dường như có độ phức tạp của O (N * M), bạn phải nhớ rằng ngăn xếp có độ sâu rất hạn chế (vì vậy N nhỏ) và M trở nên nhỏ hơn với mỗi phụ thuộc mà bạn có thể kiểm tra là "thực thi" cộng bạn có thể dừng tìm kiếm khi bạn tìm thấy một chiếc lá (vì vậy bạn không bao giờ phải kiểm tra mọi nút -> M cũng sẽ nhỏ).

Trong MetaMake, tôi đã tạo biểu đồ dưới dạng danh sách các danh sách và sau đó xóa mọi nút khi tôi thực hiện chúng để cắt giảm khối lượng tìm kiếm một cách tự nhiên. Tôi chưa bao giờ thực sự phải chạy một kiểm tra độc lập, tất cả diễn ra tự động trong quá trình thực thi bình thường.

Nếu bạn cần chế độ "chỉ kiểm tra", chỉ cần thêm cờ "chạy khô" sẽ vô hiệu hóa việc thực hiện các công việc thực tế.


7

Không có thuật toán nào có thể tìm thấy tất cả các chu kỳ trong đồ thị có hướng trong thời gian đa thức. Giả sử, đồ thị có hướng có n nút và mỗi cặp nút có kết nối với nhau có nghĩa là bạn có một đồ thị hoàn chỉnh. Vì vậy, bất kỳ tập hợp con không trống nào của n nút này chỉ ra một chu kỳ và có 2 ^ n-1 số tập con như vậy. Vì vậy, không có thuật toán thời gian đa thức tồn tại. Vì vậy, giả sử bạn có một thuật toán hiệu quả (không ngu ngốc) có thể cho bạn biết số chu kỳ được định hướng trong biểu đồ, trước tiên bạn có thể tìm thấy các thành phần được kết nối mạnh, sau đó áp dụng thuật toán của bạn trên các thành phần được kết nối này. Vì chu kỳ chỉ tồn tại trong các thành phần chứ không phải giữa chúng.


1
Đúng, nếu số lượng nút được lấy là kích thước của đầu vào. Bạn cũng có thể mô tả độ phức tạp của thời gian chạy theo số lượng cạnh hoặc thậm chí chu kỳ hoặc kết hợp các biện pháp này. Thuật toán "Tìm tất cả các mạch cơ bản của đồ thị có hướng" của Donald B. Johnson có thời gian chạy đa thức được cho bởi O ((n + e) ​​(c + 1)) trong đó n là số nút, e là số cạnh và c số lượng mạch cơ bản của đồ thị. Và đây là triển khai Java của tôi về thuật toán này: github.com/1123/johnson .
152468

4

Tôi đã thực hiện vấn đề này trong sml (lập trình bắt buộc). Đây là phác thảo. Tìm tất cả các nút có mức độ không chính xác hoặc bằng 0. Các nút như vậy không thể là một phần của một chu kỳ (vì vậy hãy loại bỏ chúng). Tiếp theo loại bỏ tất cả các cạnh đến hoặc đi từ các nút như vậy. Áp dụng đệ quy quy trình này vào biểu đồ kết quả. Nếu ở cuối bạn không bị bỏ lại với bất kỳ nút hoặc cạnh nào, đồ thị không có bất kỳ chu kỳ nào, thì nó có.


2

Cách tôi làm là thực hiện Sắp xếp tô pô, đếm số đỉnh được truy cập. Nếu số đó nhỏ hơn tổng số đỉnh trong DAG, bạn có một chu kỳ.


4
Điều đó không có ý nghĩa. Nếu biểu đồ có chu kỳ, không có sắp xếp tôpô, có nghĩa là bất kỳ thuật toán chính xác nào để sắp xếp tô pô sẽ hủy bỏ.
sleske

4
từ wikipedia: Nhiều thuật toán sắp xếp tô pô cũng sẽ phát hiện các chu kỳ, vì đó là những trở ngại cho trật tự tôpô tồn tại.
Oleg Mikheev

1
@OlegMikheev Có, nhưng Steve đang nói "Nếu con số đó nhỏ hơn tổng số đỉnh trong DAG, bạn có một chu kỳ", điều đó không có nghĩa.
nbro

@nbro Tôi cá là, họ có nghĩa là một biến thể của thuật toán sắp xếp tôpô sẽ hủy bỏ khi không tồn tại phân loại tôpô (và sau đó họ không truy cập tất cả các đỉnh).
maaartinus

Nếu bạn thực hiện sắp xếp tô pô trên biểu đồ theo chu kỳ, bạn sẽ kết thúc với một đơn hàng có số cạnh xấu ít nhất (số thứ tự> số thứ tự của hàng xóm). Nhưng sau khi bạn phải sắp xếp dễ dàng phát hiện các cạnh xấu đó dẫn đến việc phát hiện biểu đồ có chu kỳ
UGP

2

/mathpro/16393/finding-a- Motorcycle-of -fixed-length Motorcycle-of -fixed-length Tôi thích giải pháp này đặc biệt tốt nhất cho 4 chiều dài :)

Ngoài ra thuật sĩ vật lý nói rằng bạn phải làm O (V ^ 2). Tôi tin rằng chúng ta chỉ cần O (V) / O (V + E). Nếu đồ thị được kết nối thì DFS sẽ truy cập tất cả các nút. Nếu đồ thị đã kết nối các đồ thị phụ thì mỗi lần chúng ta chạy DFS trên một đỉnh của đồ thị phụ này, chúng ta sẽ tìm thấy các đỉnh được kết nối và sẽ không phải xem xét các đồ thị này cho lần chạy tiếp theo của DFS. Do đó, khả năng chạy cho mỗi đỉnh là không chính xác.


1

Nếu DFS tìm thấy một cạnh chỉ đến một đỉnh đã được truy cập, bạn có một chu kỳ ở đó.


1
Thất bại trên 1,2,3: 1,2; 1,3; 2,3;
con mèo ồn ào

4
@JakeGreene Xem tại đây: i.imgur.com/tEkM5xy.png Đủ đơn giản để hiểu. Hãy nói rằng bạn bắt đầu từ 0. Sau đó, bạn đi đến nút 1, không còn đường dẫn nào từ đó, reucrsion quay trở lại. Bây giờ bạn truy cập nút 2, có cạnh với đỉnh 1, đã được truy cập. Theo ý kiến ​​của bạn, bạn sẽ có một chu kỳ sau đó - và bạn không có một
con mèo

3
@kittyPL Biểu đồ đó không chứa chu trình. Từ Wikipedia: "Một chu kỳ có hướng trong đồ thị có hướng là một chuỗi các đỉnh bắt đầu và kết thúc tại cùng một đỉnh sao cho, mỗi hai đỉnh liên tiếp của chu kỳ, tồn tại một cạnh được định hướng từ đỉnh trước đến đỉnh sau" Bạn phải có khả năng đi theo một con đường từ V dẫn trở lại V cho một chu kỳ được định hướng. giải pháp của mafonya hoạt động cho vấn đề đã cho
Jake Greene

2
@JakeGreene Tất nhiên là không. Sử dụng thuật toán của bạn và bắt đầu từ 1, bạn sẽ phát hiện ra một chu kỳ ... Thuật toán này thật tệ ... Thông thường sẽ đủ để đi lùi bất cứ khi nào bạn gặp một đỉnh được truy cập.
con mèo ồn ào

6
@kittyPL DFS không hoạt động để phát hiện các chu kỳ từ nút bắt đầu đã cho. Nhưng khi thực hiện DFS, bạn phải tô màu các nút được truy cập để phân biệt cạnh chéo với cạnh sau. Lần đầu tiên truy cập vào một đỉnh nó chuyển sang màu xám, sau đó bạn chuyển sang màu đen một khi tất cả các cạnh của nó đã được truy cập. Nếu khi thực hiện DFS bạn chạm một đỉnh màu xám thì đỉnh đó là tổ tiên (tức là: bạn có một chu kỳ). Nếu đỉnh có màu đen thì đó chỉ là một cạnh chéo.
Tiếng Anh

0

Như bạn đã nói, bạn đã thiết lập các công việc, nó cần được thực hiện theo thứ tự nhất định. Topological sortđưa ra yêu cầu của bạn để sắp xếp công việc (hoặc cho các vấn đề phụ thuộc nếu đó là một direct acyclic graph). Chạy dfsvà duy trì một danh sách, và bắt đầu thêm nút vào đầu danh sách, và nếu bạn gặp phải một nút đã được truy cập. Sau đó, bạn tìm thấy một chu kỳ trong đồ thị đã cho.


-11

Nếu một đồ thị thỏa mãn tính chất này

|e| > |v| - 1

sau đó đồ thị chứa ít nhất trên chu kỳ.


10
Điều đó có thể đúng với các đồ thị vô hướng, nhưng chắc chắn không đúng với các đồ thị có hướng.
Hans-Peter Störr

6
Một ví dụ ngược lại sẽ là A-> B, B-> C, A-> C.
152468

Không phải tất cả các đỉnh đều có cạnh.
Debanjan Dhar
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.