0Pricing
Coding Interview Prep · Leçon

Détection de cycles dans les graphes orientés et non orientés

Détectez les cycles dans les graphes non orientés en suivant les parents, et dans les graphes orientés avec le marquage des couleurs de DFS (trois états visités : blanc, gris et noir).

Détection de cycles dans les graphes orientés et non orientés est une leçon Coding Interview Prep gratuite sur CoddyKit. Ceci est la leçon 4 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage Coding Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours Coding Interview Prep comprend 4 leçons au total.

Pourquoi la détection des cycles est importante

Un cycle dans un graphe est un chemin qui commence et se termine au même nœud. La détection des cycles est essentielle dans de nombreux algorithmes : le tri topologique échoue sur les graphes cycliques, la résolution des dépendances doit détecter les dépendances circulaires, et la détection des interblocages dans l'ordonnancement de l'OS nécessite de trouver les cycles dans les graphes d'allocation des ressources. L'approche diffère entre les graphes non orientés et orientés : ils nécessitent fondamentalement des algorithmes différents.

from collections import defaultdict

# Undirected cycle: A-B-C-A (triangle)
undirected = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
    undirected[u].append(v)
    undirected[v].append(u)

# Directed cycle: A->B->C->A
directed = defaultdict(list)
for u, v in [('A','B'),('B','C'),('C','A')]:
    directed[u].append(v)  # one direction only

# Key difference:
# Undirected: edge A-B appears as both A->B and B->A
# Must track parent to distinguish cycle from back-edge to parent
print('Undirected and directed cycles need different detection')

Détection des cycles non orientés avec DFS

Dans un graphe non orienté, un cycle existe si DFS visite un nœud qui se trouve déjà dans le chemin actuel (et pas seulement parmi les nœuds visités). La difficulté vient du fait que chaque arête apparaît dans les deux directions : lorsque nous visitons un nœud enfant, sa liste de voisins contient notre nœud actuel (le parent). Nous devons suivre le parent de chaque nœud afin de ne pas signaler à tort l'arête qui revient vers le parent comme un cycle. Si nous rencontrons un nœud visité qui n'est pas notre parent, nous avons trouvé un cycle.

def has_cycle_undirected(n, edges):
    from collections import defaultdict
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()

    def dfs(node, parent):
        visited.add(node)
        for nb in graph[node]:
            if nb not in visited:
                if dfs(nb, node):  # recurse with current as parent
                    return True
            elif nb != parent:     # visited and not parent = CYCLE
                return True
        return False

    for node in range(n):
        if node not in visited:
            if dfs(node, -1):  # -1 = no parent for root
                return True
    return False

print(has_cycle_undirected(4, [(0,1),(1,2),(2,3),(3,1)]))  # True
print(has_cycle_undirected(3, [(0,1),(1,2)]))               # False

Cycle non orienté avec BFS

La détection des cycles par BFS dans un graphe non orienté suit également le parent de chaque nœud visité. Lors du traitement des voisins d'un nœud, si un voisin est déjà visité et n'est pas le parent du nœud actuel, un cycle existe. Utilisez un dictionnaire pour stocker les parents. Cette approche en O(V + E) évite le problème de limite de récursion et constitue l'alternative itérative privilégiée pour les grands graphes.

from collections import deque, defaultdict

def has_cycle_bfs_undirected(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)

    visited = set()

    for start in range(n):
        if start in visited:
            continue
        visited.add(start)
        parent = {start: -1}
        queue = deque([start])
        while queue:
            node = queue.popleft()
            for nb in graph[node]:
                if nb not in visited:
                    visited.add(nb)
                    parent[nb] = node
                    queue.append(nb)
                elif parent[node] != nb:  # visited and not parent = CYCLE
                    return True
    return False

print(has_cycle_bfs_undirected(4, [(0,1),(1,2),(2,0)]))  # True

Cycle orienté : pourquoi le suivi du parent échoue

Dans un graphe orienté, le suivi du parent ne suffit pas. Considérez A→C et B→C : le nœud C a deux « parents », mais aucun cycle. L'approche correcte utilise une coloration à trois états : blanc (non visité), gris (dans le chemin ou la pile DFS actuelle), noir (entièrement traité). Un cycle existe si nous rencontrons un nœud gris pendant DFS : cela signifie que nous avons trouvé une arête de retour vers un ancêtre du chemin actuel.

# Three-state DFS coloring:
# WHITE (0): not yet visited
# GRAY  (1): currently being visited (in DFS stack)
# BLACK (2): fully visited (all descendants processed)

# Why parent fails for directed graphs:
# A -> C  (no cycle)
# B -> C  (no cycle)
# If we DFS from A, mark C gray
# Then DFS from B finds C is gray -- but this is NOT a cycle!
# C is gray from A's path, not B's path.
# Parent tracking only works when the back-edge goes to the IMMEDIATE parent.
print('Directed graph: use 3-state coloring (white/gray/black)')

Détection des cycles orientés avec un DFS à trois états

Utilisez un tableau state[] dont les valeurs sont 0 (blanc/non visité), 1 (gris/dans la pile) et 2 (noir/terminé). Démarrez le DFS, en marquant le nœud en gris à son entrée et en noir à sa sortie. Si DFS atteint un nœud gris, une arête de retour est détectée : il existe donc un cycle. S'il atteint un nœud noir, ce chemin a déjà été entièrement exploré et ne contient aucun cycle ; ignorez-le.

def has_cycle_directed(n, edges):
    from collections import defaultdict
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)

    state = [0] * n  # 0=white, 1=gray, 2=black

    def dfs(node):
        state[node] = 1  # mark gray (in stack)
        for nb in graph[node]:
            if state[nb] == 1:  # gray = back edge = CYCLE
                return True
            if state[nb] == 0:  # white = unvisited
                if dfs(nb):
                    return True
        state[node] = 2  # mark black (fully processed)
        return False

    for node in range(n):
        if state[node] == 0:
            if dfs(node):
                return True
    return False

print(has_cycle_directed(4, [(0,1),(1,2),(2,0),(2,3)]))  # True (0->1->2->0)
print(has_cycle_directed(3, [(0,1),(1,2)]))               # False

Ordonnancement des cours : cycle dans un DAG

Ordonnancement des cours (LeetCode n° 207) demande si tous les cours peuvent être terminés compte tenu des prérequis. Modélisez les cours comme des nœuds et les prérequis comme des arêtes orientées. Tous les cours peuvent être terminés si et seulement si le graphe est un DAG (sans cycles). Utilisez la détection des cycles par DFS à trois états : si un cycle est trouvé, renvoyez Faux ; sinon, renvoyez Vrai.

from collections import defaultdict

def can_finish(num_courses, prerequisites):
    graph = defaultdict(list)
    for a, b in prerequisites:
        graph[b].append(a)  # b is prerequisite for a: b -> a

    state = [0] * num_courses

    def dfs(course):
        if state[course] == 1: return False  # cycle!
        if state[course] == 2: return True   # already verified
        state[course] = 1  # mark as in-progress
        for next_course in graph[course]:
            if not dfs(next_course):
                return False
        state[course] = 2  # mark as done
        return True

    return all(dfs(i) for i in range(num_courses) if state[i] == 0)

print(can_finish(2, [[1,0]]))        # True: take 0 then 1
print(can_finish(2, [[1,0],[0,1]]))  # False: circular dependency

Détection des cycles avec l'algorithme de Kahn (BFS)

Une autre méthode de détection des cycles dans les graphes orientés utilise le tri topologique par BFS de Kahn. Comptez les degrés entrants de tous les nœuds. Placez dans une file les nœuds dont le degré entrant est nul. Traitez chaque nœud : décrémentez le degré entrant de ses voisins et mettez en file ceux dont le degré atteint 0. Si le nombre de nœuds traités est égal à V, il n'y a aucun cycle ; sinon, un cycle existe (les nœuds non traités forment des cycles). Cette approche en O(V + E) est intuitive et plus facile à retenir que le DFS à trois états.

from collections import defaultdict, deque

def has_cycle_kahn(n, edges):
    graph = defaultdict(list)
    in_degree = [0] * n
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1

    # Start with all zero in-degree nodes
    queue = deque(i for i in range(n) if in_degree[i] == 0)
    processed = 0
    while queue:
        node = queue.popleft()
        processed += 1
        for nb in graph[node]:
            in_degree[nb] -= 1
            if in_degree[nb] == 0:
                queue.append(nb)

    return processed != n  # if not all processed, cycle exists

print(has_cycle_kahn(4, [(0,1),(1,2),(2,0),(2,3)]))  # True
print(has_cycle_kahn(3, [(0,1),(1,2)]))               # False

Trouver le cycle : collecte des nœuds du cycle

Il faut parfois identifier les nœuds qui font partie d'un cycle, et pas seulement détecter son existence. Pendant un DFS à trois états, lorsqu'une arête de retour est trouvée, remontez la pile d'appels (ou une pile de chemin) pour collecter tous les nœuds situés entre l'ancêtre et le nœud actuel. Une pile de chemin maintenue parallèlement au tableau des états mémorise le chemin DFS actuel et permet de reconstruire le cycle en O de la longueur du cycle.

def find_cycle_nodes(n, edges):
    from collections import defaultdict
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)

    state = [0] * n
    path = []  # current DFS path
    cycle = []

    def dfs(node):
        state[node] = 1
        path.append(node)
        for nb in graph[node]:
            if state[nb] == 1:  # back edge -> found cycle
                start = path.index(nb)
                cycle.extend(path[start:])
                return True
            if state[nb] == 0 and dfs(nb):
                return True
        path.pop()
        state[node] = 2
        return False

    for i in range(n):
        if state[i] == 0 and dfs(i):
            break
    return cycle

print(find_cycle_nodes(4, [(0,1),(1,2),(2,0),(2,3)]))  # [0, 1, 2]

Trouver les états sûrs à terme

Trouver les états sûrs à terme (LeetCode n° 802) demande quels nœuds mènent finalement à un nœud terminal (sans arête sortante) sans rester bloqués dans un cycle. Un nœud est « sûr » si tous les chemins qui partent de lui mènent à des nœuds terminaux. Utilisez un DFS à trois états : les nœuds noirs (entièrement traités sans détection de cycle) sont sûrs. Les nœuds qui font partie d'un cycle ou qui y mènent ne sont pas sûrs.

def eventual_safe_nodes(graph):
    n = len(graph)
    state = [0] * n  # 0=unvisited, 1=visiting, 2=safe

    def dfs(node):
        if state[node] == 1:  # currently visiting = cycle
            return False
        if state[node] == 2:  # already verified safe
            return True
        state[node] = 1  # mark as visiting
        for nb in graph[node]:
            if not dfs(nb):
                return False  # leads to cycle, not safe
        state[node] = 2  # mark as safe
        return True

    return [i for i in range(n) if dfs(i)]

# [[1,2],[2,3],[5],[0],[5],[],[]] means:
# 0->[1,2], 1->[2,3], 2->[5], 3->[0] (cycle!), 4->[5], 5->[], 6->[]
print(eventual_safe_nodes([[1,2],[2,3],[5],[0],[5],[],[]]))
# [2, 4, 5, 6]

Connexion redondante dans un graphe non orienté

Connexion redondante (LeetCode n° 684) trouve l'arête qui crée un cycle lorsqu'elle est ajoutée à un graphe non orienté autrement acyclique. Bien que ce problème puisse être résolu avec la détection des cycles par DFS, la solution la plus claire utilise une structure de fusion-recherche (DSU) : traitez les arêtes une par une ; si les deux extrémités sont déjà reliées (dans la même composante), l'arête actuelle crée un cycle et constitue la réponse. DSU offre une complexité de O(alpha(n)) par opération, soit effectivement O(1).

def find_redundant_connection(edges):
    n = len(edges)
    parent = list(range(n + 1))
    rank = [0] * (n + 1)

    def find(x):
        if parent[x] != x:
            parent[x] = find(parent[x])  # path compression
        return parent[x]

    def union(x, y):
        px, py = find(x), find(y)
        if px == py:
            return False  # already connected = cycle!
        if rank[px] < rank[py]: px, py = py, px
        parent[py] = px
        if rank[px] == rank[py]: rank[px] += 1
        return True

    for u, v in edges:
        if not union(u, v):
            return [u, v]  # this edge creates the cycle
    return []

print(find_redundant_connection([[1,2],[1,3],[2,3]]))  # [2,3]
print(find_redundant_connection([[1,2],[2,3],[3,4],[1,4],[1,5]]))  # [1,4]

Résumé : stratégies de détection des cycles

Pour résumer la boîte à outils de détection des cycles : pour les graphes non orientés, utilisez DFS avec suivi du parent ou une structure de fusion-recherche. Pour les graphes orientés, utilisez un DFS à trois états (blanc/gris/noir) ou le tri topologique par BFS de Kahn. Choisissez la structure de fusion-recherche lorsque vous ajoutez des arêtes une par une (en ligne). Choisissez Kahn lorsque vous avez également besoin de l'ordre topologique. Choisissez le DFS à trois états lorsque vous devez identifier les nœuds précis du cycle. Dans un entretien, précisez toujours la distinction entre graphes orientés et non orientés lorsque vous parlez de détection des cycles.

# Cycle detection summary:
# Graph type  | Algorithm            | Complexity
# ------------|----------------------|-----------
# Undirected  | DFS + parent track   | O(V + E)
# Undirected  | Union-Find (DSU)     | O(E * alpha(V))
# Directed    | DFS 3-state (W/G/B)  | O(V + E)
# Directed    | Kahn's BFS topo sort | O(V + E)

# When to choose:
# Online (edges added one at a time): Union-Find
# Need topological order too: Kahn's BFS
# Need cycle nodes identified: 3-state DFS with path stack
# Simple existence check: any of the above
print('Always clarify directed vs undirected before coding')

Vérification rapide

Vérifiez votre compréhension des concepts de structures de données & algorithmes — préparation aux entretiens de programmation de cette leçon.

Récapitulatif de la leçon

Dans cette leçon, vous avez appris : la détection des cycles non orientés avec DFS et suivi du parent, la détection des cycles orientés avec une coloration à trois états blanc/gris/noir, l'alternative BFS de Kahn pour les graphes orientés, ainsi que des applications comme l'ordonnancement des cours, la connexion redondante et les états sûrs à terme. Nous abordons ensuite les bases de la programmation dynamique.

Questions Fréquemment Posées

La leçon « Détection de cycles dans les graphes orientés et non orientés » est-elle gratuite ?

Oui — le texte complet de « Détection de cycles dans les graphes orientés et non orientés » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours Coding Interview Prep, passe à CoddyKit PRO. Le cours Coding Interview Prep comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Détection de cycles dans les graphes orientés et non orientés » ?

Détectez les cycles dans les graphes non orientés en suivant les parents, et dans les graphes orientés avec le marquage des couleurs de DFS (trois états visités : blanc, gris et noir). Tu pratiques Coding Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer Coding Interview Prep ?

Aucune expérience préalable n'est requise. Coding Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 4 sur 4.

Combien de temps prend la leçon « Détection de cycles dans les graphes orientés et non orientés » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon Coding Interview Prep ?

Oui. Chaque leçon Coding Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Représentations des graphes et préparation des parcours
  2. BFS : plus court chemin et parcours par niveaux
  3. DFS : composantes connexes et remplissage
  4. Détection de cycles dans les graphes orientés et non orientés
← Retour à Coding Interview Prep