El mejor algoritmo para detectar ciclos en un gráfico dirigido

396

¿Cuál es el algoritmo más eficiente para detectar todos los ciclos dentro de un gráfico dirigido?

Tengo un gráfico dirigido que representa una programación de trabajos que deben ejecutarse, un trabajo es un nodo y una dependencia es un borde. Necesito detectar el caso de error de un ciclo dentro de este gráfico que conduce a dependencias cíclicas.

Peauters
fuente
13
Dices que quieres detectar todos los ciclos, pero tu caso de uso sugiere que sería suficiente detectar si hay ciclos.
Steve Jessop el
29
Sería mejor para detectar todos los ciclos para que pudieran fijarse en una sola vez, en lugar de verificación, revisión, verificación, etc. fijar
Peauters
2
Debería leer el documento "Encontrar todos los circuitos elementales de un gráfico dirigido" de Donald B. Johnson. Encontrará solo circuitos elementales, pero esto debería ser suficiente para su caso. Y aquí está mi implementación Java de este algoritmo lista para usar: github.com/1123/johnson
user152468
Ejecute DFS con modificación adicional para el algoritmo: marque cada nodo que visitó. Si visita un nodo que ya ha visitado, entonces tiene un ciclo. cuando se retire de una ruta, desmarque los nodos que se visitan.
Hesham Yassin,
2
@HeshamYassin, si visitas un nodo que ya visitaste, no necesariamente significa que hay un bucle. Por favor lea mi comentario cs.stackexchange.com/questions/9676/… .
Maksim Dmitriev

Respuestas:

193

El algoritmo de componentes fuertemente conectados de Tarjan tiene O(|E| + |V|)complejidad de tiempo.

Para otros algoritmos, vea Componentes fuertemente conectados en Wikipedia.

aku
fuente
70
¿Cómo encontrar los componentes fuertemente conectados te dice acerca de los ciclos que existen en el gráfico?
Peter
44
Puede que alguien pueda confirmar, pero el algoritmo de Tarjan no admite ciclos de nodos que apuntan directamente a sí mismos, como A-> A.
Cédric Guillemette
24
@Cedrik Correcto, no directamente. Esto no es una falla en el algoritmo de Tarjan, sino la forma en que se usa para esta pregunta. Tarjan no encuentra ciclos directamente , encuentra componentes fuertemente conectados. Por supuesto, cualquier SCC con un tamaño mayor que 1 implica un ciclo. Los componentes no cíclicos tienen un SCC singleton por sí mismos. El problema es que un bucle automático también entrará en un SCC por sí mismo. Por lo tanto, necesita una comprobación por separado para los bucles automáticos, lo cual es bastante trivial.
mgiuca
13
(todos los componentes fuertemente conectados en el gráfico)! = (todos los ciclos en el gráfico)
optimusfrenk
44
@ aku: un DFS tricolor también tiene el mismo tiempo de ejecución O(|E| + |V|). Usando codificación de color blanco (nunca visitado), gris (el nodo actual se visita pero todos los nodos accesibles aún no se han visitado) y negro (todos los nodos accesibles se visitan junto con el actual), si un nodo gris encuentra otro nodo gris, entonces ' Ve un ciclo. [Más o menos lo que tenemos en el libro de algoritmos de Cormen]. ¡Me pregunto si el 'algoritmo de Tarjan' tiene algún beneficio sobre tal DFS!
KGhatak
73

Dado que este es un cronograma de trabajos, sospecho que en algún momento los clasificará en un orden de ejecución propuesto.

Si ese es el caso, una implementación de ordenamiento topológico en cualquier caso puede detectar ciclos. UNIX tsortciertamente lo hace. Creo que es probable que, por lo tanto, sea más eficiente detectar ciclos al mismo tiempo que la clasificación, en lugar de en un paso separado.

Por lo tanto, la pregunta podría ser: "¿Cómo hago una clasificación más eficiente", en lugar de "¿Cómo detecto los bucles más eficientemente?". Para lo cual la respuesta es probablemente "usar una biblioteca", pero en su defecto el siguiente artículo de Wikipedia:

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

tiene el pseudocódigo para un algoritmo y una breve descripción de otro de Tarjan. Ambos tienen O(|V| + |E|)complejidad de tiempo.

Steve Jessop
fuente
Un tipo topológico puede detectar ciclos, en la medida en que se basa en un algoritmo de búsqueda de profundidad primero, pero necesita una contabilidad adicional para detectar realmente los ciclos. Ver la respuesta correcta de Kurt Peek.
Luke Hutchison
33

La forma más sencilla de hacerlo es hacer un primer recorrido en profundidad (DFT) del gráfico .

Si el gráfico tiene nvértices, este es un O(n)algoritmo de complejidad temporal. Como posiblemente tendrá que hacer un DFT a partir de cada vértice, la complejidad total se vuelve O(n^2).

Debe mantener una pila que contenga todos los vértices en el primer recorrido de profundidad actual , siendo su primer elemento el nodo raíz. Si te encuentras con un elemento que ya está en la pila durante el DFT, entonces tienes un ciclo.

nbro
fuente
21
Esto sería cierto para un gráfico "regular", pero es falso para un gráfico dirigido . Por ejemplo, considere el "diagrama de dependencia del diamante" con cuatro nodos: A con bordes apuntando a B y C, cada uno de los cuales tiene un borde apuntando a D. Su recorrido DFT de este diagrama desde A concluiría incorrectamente que el "bucle" fue en realidad un ciclo, aunque hay un ciclo, no es un ciclo porque no se puede recorrer siguiendo las flechas.
Peter
99
@peter, ¿puede explicar cómo DFT de A concluirá incorrectamente que hay un ciclo?
Deepak
10
@Deepak - De hecho, leí mal la respuesta del "asistente de física": donde escribió "en la pila" Pensé que "ya se ha encontrado". De hecho, sería suficiente (para detectar un bucle dirigido) verificar si hay engaños "en la pila" durante la ejecución de un DFT. Un voto a favor para cada uno de ustedes.
Peter
2
¿Por qué dice que la complejidad del tiempo es O(n)mientras sugiere verificar la pila para ver si ya contiene un nodo visitado? Escanear la pila agrega tiempo al tiempo de O(n)ejecución porque tiene que escanear la pila en cada nuevo nodo. Puede lograrlo O(n)si marca los nodos visitados
James Wierzba
Como dijo Peter, esto está incompleto para los gráficos dirigidos. Ver la respuesta correcta de Kurt Peek.
Luke Hutchison
32

De acuerdo con el Lema 22.11 de Cormen et al., Introducción a los algoritmos (CLRS):

Un gráfico dirigido G es acíclico si y solo si una búsqueda en profundidad de G no produce bordes posteriores.

Esto ha sido mencionado en varias respuestas; Aquí también proporcionaré un ejemplo de código basado en el capítulo 22 de CLRS. El gráfico de ejemplo se ilustra a continuación.

ingrese la descripción de la imagen aquí

El seudocódigo de CLRS para lecturas de búsqueda en profundidad primero:

ingrese la descripción de la imagen aquí

En el ejemplo de la Figura 22.4 de CLRS, el gráfico consta de dos árboles DFS: uno formado por los nodos u , v , x e y , y el otro por los nodos w y z . Cada árbol contiene un borde posterior: uno de x a v y otro de z a z (un bucle automático).

La comprensión clave es que se encuentra un borde posterior cuando, en la DFS-VISITfunción, al iterar sobre los vecinos vde u, se encuentra un nodo con el GRAYcolor.

El siguiente código de Python es una adaptación del pseudocódigo de CLRS con una ifcláusula agregada que detecta ciclos:

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)

Tenga en cuenta que en este ejemplo, el timepseudocódigo en CLRS no se captura porque solo estamos interesados ​​en detectar ciclos. También hay un código repetitivo para construir la representación de la lista de adyacencia de un gráfico a partir de una lista de bordes.

Cuando se ejecuta este script, imprime el siguiente resultado:

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

Estos son exactamente los bordes posteriores en el ejemplo en CLRS Figura 22.4.

Kurt Peek
fuente
29

Comience con un DFS: existe un ciclo si y solo si se descubre un back-edge durante DFS . Esto se demuestra como resultado del teorum de trayectoria blanca.

Ajay Garg
fuente
3
Sí, pienso lo mismo, pero esto no es suficiente, publico mi camino cs.stackexchange.com/questions/7216/find-the-simple-cycles-in-a-directed-graph
jonaprieto
Cierto. Ajay Garg solo cuenta cómo encontrar "un ciclo", que es una respuesta parcial a esta pregunta. Su enlace habla de encontrar todos los ciclos según la pregunta formulada, pero nuevamente parece que usa el mismo enfoque que Ajay Garg, pero también hace todos los posibles árboles dfs.
Manohar Reddy Poreddy
Esto está incompleto para gráficos dirigidos. Ver la respuesta correcta de Kurt Peek.
Luke Hutchison
26

En mi opinión, el algoritmo más comprensible para detectar el ciclo en un gráfico dirigido es el algoritmo de coloreado de gráficos.

Básicamente, el algoritmo de coloración del gráfico recorre el gráfico de una manera DFS (Profund First Search, lo que significa que explora una ruta completamente antes de explorar otra ruta). Cuando encuentra un borde posterior, marca el gráfico como que contiene un bucle.

Para obtener una explicación detallada del algoritmo de coloración de gráficos, lea este artículo: http://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

Además, proporciono una implementación de coloración de gráficos en JavaScript https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js

Armin Primadi
fuente
8

Si no puede agregar una propiedad "visitada" a los nodos, use un conjunto (o mapa) y simplemente agregue todos los nodos visitados al conjunto a menos que ya estén en el conjunto. Use una clave única o la dirección de los objetos como "clave".

Esto también le proporciona la información sobre el nodo "raíz" de la dependencia cíclica que será útil cuando un usuario tenga que solucionar el problema.

Otra solución es tratar de encontrar la siguiente dependencia para ejecutar. Para esto, debe tener una pila donde pueda recordar dónde se encuentra ahora y qué debe hacer a continuación. Compruebe si ya hay una dependencia en esta pila antes de ejecutarla. Si es así, has encontrado un ciclo.

Si bien esto puede parecer que tiene una complejidad de O (N * M), debe recordar que la pila tiene una profundidad muy limitada (por lo que N es pequeña) y que M se vuelve más pequeña con cada dependencia que puede marcar como "ejecutada" más puede detener la búsqueda cuando encuentre una hoja (para que nunca tenga que verificar cada nodo -> M también será pequeño).

En MetaMake, creé el gráfico como una lista de listas y luego eliminé cada nodo a medida que los ejecutaba, lo que naturalmente redujo el volumen de búsqueda. En realidad, nunca tuve que ejecutar una verificación independiente, todo sucedió automáticamente durante la ejecución normal.

Si necesita un modo de "solo prueba", simplemente agregue un indicador de "ejecución en seco" que deshabilita la ejecución de los trabajos reales.

Aaron Digulla
fuente
7

No existe un algoritmo que pueda encontrar todos los ciclos en un gráfico dirigido en tiempo polinómico. Supongamos que el gráfico dirigido tiene n nodos y cada par de nodos tiene conexiones entre sí, lo que significa que tiene un gráfico completo. Entonces, cualquier subconjunto no vacío de estos n nodos indica un ciclo y hay 2 ^ n-1 número de tales subconjuntos. Por lo tanto, no existe un algoritmo de tiempo polinómico. Supongamos que tiene un algoritmo eficiente (no estúpido) que le puede decir la cantidad de ciclos dirigidos en un gráfico, primero puede encontrar los componentes conectados fuertes y luego aplicar su algoritmo en estos componentes conectados. Dado que los ciclos solo existen dentro de los componentes y no entre ellos.

Yuwen
fuente
1
Es cierto, si el número de nodos se toma como el tamaño de la entrada. También podría describir la complejidad del tiempo de ejecución en términos del número de aristas o incluso ciclos, o una combinación de estas medidas. El algoritmo "Encontrar todos los circuitos elementales de un gráfico dirigido" por Donald B. Johnson tiene un tiempo de ejecución polinómico dado por O ((n + e) ​​(c + 1)) donde n es el número de nodos, e el número de aristas yc el número de circuitos elementales de la gráfica. Y aquí está mi implementación Java de este algoritmo: github.com/1123/johnson .
user152468
4

Había implementado este problema en sml (programación imperativa). Aquí está el bosquejo. Encuentre todos los nodos que tengan un grado de entrada o salida de 0. Tales nodos no pueden ser parte de un ciclo (así que elimínelos). Luego, elimine todos los bordes entrantes o salientes de dichos nodos. Aplicar recursivamente este proceso al gráfico resultante. Si al final no le queda ningún nodo o borde, el gráfico no tiene ningún ciclo, de lo contrario lo tiene.

Rpant
fuente
2

La forma en que lo hago es hacer un ordenamiento topológico, contando el número de vértices visitados. Si ese número es menor que el número total de vértices en el DAG, tiene un ciclo.

Steve
fuente
44
Eso no tiene sentido. Si el gráfico tiene ciclos, no hay ordenación topológica, lo que significa que cualquier algoritmo correcto para la ordenación topológica abortará.
sleske
44
de wikipedia: Muchos algoritmos de clasificación topológica también detectarán ciclos, ya que son obstáculos para que exista el orden topológico.
Oleg Mikheev
1
@OlegMikheev Sí, pero Steve dice "Si ese número es menor que el número total de vértices en el DAG, tienes un ciclo", lo cual no tiene sentido.
nbro
@nbro Apuesto a que se refieren a una variante del algoritmo de clasificación topológica que aborta cuando no existe una clasificación topológica (y luego no visitan todos los vértices).
maaartinus
Si realiza una clasificación topológica en un gráfico con ciclo, terminará con un orden que tenga la menor cantidad de bordes defectuosos (número de orden> número de orden del vecino). Pero después de que tiene que ordenar, es fácil detectar esos bordes defectuosos que resultan en la detección de un gráfico con un ciclo
UGP
2

/mathpro/16393/finding-a-cycle-of-fixed-length Me gusta esta solución, la mejor especialmente para 4 longitudes :)

También el asistente físico dice que tienes que hacer O (V ^ 2). Creo que solo necesitamos O (V) / O (V + E). Si el gráfico está conectado, DFS visitará todos los nodos. Si el gráfico tiene gráficos secundarios conectados, cada vez que ejecutamos un DFS en un vértice de este gráfico secundario, encontraremos los vértices conectados y no tendremos que considerarlos para la próxima ejecución del DFS. Por lo tanto, la posibilidad de ejecutar para cada vértice es incorrecta.

amitgoswami
fuente
1

Si DFS encuentra un borde que apunta a un vértice ya visitado, tiene un ciclo allí.

mafonya
fuente
1
Falla en 1,2,3: 1,2; 1,3; 2,3;
gato ruidoso
44
@JakeGreene Mira aquí: i.imgur.com/tEkM5xy.png Suficientemente simple de entender. Digamos que comienzas desde 0. Luego vas al nodo 1, no hay más caminos desde allí, la recursión regresa. Ahora visita el nodo 2, que tiene un borde al vértice 1, que ya fue visitado. En su opinión, tendría un ciclo entonces, y no tiene uno realmente
gato ruidoso
3
@kittyPL Ese gráfico no contiene un ciclo. De Wikipedia: "Un ciclo dirigido en un gráfico dirigido es una secuencia de vértices que comienza y termina en el mismo vértice de tal manera que, para cada dos vértices consecutivos del ciclo, existe un borde dirigido desde el vértice anterior al posterior". tiene que poder seguir un camino desde V que lo lleve de regreso a V durante un ciclo dirigido. La solución de mafonya funciona para el problema dado
Jake Greene
2
@JakeGreene Por supuesto que no. Usando su algoritmo y comenzando desde 1, detectaría un ciclo de todos modos ... Este algoritmo es simplemente malo ... Por lo general, sería suficiente caminar hacia atrás cada vez que encuentre un vértice visitado.
gato ruidoso
66
@kittyPL DFS funciona para detectar ciclos desde el nodo de inicio dado. Pero al hacer DFS, debe colorear los nodos visitados para distinguir un borde cruzado del borde posterior. La primera vez que visita un vértice se vuelve gris, luego lo vuelve negro una vez que se han visitado todos sus bordes. Si al hacer el DFS golpeas un vértice gris, entonces ese vértice es un antepasado (es decir, tienes un ciclo). Si el vértice es negro, entonces es solo un borde cruzado.
Kyrra
0

Como dijiste, tienes un conjunto de trabajos, debe ejecutarse en cierto orden. Topological sortdado el pedido requerido para la programación de trabajos (o para problemas de dependencia si es a direct acyclic graph). Ejecute dfsy mantenga una lista, y comience a agregar un nodo al comienzo de la lista, y si encontró un nodo que ya ha sido visitado. Luego encontraste un ciclo en un gráfico dado.

Bhagwati Malav
fuente
-11

Si un gráfico satisface esta propiedad

|e| > |v| - 1

entonces el gráfico contiene al menos el ciclo.

dharmendra singh
fuente
10
Eso podría ser cierto para los gráficos no dirigidos, pero ciertamente no para los gráficos dirigidos.
Hans-Peter Störr
66
Un ejemplo contrario sería A-> B, B-> C, A-> C.
user152468
No todos los vértices tienen bordes.
Debanjan Dhar el