Il miglior algoritmo per rilevare i cicli in un grafico diretto


396

Qual è l'algoritmo più efficiente per rilevare tutti i cicli all'interno di un grafico diretto?

Ho un grafico diretto che rappresenta un programma di lavori che devono essere eseguiti, un lavoro è un nodo e una dipendenza è un vantaggio. Devo rilevare il caso di errore di un ciclo all'interno di questo grafico che porta a dipendenze cicliche.


13
Dici di voler rilevare tutti i cicli, ma il tuo caso d'uso suggerisce che sarebbe sufficiente rilevare se ci sono dei cicli.
Steve Jessop,

29
Sarebbe meglio rilevare tutti i cicli in modo che possano essere risolti in una volta, piuttosto che controllare, correggere, controllare, correggere ecc.
Peauters

2
Dovresti leggere l'articolo "Trovare tutti i circuiti elementari di un grafico diretto" di Donald B. Johnson. Troverà solo circuiti elementari, ma questo dovrebbe essere sufficiente per il tuo caso. Ed ecco la mia implementazione Java di questo algoritmo pronto per l'uso: github.com/1123/johnson
user152468

Esegui DFS con ulteriori modifiche per l'algoritmo: segna ogni nodo che hai visitato. se visiti un nodo già visitato, allora hai un ciclo. quando ci si ritira da un percorso, deselezionare i nodi visitati.
Hesham Yassin,

2
@HeshamYassin, se visiti un nodo che hai già visitato, non significa necessariamente che ci sia un loop. Si prega di leggere il mio commento cs.stackexchange.com/questions/9676/… .
Maksim Dmitriev il

Risposte:


193

L'algoritmo dei componenti fortemente connessi di Tarjan haO(|E| + |V|) complessità temporale.

Per altri algoritmi, consultare Componenti fortemente connesse su Wikipedia.


70
In che modo trovare i componenti fortemente connessi ti dice dei cicli che esistono nel grafico?
Peter,

4
Qualcuno può confermare, ma l'algoritmo Tarjan non supporta cicli di nodi che puntano direttamente a se stessi, come A-> A.
Cédric Guillemette,

24
@Cedrik Giusto, non direttamente. Questo non è un difetto dell'algoritmo di Tarjan, ma il modo in cui viene utilizzato per questa domanda. Tarjan non trova direttamente i cicli , trova componenti fortemente collegati. Naturalmente, qualsiasi SCC con una dimensione maggiore di 1 implica un ciclo. I componenti non ciclici hanno un SCC singleton da soli. Il problema è che un self-loop andrà anche in un SCC da solo. Quindi è necessario un controllo separato per i loop automatici, il che è piuttosto banale.
mgiuca,

13
(tutti i componenti fortemente collegati nel grafico)! = (tutti i cicli nel grafico)
optimusfrenk

4
@ aku: anche un DFS a tre colori ha lo stesso tempo di esecuzione O(|E| + |V|). Usando il codice colore bianco (mai visitato), grigio (il nodo corrente è visitato ma non tutti i nodi raggiungibili sono ancora visitati) e nero (tutti i nodi raggiungibili sono visitati insieme a quello corrente), se un nodo grigio trova un altro nodo grigio, allora ' ho un ciclo. [Praticamente quello che abbiamo nel libro dell'algoritmo di Cormen]. Mi chiedo se "l'algoritmo di Tarjan" abbia qualche vantaggio rispetto a tale DFS !!
KGhatak,

73

Dato che si tratta di un programma di lavori, ho il sospetto che ad un certo punto stai per ordinare li in un ordine di esecuzione proposto.

In tal caso, un'implementazione di ordinamento topologico può comunque rilevare cicli. UNIXtsort fa certamente. Penso sia probabile che sia quindi più efficiente rilevare cicli contemporaneamente allo tsorting, piuttosto che in una fase separata.

Quindi la domanda potrebbe diventare: "come posso eseguire lo tsort nel modo più efficiente", piuttosto che "come posso rilevare i loop nel modo più efficiente". A cui probabilmente la risposta è "usa una libreria", ma in mancanza del seguente articolo di Wikipedia:

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

ha lo pseudo-codice per un algoritmo e una breve descrizione di un altro da Tarjan. Entrambi hanno una O(|V| + |E|)complessità temporale.


Un ordinamento topologico è in grado di rilevare i cicli, in quanto si basa su un algoritmo di ricerca approfondito, ma è necessaria una contabilità aggiuntiva per rilevare effettivamente i cicli. Vedi la risposta corretta di Kurt Peek.
Luke Hutchison,

33

Il modo più semplice per farlo è eseguire una prima traversata in profondità (DFT) del grafico .

Se il grafico ha nvertici, questo è un O(n)algoritmo di complessità temporale. Dal momento che probabilmente dovrai fare un DFT a partire da ciascun vertice, la complessità totale diventaO(n^2) .

Devi mantenere uno stack contenente tutti i vertici nel primo attraversamento della profondità corrente , con il suo primo elemento come nodo radice. Se ti imbatti in un elemento che è già nello stack durante il DFT, allora hai un ciclo.


21
Questo sarebbe vero per un grafico "normale", ma è falso per un grafico diretto . Ad esempio, considera il "diagramma di dipendenza del diamante" con quattro nodi: A con i bordi che puntano a B e C, ciascuno dei quali ha un bordo che punta a D. Il tuo attraversamento DFT di questo diagramma da A concluderebbe erroneamente che il "loop" era in realtà un ciclo - sebbene ci sia un ciclo, non è un ciclo perché non può essere attraversato seguendo le frecce.
Peter,

9
@peter puoi spiegare come DFT di A concluderà erroneamente che esiste un ciclo?
Deepak,

10
@Deepak - In effetti, ho letto male la risposta di "mago fisico": dove ha scritto "nello stack" Ho pensato "è già stato trovato". Sarebbe infatti sufficiente (per rilevare un loop diretto) verificare la presenza di duplicati "nello stack" durante l'esecuzione di un DFT. Un voto per ciascuno di voi.
Peter,

2
Perché dici che la complessità del tempo è O(n)mentre suggerisci di controllare lo stack per vedere se contiene già un nodo visitato? La scansione dello stack aggiunge tempo al O(n)runtime perché deve scansionare lo stack su ogni nuovo nodo. Puoi ottenere O(n)se contrassegni i nodi visitati
James Wierzba,

Come ha detto Peter, questo è incompleto per i grafici diretti. Vedi la risposta corretta di Kurt Peek.
Luke Hutchison,

32

Secondo Lemma 22.11 di Cormen et al., Introduzione agli algoritmi (CLRS):

Un grafico diretto G è aciclico se e solo se una ricerca in profondità di G non produce bordi posteriori.

Questo è stato menzionato in diverse risposte; qui fornirò anche un esempio di codice basato sul capitolo 22 di CLRS. Il grafico di esempio è illustrato di seguito.

inserisci qui la descrizione dell'immagine

Lo pseudo-codice CLRS per le ricerche approfondite:

inserisci qui la descrizione dell'immagine

Nell'esempio in Figura 22.4 di CLRS, il grafico è costituito da due alberi DFS: uno costituito da nodi u , v , x e y e l'altro da nodi w e z . Ogni albero contiene un bordo posteriore: uno da x a v e un altro da z a z (un loop automatico).

La chiave di realizzazione è che si incontra un bordo posteriore quando, nella DFS-VISITfunzione, mentre scorre sui vicini vdi u, si incontra un nodo con il GRAYcolore.

Il seguente codice Python è un adattamento dello pseudocodice di CLRS con una ifclausola aggiunta che rileva i cicli:

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)

Si noti che in questo esempio, il time pseudocodice in CLRS non viene acquisito perché ci interessa solo rilevare i cicli. Esiste anche un codice di boilerplate per creare la rappresentazione dell'elenco di adiacenza di un grafico da un elenco di bordi.

Quando questo script viene eseguito, stampa il seguente output:

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

Questi sono esattamente i bordi posteriori nell'esempio in CLRS Figura 22.4.


29

Inizia con un DFS: esiste un ciclo se e solo se viene rilevato un back-edge durante DFS . Ciò è dimostrato come risultato della teoria del percorso bianco.


3
Sì, penso lo stesso, ma non è abbastanza, inserisco la mia strada cs.stackexchange.com/questions/7216/find-the-simple-cycles-in-a-directed-graph
jonaprieto

Vero. Ajay Garg sta solo raccontando come trovare "un ciclo", che è una risposta parziale a questa domanda. Il tuo link parla di trovare tutti i cicli secondo la domanda posta, ma sembra che usi lo stesso approccio di Ajay Garg, ma fa anche tutti i possibili dfs-tree.
Manohar Reddy Poreddy l'

Questo è incompleto per i grafici diretti. Vedi la risposta corretta di Kurt Peek.
Luke Hutchison,

26

A mio avviso, l'algoritmo più comprensibile per rilevare il ciclo in un grafico diretto è l'algoritmo di colorazione del grafico.

Fondamentalmente, l'algoritmo di colorazione del grafico percorre il grafico in modo DFS (Depth First Search, il che significa che esplora completamente un percorso prima di esplorarne un altro). Quando trova un bordo posteriore, segna il grafico come contenente un ciclo.

Per una spiegazione approfondita dell'algoritmo di colorazione dei grafici, leggi questo articolo: http://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

Inoltre, fornisco un'implementazione della colorazione dei grafici in JavaScript https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js


8

Se non è possibile aggiungere una proprietà "visitata" ai nodi, utilizzare un set (o mappa) e aggiungere semplicemente tutti i nodi visitati al set a meno che non siano già nel set. Utilizzare una chiave univoca o l'indirizzo degli oggetti come "chiave".

Questo ti dà anche le informazioni sul nodo "root" della dipendenza ciclica che sarà utile quando un utente deve risolvere il problema.

Un'altra soluzione è cercare di trovare la dipendenza successiva da eseguire. Per questo, devi avere un po 'di stack in cui puoi ricordare dove sei ora e cosa devi fare dopo. Controllare se una dipendenza è già in questo stack prima di eseguirla. Se lo è, hai trovato un ciclo.

Mentre questo potrebbe sembrare avere una complessità di O (N * M), devi ricordare che lo stack ha una profondità molto limitata (quindi N è piccolo) e che M diventa più piccola con ogni dipendenza che puoi spuntare come "eseguita" più puoi interrompere la ricerca quando hai trovato una foglia (quindi non devi mai controllare ogni nodo -> M sarà anch'esso piccolo).

In MetaMake, ho creato il grafico come un elenco di elenchi e quindi ho eliminato tutti i nodi mentre li eseguivo, riducendo naturalmente il volume di ricerca. In realtà non ho mai dovuto eseguire un controllo indipendente, tutto è accaduto automaticamente durante la normale esecuzione.

Se è necessaria una modalità "solo test", è sufficiente aggiungere un flag "dry-run" che disabilita l'esecuzione dei lavori effettivi.


7

Non esiste un algoritmo in grado di trovare tutti i cicli in un grafico diretto in tempo polinomiale. Supponiamo che il grafico diretto abbia n nodi e ogni coppia di nodi abbia connessioni tra loro, il che significa che hai un grafico completo. Quindi qualsiasi sottoinsieme non vuoto di questi n nodi indica un ciclo e ci sono 2 ^ n-1 numero di tali sottogruppi. Quindi non esiste un algoritmo temporale polinomiale. Supponiamo quindi che tu abbia un algoritmo efficiente (non stupido) in grado di dirti il ​​numero di cicli diretti in un grafico, puoi prima trovare i componenti forti collegati, quindi applicare il tuo algoritmo su questi componenti collegati. Poiché i cicli esistono solo all'interno dei componenti e non tra di essi.


1
Vero, se il numero di nodi viene preso come dimensione dell'input. È inoltre possibile descrivere la complessità del tempo di esecuzione in termini di numero di spigoli o cicli pari o una combinazione di queste misure. L'algoritmo "Trovare tutti i circuiti elementari di un grafico diretto" di Donald B. Johnson ha un tempo di esecuzione polinomiale dato da O ((n + e) ​​(c + 1)) dove n è il numero di nodi, e il numero di spigoli e c il numero di circuiti elementari del grafico. Ed ecco la mia implementazione Java di questo algoritmo: github.com/1123/johnson .
user152468,

4

Avevo implementato questo problema in sml (programmazione imperativa). Ecco il contorno. Trova tutti i nodi che hanno un livello di errore o un livello inferiore a 0. Tali nodi non possono far parte di un ciclo (quindi rimuoverli). Quindi rimuovere tutti i bordi in entrata o in uscita da tali nodi. Applicare in modo ricorsivo questo processo al grafico risultante. Se alla fine non ti rimane alcun nodo o bordo, il grafico non ha cicli, altrimenti lo è.


2

Il modo in cui lo faccio è fare un ordinamento topologico, contando il numero di vertici visitati. Se quel numero è inferiore al numero totale di vertici nel DAG, hai un ciclo.


4
Questo non ha senso. Se il grafico presenta cicli, non esiste un ordinamento topologico, il che significa che qualsiasi algoritmo corretto per l'ordinamento topologico verrà interrotto.
sleske,

4
da wikipedia: anche molti algoritmi di ordinamento topologico rileveranno cicli, poiché questi sono ostacoli all'esistenza dell'ordine topologico.
Oleg Mikheev,

1
@OlegMikheev Sì, ma Steve sta dicendo "Se quel numero è inferiore al numero totale di vertici nel DAG, hai un ciclo", il che non ha senso.
nbro,

@nbro Scommetto che significano una variante dell'algoritmo di ordinamento topologico che si interrompe quando non esiste un ordinamento topologico (e quindi non visitano tutti i vertici).
maaartinus,

Se si esegue un ordinamento topologico su un grafico con ciclo, si otterrà un ordine con il minor numero di bordi non validi (numero ordine> numero ordine del vicino). Ma dopo lo smistamento è facile rilevare quei bordi difettosi con conseguente rilevazione di un grafico con un ciclo
UGP

2

/mathpro/16393/finding-a-cycle-of-fixed-length Mi piace questa soluzione la migliore specialmente per 4 lunghezze :)

Anche il mago fisico dice che devi fare O (V ^ 2). Credo che abbiamo bisogno solo di O (V) / O (V + E). Se il grafico è collegato, DFS visiterà tutti i nodi. Se il grafico ha dei sotto-grafici collegati, ogni volta che eseguiamo un DFS su un vertice di questo sotto-grafico troveremo i vertici collegati e non dovremo considerare questi per la prossima corsa del DFS. Pertanto la possibilità di esecuzione per ciascun vertice non è corretta.


1

Se DFS trova un bordo che punta a un vertice già visitato, hai un ciclo lì.


1
Errore su 1,2,3: 1,2; 1,3; 2,3;
gatto rumoroso

4
@JakeGreene Guarda qui: i.imgur.com/tEkM5xy.png Abbastanza semplice da capire. Diciamo che inizi da 0. Quindi vai al nodo 1, non ci sono più percorsi da lì, la reindirizzamento torna indietro. Ora visiti il ​​nodo 2, che ha un margine rispetto al vertice 1, che era già stato visitato. Secondo te allora avresti un ciclo - e non ne hai davvero uno
gatto rumoroso

3
@kittyPL Questo grafico non contiene un ciclo. Da Wikipedia: "Un ciclo diretto in un grafico diretto è una sequenza di vertici che inizia e termina allo stesso vertice in modo tale che, per ogni due vertici consecutivi del ciclo, esiste un bordo diretto dal vertice precedente al successivo" Tu deve essere in grado di seguire un percorso da V che riconduce a V per un ciclo diretto. La soluzione di mafonya funziona per il problema dato
Jake Greene,

2
@JakeGreene Certo che no. Usando il tuo algoritmo e partendo da 1 rileveresti comunque un ciclo ... Questo algoritmo è semplicemente cattivo ... Di solito sarebbe sufficiente camminare all'indietro ogni volta che incontri un vertice visitato.
gatto rumoroso

6
@kittyPL DFS funziona per rilevare i cicli dal nodo iniziale specificato. Ma quando si esegue DFS è necessario colorare i nodi visitati per distinguere un bordo trasversale da quello posteriore. La prima volta che visiti un vertice diventa grigio, poi lo diventi nero una volta che tutti i suoi bordi sono stati visitati. Se quando fai il DFS colpisci un vertice grigio, quel vertice è un antenato (cioè: hai un ciclo). Se il vertice è nero, allora è solo una croce.
Kyrra il

0

Come hai detto, hai una serie di lavori, deve essere eseguito in un certo ordine. Topological sortdato l'ordine richiesto per la pianificazione dei lavori (o per problemi di dipendenza se è un direct acyclic graph). Esegui dfse gestisci un elenco e inizia ad aggiungere un nodo all'inizio dell'elenco e se hai riscontrato un nodo già visitato. Quindi hai trovato un ciclo nel grafico dato.


-11

Se un grafico soddisfa questa proprietà

|e| > |v| - 1

quindi il grafico contiene almeno sul ciclo.


10
Questo potrebbe essere vero per i grafici non indirizzati, ma certamente non per i grafici diretti.
Hans-Peter Störr,

6
Un contro esempio sarebbe A-> B, B-> C, A-> C.
user152468,

Non tutti i vertici hanno bordi.
Debanjan Dhar
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.