Meilleur algorithme pour détecter les cycles dans un graphique dirigé

396

Quel est l'algorithme le plus efficace pour détecter tous les cycles dans un graphe orienté?

J'ai un graphique dirigé représentant un calendrier des travaux qui doivent être exécutés, un travail étant un nœud et une dépendance étant un bord. J'ai besoin de détecter le cas d'erreur d'un cycle dans ce graphique conduisant à des dépendances cycliques.

Peauters
la source
13
Vous dites que vous voulez détecter tous les cycles, mais votre cas d'utilisation suggère qu'il suffirait de détecter s'il y a des cycles.
Steve Jessop
29
Il serait préférable de détecter tous les cycles afin qu'ils puissent être corrigés en une seule fois, plutôt que de vérifier, réparer, vérifier, corriger etc.
Peauters
2
Vous devriez lire l'article "Trouver tous les circuits élémentaires d'un graphe orienté" de Donald B. Johnson. Il ne trouvera que des circuits élémentaires, mais cela devrait suffire pour votre cas. Et voici mon implémentation Java de cet algorithme prêt à l'emploi: github.com/1123/johnson
user152468
Exécutez DFS avec une modification supplémentaire pour l'algorithme: marquez chaque nœud que vous avez visité. si vous visitez un nœud qui est déjà visité, vous avez un cicle. lorsque vous vous retirez d'un chemin, décochez les nœuds visités.
Hesham Yassin
2
@HeshamYassin, si vous visitez un nœud que vous avez déjà visité, cela ne signifie pas nécessairement qu'il y a une boucle. Veuillez lire mon commentaire cs.stackexchange.com/questions/9676/… .
Maksim Dmitriev

Réponses:

193

L'algorithme des composants fortement connectés de Tarjan a une O(|E| + |V|)complexité temporelle.

Pour d'autres algorithmes, voir Composants fortement connectés sur Wikipédia.

aku
la source
70
Comment la recherche des composants fortement connectés vous informe-t-elle sur les cycles qui existent dans le graphique?
Peter
4
Peut-être que quelqu'un peut confirmer, mais l'algorithme de Tarjan ne prend pas en charge les cycles de nœuds pointant directement vers eux-mêmes, comme A-> A.
Cédric Guillemette
24
@Cedrik Oui, pas directement. Ce n'est pas un défaut dans l'algorithme de Tarjan, mais la façon dont il est utilisé pour cette question. Tarjan ne trouve pas directement les cycles , il trouve des composants fortement connectés. Bien sûr, tout SCC avec une taille supérieure à 1 implique un cycle. Les composants non cycliques ont un SCC singleton par eux-mêmes. Le problème est qu'une auto-boucle ira également dans un SCC par elle-même. Vous avez donc besoin d'une vérification distincte pour les boucles automatiques, ce qui est assez trivial.
mgiuca
13
(tous les composants fortement connectés dans le graphique)! = (tous les cycles dans le graphique)
optimusfrenk
4
@ aku: Un DFS en trois couleurs a également le même temps d'exécution O(|E| + |V|). En utilisant le codage couleur blanc (jamais visité), gris (le nœud actuel est visité mais tous les nœuds accessibles ne sont pas encore visités) et noir (tous les nœuds accessibles sont visités avec le courant), si un nœud gris trouve un autre nœud gris, alors nous '' ve un cycle. [À peu près ce que nous avons dans le livre d'algorithmes de Cormen]. Vous vous demandez si «l'algorithme de Tarjan» a un quelconque avantage sur un tel DFS !!
KGhatak
73

Étant donné qu'il s'agit d'un calendrier de travaux, je soupçonne qu'à un moment donné, vous allez les trier dans un ordre d'exécution proposé.

Si tel est le cas, une implémentation de tri topologique peut dans tous les cas détecter les cycles. UNIX le fait tsortcertainement. Je pense qu'il est probable qu'il est donc plus efficace de détecter des cycles en même temps que le tri, plutôt que dans une étape distincte.

Ainsi, la question pourrait devenir «comment puis-je tsort plus efficacement», plutôt que «comment puis-je détecter plus efficacement les boucles». Pour lequel la réponse est probablement "utiliser une bibliothèque", mais à défaut l'article Wikipédia suivant:

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

a le pseudo-code pour un algorithme, et une brève description d'un autre de Tarjan. Les deux ont une O(|V| + |E|)complexité temporelle.

Steve Jessop
la source
Un tri topologique peut détecter des cycles, dans la mesure où il repose sur un algorithme de recherche en profondeur, mais vous avez besoin d'une comptabilité supplémentaire pour détecter réellement les cycles. Voir la bonne réponse de Kurt Peek.
Luke Hutchison
33

La façon la plus simple de le faire est de faire une première traversée en profondeur (DFT) du graphique .

Si le graphique a des nsommets, il s'agit d'un O(n)algorithme de complexité temporelle. Puisque vous devrez éventuellement faire une DFT à partir de chaque sommet, la complexité totale devient O(n^2).

Vous devez conserver une pile contenant tous les sommets dans la première traversée de profondeur actuelle , son premier élément étant le nœud racine. Si vous rencontrez un élément qui est déjà dans la pile pendant la DFT, alors vous avez un cycle.

nbro
la source
21
Ce serait vrai pour un graphique « régulier », mais il est faux pour un dirigé graphique. Par exemple, considérons le "diagramme de dépendance en diamant" à quatre nœuds: A avec des bords pointant vers B et C, dont chacun a un bord pointant vers D. Votre traversée DFT de ce diagramme à partir de A conclurait à tort que la "boucle" était en fait un cycle - bien qu'il y ait une boucle, ce n'est pas un cycle car il ne peut pas être parcouru en suivant les flèches.
Peter
9
@peter pouvez-vous expliquer comment la DFT de A conclura à tort qu'il y a un cycle?
Deepak
10
@Deepak - En fait, j'ai mal lu la réponse de "l'assistant physique": où il a écrit "dans la pile" je pensais "a déjà été trouvé". Il suffirait en effet (pour détecter une boucle dirigée) de vérifier les dupes "dans la pile" lors de l'exécution d'un DFT. Un vote positif pour chacun de vous.
Peter
2
Pourquoi dites-vous que la complexité temporelle est O(n)alors que vous proposez de vérifier la pile pour voir si elle contient déjà un nœud visité? L'analyse de la pile ajoute du temps à l' O(n)exécution car elle doit analyser la pile sur chaque nouveau nœud. Vous pouvez y arriver en O(n)marquant les nœuds visités
James Wierzba
Comme l'a dit Peter, cela est incomplet pour les graphiques dirigés. Voir la bonne réponse de Kurt Peek.
Luke Hutchison
32

Selon le lemme 22.11 de Cormen et al., Introduction to Algorithms (CLRS):

Un graphe orienté G est acyclique si et seulement si une recherche en profondeur d'abord de G ne donne aucun bord arrière.

Cela a été mentionné dans plusieurs réponses; ici, je vais également fournir un exemple de code basé sur le chapitre 22 de CLRS. Le graphique d'exemple est illustré ci-dessous.

entrez la description de l'image ici

Le pseudo-code de CLRS pour la recherche en profondeur d'abord se lit comme suit:

entrez la description de l'image ici

Dans l'exemple de la figure 22.4 du CLRS, le graphique se compose de deux arbres DFS: l'un composé des nœuds u , v , x et y et l'autre des nœuds w et z . Chaque arbre contient un bord arrière: un de x à v et un autre de z à z (une auto-boucle).

La réalisation clé est qu'un bord arrière est rencontré lorsque, dans la DFS-VISITfonction, tout en itérant sur les voisins vde u, un nœud est rencontré avec la GRAYcouleur.

Le code Python suivant est une adaptation du pseudocode de CLRS avec une ifclause ajoutée qui détecte les cycles:

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)

Notez que dans cet exemple, le timepseudocode de CLRS n'est pas capturé car nous ne sommes intéressés que par la détection des cycles. Il existe également du code standard pour construire la représentation de liste d'adjacence d'un graphique à partir d'une liste d'arêtes.

Lorsque ce script est exécuté, il imprime la sortie suivante:

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

Ce sont exactement les bords arrière de l'exemple de la figure 22.4 du CLRS.

Kurt Peek
la source
29

Commencez avec un DFS: un cycle existe si et seulement si un back-edge est découvert pendant DFS . Ceci est prouvé à la suite du théorème du chemin blanc.

Ajay Garg
la source
3
Oui, je pense la même chose, mais cela ne suffit pas, je poste mon chemin cs.stackexchange.com/questions/7216/find-the-simple-cycles-in-a-directed-graph
jonaprieto
Vrai. Ajay Garg ne dit que comment trouver "un cycle", qui est une réponse partielle à cette question. Votre lien parle de trouver tous les cycles selon la question posée, mais encore une fois, il semble qu'il utilise la même approche que Ajay Garg, mais fait également tous les arbres DFS possibles.
Manohar Reddy Poreddy
Ceci est incomplet pour les graphiques dirigés. Voir la bonne réponse de Kurt Peek.
Luke Hutchison
26

À mon avis, l'algorithme le plus compréhensible pour détecter le cycle dans un graphe orienté est l'algorithme de coloration du graphe.

Fondamentalement, l'algorithme de coloration du graphique parcourt le graphique d'une manière DFS (Depth First Search, ce qui signifie qu'il explore complètement un chemin avant d'explorer un autre chemin). Lorsqu'il trouve un bord arrière, il marque le graphe comme contenant une boucle.

Pour une explication approfondie de l'algorithme de coloration des graphiques, veuillez lire cet article: http://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/

De plus, je fournis une implémentation de la coloration des graphes en JavaScript https://github.com/dexcodeinc/graph_algorithm.js/blob/master/graph_algorithm.js

Armin Primadi
la source
8

Si vous ne pouvez pas ajouter une propriété "visitée" aux nœuds, utilisez un ensemble (ou une carte) et ajoutez simplement tous les nœuds visités à l'ensemble à moins qu'ils ne soient déjà dans l'ensemble. Utilisez une clé unique ou l'adresse des objets comme "clé".

Cela vous donne également les informations sur le nœud "racine" de la dépendance cyclique qui seront utiles lorsqu'un utilisateur doit résoudre le problème.

Une autre solution consiste à essayer de trouver la prochaine dépendance à exécuter. Pour cela, vous devez avoir une pile où vous pouvez vous rappeler où vous êtes maintenant et ce que vous devez faire ensuite. Vérifiez si une dépendance existe déjà sur cette pile avant de l'exécuter. Si c'est le cas, vous avez trouvé un cycle.

Bien que cela puisse sembler avoir une complexité de O (N * M), vous devez vous rappeler que la pile a une profondeur très limitée (donc N est petit) et que M devient plus petit avec chaque dépendance que vous pouvez cocher comme "exécutée" plus vous pouvez arrêter la recherche lorsque vous avez trouvé une feuille (vous n'avez donc jamais à vérifier chaque nœud -> M sera petit aussi).

Dans MetaMake, j'ai créé le graphique sous forme de liste de listes, puis supprimé tous les nœuds au fur et à mesure que je les exécutais, ce qui a naturellement réduit le volume de recherche. En fait, je n'ai jamais eu à effectuer de vérification indépendante, tout s'est produit automatiquement lors de l'exécution normale.

Si vous avez besoin d'un mode "test uniquement", ajoutez simplement un indicateur "dry-run" qui désactive l'exécution des travaux réels.

Aaron Digulla
la source
7

Il n'y a pas d'algorithme qui puisse trouver tous les cycles dans un graphe orienté en temps polynomial. Supposons que le graphe dirigé ait n nœuds et que chaque paire de nœuds ait des connexions entre elles, ce qui signifie que vous avez un graphe complet. Donc, tout sous-ensemble non vide de ces n nœuds indique un cycle et il y a 2 ^ n-1 nombre de ces sous-ensembles. Il n'existe donc pas d'algorithme de temps polynomial. Supposons donc que vous ayez un algorithme efficace (non stupide) qui peut vous indiquer le nombre de cycles dirigés dans un graphique, vous pouvez d'abord trouver les composants connectés forts, puis appliquer votre algorithme sur ces composants connectés. Puisque les cycles n'existent qu'à l'intérieur des composants et non entre eux.

Yuwen
la source
1
Vrai, si le nombre de nœuds est pris comme la taille de l'entrée. Vous pouvez également décrire la complexité d'exécution en termes de nombre d'arêtes ou même de cycles, ou d'une combinaison de ces mesures. L'algorithme "Trouver tous les circuits élémentaires d'un graphe orienté" de Donald B. Johnson a un temps d'exécution polynomial donné par O ((n + e) ​​(c + 1)) où n est le nombre de nœuds, e le nombre d'arêtes et c le nombre de circuits élémentaires du graphe. Et voici mon implémentation Java de cet algorithme: github.com/1123/johnson .
user152468
4

J'avais implémenté ce problème en sml (programmation impérative). Voici le plan. Trouvez tous les nœuds qui ont un degré ou un degré de 0. Ces nœuds ne peuvent pas faire partie d'un cycle (supprimez-les donc). Supprimez ensuite tous les bords entrants ou sortants de ces nœuds. Appliquer récursivement ce processus au graphique résultant. Si à la fin vous ne vous retrouvez avec aucun nœud ou bord, le graphique n'a pas de cycles, sinon il en a.

Rpant
la source
2

La façon dont je le fais est de faire un tri topologique, en comptant le nombre de sommets visités. Si ce nombre est inférieur au nombre total de sommets dans le DAG, vous avez un cycle.

Steve
la source
4
Ça n'a pas de sens. Si le graphique comporte des cycles, il n'y a pas de tri topologique, ce qui signifie que tout algorithme correct pour le tri topologique sera abandonné.
sleske
4
from wikipedia: De nombreux algorithmes de tri topologique détectent également les cycles, car ce sont des obstacles à l'existence de l'ordre topologique.
Oleg Mikheev
1
@OlegMikheev Oui, mais Steve dit "Si ce nombre est inférieur au nombre total de sommets dans le DAG, vous avez un cycle", ce qui n'a pas de sens.
nbro
@nbro Je parie qu'ils signifient une variante de l'algorithme de tri topologique qui s'interrompt lorsqu'il n'existe aucun tri topologique (et qu'ils ne visitent pas tous les sommets).
maaartinus
Si vous faites un tri topologique sur un graphe avec cycle vous vous retrouverez avec une commande qui aura le moins de mauvais bords (numéro de commande> numéro de commande du voisin). Mais après le tri, il est facile de détecter ces mauvais bords, ce qui entraîne la détection d'un graphique avec un cycle
UGP
2

/mathpro/16393/finding-a-cycle-of-fixed-length J'aime cette solution la meilleure spécialement pour 4 longueurs :)

L'assistant physique dit également que vous devez faire O (V ^ 2). Je crois que nous n'avons besoin que de O (V) / O (V + E). Si le graphique est connecté, DFS visitera tous les nœuds. Si le graphique a des sous-graphiques connectés, chaque fois que nous exécuterons un DFS sur un sommet de ce sous-graphique, nous trouverons les sommets connectés et n'aurons pas à les considérer pour la prochaine exécution du DFS. Par conséquent, la possibilité de courir pour chaque sommet est incorrecte.

amitgoswami
la source
1

Si DFS trouve une arête qui pointe vers un sommet déjà visité, vous y avez un cycle.

mafonya
la source
1
Échoue sur 1,2,3: 1,2; 1,3; 2,3;
chat bruyant
4
@JakeGreene Regardez ici: i.imgur.com/tEkM5xy.png Assez simple à comprendre. Disons que vous partez de 0. Ensuite, vous allez au nœud 1, plus de chemins à partir de là, la reconnexion revient. Maintenant, vous visitez le nœud 2, qui a un bord au sommet 1, qui a déjà été visité. À votre avis, vous auriez alors un cycle - et vous n'en avez pas vraiment
chat bruyant
3
@kittyPL Ce graphique ne contient pas de cycle. Extrait de Wikipédia: "Un cycle dirigé dans un graphique dirigé est une séquence de sommets commençant et se terminant au même sommet de telle sorte que, pour chacun des deux sommets consécutifs du cycle, il existe une arête dirigée du sommet précédent vers le dernier" Vous doivent être en mesure de suivre un chemin de V qui mène à V pour un cycle dirigé. La solution de mafonya fonctionne pour le problème donné
Jake Greene
2
@JakeGreene Bien sûr que non. En utilisant votre algorithme et à partir de 1, vous détecteriez un cycle de toute façon ... Cet algorithme est tout simplement mauvais ... Habituellement, il suffirait de reculer chaque fois que vous rencontrez un sommet visité.
noisy cat
6
@kittyPL DFS fonctionne pour détecter les cycles à partir du nœud de départ donné. Mais lorsque vous faites DFS, vous devez colorer les nœuds visités pour distinguer un bord transversal d'un bord arrière. La première fois que vous visitez un sommet, il devient gris, puis vous le rendez noir une fois que tous ses bords ont été visités. Si, lors de la réalisation du DFS, vous atteignez un sommet gris, ce sommet est un ancêtre (c'est-à-dire que vous avez un cycle). Si le sommet est noir, ce n'est qu'un bord transversal.
Kyrra
0

Comme vous l'avez dit, vous avez défini des tâches, elles doivent être exécutées dans un certain ordre. Topological sortétant donné l'ordre requis pour la planification des travaux (ou pour les problèmes de dépendance s'il s'agit d'un direct acyclic graph). Exécutez dfset maintenez une liste, et commencez à ajouter un nœud au début de la liste, et si vous avez rencontré un nœud qui est déjà visité. Ensuite, vous avez trouvé un cycle dans un graphique donné.

Bhagwati Malav
la source
-11

Si un graphique satisfait cette propriété

|e| > |v| - 1

alors le graphique contient au moins sur le cycle.

dharmendra singh
la source
10
C'est peut-être vrai pour les graphiques non dirigés, mais certainement pas pour les graphiques dirigés.
Hans-Peter Störr
6
Un contre-exemple serait A-> B, B-> C, A-> C.
user152468
Tous les sommets n'ont pas d'arêtes.
Debanjan Dhar