420 votes

Quelles détections de cycle au sein d'un graphe dirigé sont plus efficaces que O(n^2) ?

Existe-t-il un algorithme plus efficace en termes de temps que O(n^2) pour détecter les cycles au sein d'un graphe orienté?

J'ai un graphe orienté représentant un emploi du temps des tâches à exécuter, une tâche étant un noeud et une dépendance étant une arête. Je dois détecter le cas d'erreur d'un cycle au sein de ce graphe entraînant des dépendances cycliques.

16 votes

Vous dites que vous voulez détecter tous les cycles, mais votre cas d'utilisation suggère qu'il serait suffisant de détecter s'il y a des cycles.

35 votes

Il serait préférable de détecter tous les cycles afin de les corriger en une seule fois, plutôt que de vérifier, corriger, vérifier, corriger, etc.

2 votes

Vous devriez lire l'article "Finding all the elementary circuits of a directed graph" de Donald B. Johnson. Il trouvera uniquement les circuits élémentaires, mais cela devrait être suffisant pour votre cas. Et voici mon implémentation en Java de cet algorithme prête à être utilisée : github.com/1123/johnson

203voto

aku Points 54867

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

Pour d'autres algorithmes, voir Composantes fortement connexes sur Wikipedia.

0 votes

Merci Aku, cela devrait fonctionner et me permettra également d'afficher les sous-graphiques causant le problème.

78 votes

Comment les composantes fortement connexes permettent-elles de comprendre les cycles qui existent dans le graphe ?

4 votes

Peut-être que quelqu'un pourrait 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.

81voto

Steve Jessop Points 166970

Étant donné qu'il s'agit d'un planning de tâches, je soupçonne qu'à un moment donné vous allez les triez dans un ordre d'exécution proposé.

Si c'est le cas, alors une implémentation de tri topologique pourrait détecter des cycles. UNIX tsort le fait certainement. Je pense qu'il est donc probablement plus efficace de détecter les cycles en même temps que le tri topologique, plutôt que lors d'une étape séparée.

Donc la question pourrait être, "comment puis-je trier de la manière la plus efficace", plutôt que "comment détecter les boucles de la manière la plus efficace". La réponse est probablement "utiliser une bibliothèque", mais en cas d'échec l'article Wikipedia suivant:

http://fr.wikipedia.org/wiki/Tri_topologique

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

0 votes

Un tri topologique peut détecter les cycles, dans la mesure où il repose sur un algorithme de recherche en profondeur, mais vous avez besoin d'une gestion supplémentaire pour détecter effectivement les cycles. Voir la réponse correcte de Kurt Peek.

50voto

Kurt Peek Points 8050

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

Un graphe dirigé G est acyclique si et seulement si une recherche en profondeur de G ne produit pas de arêtes de retour.

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 graphe exemple est illustré ci-dessous.

enter image description here

Le pseudo-code de CLRS pour la recherche en profondeur est le suivant :

enter image description here

Dans l'exemple de la Figure 22.4 de CLRS, le graphe 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 une arête de retour : une de x à v et une autre de z à z (une boucle locale).

La clé de la réalisation est qu'une arête de retour est rencontrée lorsque, dans la fonction DFS-VISIT, en itérant sur les voisins v de u, un nœud avec la couleur GRAY est rencontré.

Le code Python suivant est une adaptation du pseudo-code de CLRS avec une clause if 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])
            adj[edge[1]] # effets secondaires uniquement
        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]:
        # Détecter les cycles
        if v in discovered:
            print(f"Cycle détecté : une arête de retour trouvée de {u} à {v}.")
            break

        # Récursivité dans l'arbre DFS
        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)

Remarquez que dans cet exemple, le time dans le pseudo-code de CLRS n'est pas capturé car nous nous intéressons uniquement à la détection des cycles. Il y a aussi un certain code de boilerplate pour construire la représentation de liste d'adjacence d'un graphe à partir d'une liste d'arêtes.

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

Cycle détecté : une arête de retour trouvée de x à v.
Cycle détecté : une arête de retour trouvée de z à z.

Il s'agit exactement des arêtes de retour dans l'exemple de la Figure 22.4 de CLRS.

0 votes

Je reçois RecursionError: maximum recursion depth exceeded while calling a Python object pour ce code.

1 votes

@zino Il semble qu'il devrait y avoir un break après que le cycle soit détecté. J'ai essayé de l'ajouter mais la file d'attente de modification est pleine.

0 votes

Nit: découvert, fini = dfs_visit(G, u, découvert, fini) can be replaced with: dfs_visit(G, u, découvert, fini) and dfs-visit can return Aucun

34voto

La manière la plus simple de le faire est de faire un parcours en profondeur (DFT) du graphe.

Si le graphe a n sommets, cela correspond à un algorithme de complexité temporelle O(n). Comme vous devrez potentiellement faire un DFT en partant de chaque sommet, la complexité totale devient O(n^2).

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

26 votes

Cela serait vrai pour un graphe "normal", mais est faux pour un graphe orienté. Par exemple, considérez le "diagramme de dépendance en forme de diamant" avec quatre nœuds : A avec des arêtes pointant vers B et C, chacun ayant une arête pointant vers D. Votre traversée DFT de ce diagramme de A conclurait incorrectement que la "boucle" était en réalité un cycle - bien qu'il y ait une boucle, ce n'est pas un cycle car elle ne peut pas être traversée en suivant les flèches.

12 votes

@peter pouvez-vous s'il vous plaît expliquer comment DFT à partir de A conclura incorrectement qu'il y a un cycle?

12 votes

@Deepak - En fait, j'ai mal lu la réponse du "phys wizard" : là où il a écrit "dans la pile" j'ai pensé "a déjà été trouvé". Il serait en effet suffisant (pour détecter une boucle dirigée) de vérifier les doublons "dans la pile" lors de l'exécution d'un DFT. Un vote positif pour chacun de vous.

31voto

Armin Primadi Points 334

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

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

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

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

0 votes

100% d'accord. Je viens de passer beaucoup trop de temps sur un problème impliquant cela. La colorisation est le seul algorithme qui ait un sens, et donc le seul que je peux implémenter avec succès. Je vais certainement le mémoriser, ou du moins le fait qu'il existe.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X