Najlepszy algorytm do wykrywania cykli na ukierunkowanym wykresie

395

Jaki jest najskuteczniejszy algorytm wykrywania wszystkich cykli w obrębie ukierunkowanego wykresu?

Mam ukierunkowany wykres przedstawiający harmonogram zadań, które należy wykonać, zadanie jest węzłem, a zależność jest krawędzią. Muszę wykryć przypadek błędu cyklu na tym wykresie, co prowadzi do cyklicznych zależności.

Peauters
źródło
13
Mówisz, że chcesz wykryć wszystkie cykle, ale twój przypadek użycia sugeruje, że wystarczyłoby wykryć, czy są jakieś cykle.
Steve Jessop
29
Lepiej byłoby wykryć wszystkie cykle, aby można je było naprawić za jednym razem, niż sprawdzać, naprawiać, sprawdzać, naprawiać itp.
Peauters
2
Powinieneś przeczytać artykuł „Znajdowanie wszystkich obwodów elementarnych ukierunkowanego wykresu” Donalda B. Johnsona. Znajdzie tylko obwody elementarne, ale powinno to wystarczyć dla twojego przypadku. A oto moja implementacja Java tego algorytmu gotowa do użycia: github.com/1123/johnson
user152468
Uruchom DFS z dodatkową modyfikacją algorytmu: zaznacz każdy odwiedzony węzeł. jeśli odwiedzisz węzeł, który już był odwiedzony, masz cicle. kiedy wycofujesz się ze ścieżki, odznacz odwiedzone węzły.
Hesham Yassin
2
@HeshamYassin, jeśli odwiedzasz węzeł, który już odwiedziłeś, niekoniecznie oznacza to, że istnieje pętla. Proszę przeczytać mój komentarz cs.stackexchange.com/questions/9676/… .
Maksim Dmitriev

Odpowiedzi:

193

Silnie powiązany algorytm Tarjana ma O(|E| + |V|)złożoność czasową.

Aby zapoznać się z innymi algorytmami, zobacz Silnie połączone komponenty na Wikipedii.

aku
źródło
69
W jaki sposób znalezienie silnie połączonych komponentów mówi ci o cyklach istniejących na wykresie?
Peter
4
Być może ktoś może potwierdzić, ale algorytm Tarjan nie obsługuje cykli węzłów wskazujących bezpośrednio na siebie, takich jak A-> A.
Cédric Guillemette,
24
@Cedrik Racja, nie bezpośrednio. Nie jest to wada algorytmu Tarjana, ale sposób, w jaki jest on używany w tym pytaniu. Tarjan nie znajduje bezpośrednio cykli , znajduje silnie połączone komponenty. Oczywiście każdy SCC o rozmiarze większym niż 1 implikuje cykl. Składniki niecykliczne same mają singleton SCC. Problem polega na tym, że pętla własna również sama wejdzie w SCC. Potrzebujesz więc osobnego sprawdzania pętli własnych, co jest dość trywialne.
mgiuca
13
(wszystkie silnie połączone komponenty na wykresie)! = (wszystkie cykle na wykresie)
optimusfrenk
4
@ aku: Trójkolorowy DFS ma również ten sam czas działania O(|E| + |V|). Używając białego (nigdy nie odwiedzanego), szarego (odwiedzany jest aktualny węzeł, ale wszystkie osiągalne węzły nie są jeszcze odwiedzane) i czarnego (wszystkie osiągalne węzły są odwiedzane wraz z bieżącym), kodowanie kolorami, jeśli szary węzeł znajdzie inny szary węzeł, wówczas „ cykl. [W zasadzie to, co mamy w książce algorytmów Cormena]. Zastanawiasz się, czy „algorytm Tarjana” ma jakąkolwiek przewagę nad takim DFS !!
KGhatak
73

Biorąc pod uwagę, że jest to harmonogram zadań, podejrzewam, że w pewnym momencie zamierzasz je posortować według proponowanej kolejności wykonania.

W takim przypadku implementacja sortowania topologicznego może w każdym przypadku wykryć cykle. UNIX na tsortpewno tak. Myślę, że jest bardziej prawdopodobne, że bardziej efektywne jest wykrywanie cykli w tym samym czasie, co tsortowanie, niż w osobnym kroku.

Zatem pytanie może brzmieć „jak najskuteczniej tsortować”, a nie „jak najskuteczniej wykrywać pętle”. Na którą odpowiedź brzmi prawdopodobnie „użyj biblioteki”, ale w przypadku braku następującego artykułu z Wikipedii:

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

ma pseudo-kod dla jednego algorytmu i krótki opis innego z Tarjan. Oba mają O(|V| + |E|)złożoność czasową.

Steve Jessop
źródło
Sortowanie topologiczne może wykrywać cykle, ponieważ opiera się na algorytmie wyszukiwania w pierwszej kolejności, ale do wykrycia cykli potrzebujesz dodatkowej księgowości. Zobacz poprawną odpowiedź Kurta Peka.
Luke Hutchison
33

Najprostszym sposobem na to jest wykonanie pierwszego przejścia na głębokości (DFT) na wykresie .

Jeśli wykres ma nwierzchołki, jest to O(n)algorytm złożoności czasowej. Ponieważ prawdopodobnie będziesz musiał wykonać DFT, zaczynając od każdego wierzchołka, całkowita złożoność staje się O(n^2).

Musisz utrzymać stos zawierający wszystkie wierzchołki na bieżącej głębokości pierwszego przejścia , przy czym pierwszym elementem jest węzeł główny. Jeśli natrafisz na element, który jest już na stosie podczas DFT, to masz cykl.

nbro
źródło
21
Byłoby to prawdą dla „zwykłego” wykresu, ale jest fałszywe dla wykresu ukierunkowanego . Rozważmy na przykład „diamentowy diagram zależności” z czterema węzłami: A z krawędziami skierowanymi w stronę B i C, z których każdy ma krawędź skierowaną w stronę D. Twoje przejście DFT z tego diagramu z A nieprawidłowo wnioskuje, że „pętla” była w rzeczywistości cykl - chociaż istnieje pętla, nie jest to cykl, ponieważ nie można go przechodzić, postępując zgodnie ze strzałkami.
Peter
9
@peter czy możesz wyjaśnić, w jaki sposób DFT z A nieprawidłowo uzna, że ​​istnieje cykl?
Deepak
10
@Deepak - W rzeczywistości źle odczytałem odpowiedź z „phys wizard”: gdzie napisał „na stosie” myślałem, że „już znaleziono”. Rzeczywiście wystarczy (do wykrywania ukierunkowanej pętli) sprawdzić duplikaty „w stosie” podczas wykonywania DFT. Jeden głos za każdym z was.
Peter
2
Dlaczego mówisz, że złożoność czasu występuje, O(n)gdy sugerujesz sprawdzenie stosu, aby sprawdzić, czy zawiera on już odwiedzony węzeł? Skanowanie stosu wydłuża czas O(n)działania, ponieważ musi skanować stos w każdym nowym węźle. Możesz to osiągnąć O(n), zaznaczając odwiedzone węzły
James Wierzba
Jak powiedział Piotr, jest to niekompletne dla grafów ukierunkowanych. Zobacz poprawną odpowiedź Kurta Peka.
Luke Hutchison
32

Według Lemma 22.11 z Cormen i wsp., Wstęp do algorytmów (CLRS):

Kierunkowy wykres G jest acykliczny wtedy i tylko wtedy, gdy przeszukiwanie G w pierwszej głębokości nie daje żadnych tylnych krawędzi.

Zostało to wspomniane w kilku odpowiedziach; tutaj podam również przykład kodu oparty na rozdziale 22 CLRS. Przykładowy wykres pokazano poniżej.

wprowadź opis zdjęcia tutaj

Pseudo-kod CLRS dla pierwszego wyszukiwania w głąb brzmi:

wprowadź opis zdjęcia tutaj

W przykładzie na rysunku 22.4 CLRS wykres składa się z dwóch drzew DFS: jednego składającego się z węzłów u , v , x i y , a drugiego z węzłów w i z . Każde drzewo zawiera jedną tylną krawędź: jedną od x do v, a drugą od z do z (pętla własna).

Realizacja kluczem jest to, że krawędź tylna napotkaniu gdy w DFS-VISITfunkcji podczas iteracji nad sąsiadami vo uwęzeł spotyka się z GRAYkolorem.

Poniższy kod Python jest adaptacją pseudokodu CLRS z ifdodaną klauzulą ​​wykrywającą cykle:

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)

Zauważ, że w tym przykładzie timepseudokod CLRS nie jest przechwytywany, ponieważ jesteśmy zainteresowani jedynie wykrywaniem cykli. Istnieje również pewien kod do zbudowania reprezentacji wykresu sąsiedztwa na podstawie listy krawędzi.

Po uruchomieniu tego skryptu drukuje następujące dane wyjściowe:

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

Są to dokładnie tylne krawędzie w przykładzie na CLRS, rysunek 22.4.

Kurt Peek
źródło
4
Jest to jedyna pokrewna, akceptowalna i działająca odpowiedź tutaj, zasługuje na dużo więcej pochwały.
plasmacel
29

Rozpocznij z DFS: cykl istnieje tylko i tylko wtedy, gdy podczas DFS zostanie wykryty back-edge . Zostało to udowodnione w wyniku teorii białej ścieżki.

Ajay Garg
źródło
3
Tak, myślę tak samo, ale to nie wystarczy, publikuję po swojemu cs.stackexchange.com/questions/7216/find-the-simple-cycles-in-a-directed-graph
jonaprieto
Prawdziwe. Ajay Garg mówi tylko o tym, jak znaleźć „cykl”, który jest częściowo odpowiedzią na to pytanie. Twój link mówi o znalezieniu wszystkich cykli zgodnie z zadanym pytaniem, ale znowu wygląda na to, że używa tego samego podejścia co Ajay Garg, ale także robi wszystkie możliwe drzewa DFS.
Manohar Reddy Poreddy
Jest to niekompletne w przypadku grafów ukierunkowanych. Zobacz poprawną odpowiedź Kurta Peka.
Luke Hutchison
26

Moim zdaniem najbardziej zrozumiałym algorytmem wykrywania cyklu na ukierunkowanym wykresie jest algorytm kolorowania wykresów.

Zasadniczo algorytm kolorowania wykresów prowadzi wykres w systemie DFS (Głębokie pierwsze wyszukiwanie, co oznacza, że ​​bada ścieżkę całkowicie przed eksploracją innej ścieżki). Gdy znajdzie tylną krawędź, oznacza wykres jako zawierający pętlę.

Aby uzyskać szczegółowe wyjaśnienie algorytmu kolorowania wykresów, przeczytaj ten artykuł: http://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

Zapewniam również implementację kolorowania wykresów w JavaScript https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js

Armin Primadi
źródło
8

Jeśli nie możesz dodać właściwości „odwiedzonych” do węzłów, użyj zestawu (lub mapy) i po prostu dodaj wszystkie odwiedzone węzły do ​​zestawu, chyba że są już w zestawie. Użyj unikalnego klucza lub adresu obiektów jako „klucza”.

Daje to również informacje o węźle „root” cyklicznej zależności, który przyda się, gdy użytkownik będzie musiał rozwiązać problem.

Innym rozwiązaniem jest próba znalezienia następnej zależności do wykonania. Aby to zrobić, musisz mieć stos, w którym możesz zapamiętać, gdzie jesteś teraz i co musisz zrobić dalej. Sprawdź, czy zależność jest już na tym stosie, zanim ją wykonasz. Jeśli tak, znalazłeś cykl.

Chociaż może wydawać się, że ma złożoność O (N * M), musisz pamiętać, że stos ma bardzo ograniczoną głębokość (więc N jest mały) i że M staje się mniejszy z każdą zależnością, którą możesz sprawdzić jako „wykonany” plus możesz zatrzymać wyszukiwanie, gdy znajdziesz liść (więc nigdy nie musisz sprawdzać każdego węzła -> M też będzie mały).

W MetaMake utworzyłem wykres jako listę list, a następnie usunąłem każdy węzeł, gdy je wykonałem, co naturalnie zmniejszyło objętość wyszukiwania. W rzeczywistości nigdy nie musiałem przeprowadzać niezależnej kontroli, wszystko działo się automatycznie podczas normalnego wykonywania.

Jeśli potrzebujesz trybu „tylko test”, po prostu dodaj flagę „pracy na sucho”, która wyłącza wykonywanie rzeczywistych zadań.

Aaron Digulla
źródło
7

Nie ma algorytmu, który mógłby znaleźć wszystkie cykle na ukierunkowanym wykresie w czasie wielomianowym. Załóżmy, że skierowany wykres ma n węzłów, a każda para węzłów ma połączenia ze sobą, co oznacza, że ​​masz pełny wykres. Tak więc każdy niepusty podzbiór tych n węzłów wskazuje cykl i istnieje 2 ^ n-1 takich podzbiorów. Dlatego nie istnieje algorytm wielomianowy. Załóżmy więc, że dysponujesz wydajnym (nie głupim) algorytmem, który może określić liczbę ukierunkowanych cykli na wykresie, możesz najpierw znaleźć silnie połączone komponenty, a następnie zastosować swój algorytm na tych połączonych komponentach. Ponieważ cykle istnieją tylko wewnątrz komponentów, a nie między nimi.

Yuwen
źródło
1
Prawda, jeśli liczba węzłów zostanie przyjęta jako wielkość danych wejściowych. Można również opisać złożoność środowiska wykonawczego w kategoriach liczby krawędzi, a nawet cykli lub kombinacji tych miar. Algorytm „Znalezienie wszystkich obwodów elementarnych ukierunkowanego wykresu” Donalda B. Johnsona ma wielomianowy czas działania podany przez O ((n + e) ​​(c + 1)), gdzie n jest liczbą węzłów, e liczbą krawędzi oraz c liczba obwodów elementarnych wykresu. A oto moja implementacja tego algorytmu w Javie : github.com/1123/johnson .
user152468
4

Zaimplementowałem ten problem w sml (programowanie imperatywne). Oto zarys. Znajdź wszystkie węzły, które mają stopień niezależny lub stopień zerowy 0. Takie węzły nie mogą być częścią cyklu (więc je usuń). Następnie usuń wszystkie przychodzące lub wychodzące krawędzie z takich węzłów. Rekurencyjnie zastosuj ten proces do wynikowego wykresu. Jeśli na końcu nie pozostanie ci żaden węzeł ani krawędź, wykres nie będzie miał żadnych cykli, w przeciwnym razie ma.

Rpant
źródło
2

W ten sposób robię Sortowanie topologiczne, licząc liczbę odwiedzonych wierzchołków. Jeśli liczba ta jest mniejsza niż całkowita liczba wierzchołków w DAG, masz cykl.

Steve
źródło
4
To nie ma sensu. Jeśli wykres ma cykle, nie ma sortowania topologicznego, co oznacza, że ​​dowolny poprawny algorytm sortowania topologicznego zostanie przerwany.
sleske
4
z wikipedii: Wiele algorytmów sortowania topologicznego wykrywa również cykle, ponieważ są to przeszkody w istnieniu porządku topologicznego.
Oleg Mikheev
1
@OlegMikheev Tak, ale Steve mówi „Jeśli ta liczba jest mniejsza niż całkowita liczba wierzchołków w DAG, masz cykl”, co nie ma sensu.
nro
@nbro Założę się, że oznaczają one wariant algorytmu sortowania topologicznego, który przerywa się, gdy nie istnieje sortowanie topologiczne (a wtedy nie odwiedzają wszystkich wierzchołków).
maaartinus,
Jeśli wykonasz sortowanie topologiczne na wykresie z cyklem, skończysz z zamówieniem, które ma najmniejszą liczbę złych krawędzi (numer zamówienia> numer zamówienia sąsiada). Ale po sortowaniu łatwo jest wykryć te złe krawędzie, co powoduje wykrycie wykresu z cyklem
UGP
2

/mathpro/16393/finding-a-cycle-of-fixed-length Podoba mi się to rozwiązanie najlepiej na 4 długości :)

Również czarodziej mówi, że musisz zrobić O (V ^ 2). Uważam, że potrzebujemy tylko O ​​(V) / O (V + E). Jeśli wykres jest podłączony, DFS odwiedzi wszystkie węzły. Jeśli wykres ma połączone wykresy podrzędne, to za każdym razem, gdy uruchamiamy DFS na wierzchołku tego wykresu podrzędnego, znajdziemy połączone wierzchołki i nie będziemy musieli brać ich pod uwagę przy następnym uruchomieniu DFS. Dlatego możliwość działania dla każdego wierzchołka jest nieprawidłowa.

amitgoswami
źródło
1

Jeśli DFS znajdzie krawędź, która wskazuje na już odwiedzony wierzchołek, masz tam cykl.

mafonya
źródło
1
Nie działa na 1,2,3: 1,2; 1,3; 2,3;
głośny kot
4
@JakeGreene Zajrzyj tutaj: i.imgur.com/tEkM5xy.png Wystarczająco prosty do zrozumienia. Powiedzmy, że zaczynasz od 0. Następnie idziesz do węzła 1, nie ma już stamtąd ścieżek, reucrsion wraca. Teraz odwiedzasz węzeł 2, który ma krawędź do wierzchołka 1, który już był odwiedzany. Twoim zdaniem miałbyś wtedy cykl - a tak naprawdę nie masz
głośnego kota
3
@kittyPL Ten wykres nie zawiera cyklu. Z Wikipedii: „Cykl ukierunkowany na wykresie ukierunkowanym to sekwencja wierzchołków rozpoczynająca się i kończąca na tym samym wierzchołku, tak że dla każdego dwóch kolejnych wierzchołków cyklu istnieje krawędź skierowana od wcześniejszego do późniejszego” muszą być w stanie podążać ścieżką od V, która prowadzi z powrotem do V dla ukierunkowanego cyklu. Rozwiązanie mafonyi działa na dany problem
Jake Greene
2
@JakeGreene Oczywiście, że nie. Korzystając z algorytmu i zaczynając od 1, i tak wykryłbyś cykl ... Ten algorytm jest po prostu zły ... Zwykle wystarczyłoby cofnąć się za każdym razem, gdy napotkasz odwiedzony wierzchołek.
głośny kot
6
@kittyPL DFS działa w celu wykrycia cykli z danego węzła początkowego. Ale podczas wykonywania DFS musisz pokolorować odwiedzane węzły, aby odróżnić poprzeczkę od tylnej krawędzi. Po raz pierwszy odwiedzając wierzchołek zmienia kolor na szary, a następnie zmienia kolor na czarny, gdy wszystkie jego krawędzie zostaną odwiedzone. Jeśli podczas wykonywania DFS trafisz w szary wierzchołek, to ten wierzchołek jest przodkiem (tj .: masz cykl). Jeśli wierzchołek jest czarny, to jest to tylko krawędź.
Kyrra,
0

Jak powiedziałeś, masz zestaw zadań, które należy wykonać w określonej kolejności. Topological sortbiorąc pod uwagę wymaganą kolejność planowania zadań (lub problemów z zależnościami, jeśli jest to a direct acyclic graph). Uruchom dfsi utrzymuj listę i zacznij dodawać węzeł na początku listy, a jeśli napotkasz węzeł, który jest już odwiedzany. Następnie znalazłeś cykl na danym wykresie.

Bhagwati Malav
źródło
-11

Jeśli wykres spełnia tę właściwość

|e| > |v| - 1

wtedy wykres zawiera przynajmniej cykl.

dharmendra singh
źródło
10
Może tak być w przypadku grafów bezkierunkowych, ale z pewnością nie w przypadku grafów ukierunkowanych.
Hans-Peter Störr
6
Przeciwnym przykładem byłoby A-> B, B-> C, A-> C.
user152468
Nie wszystkie wierzchołki mają krawędzie.
Debanjan Dhar