Voorbereiding op programmeerinterviews · Les

Cycli detecteren in gerichte en ongerichte grafen

Detecteer cycli in ongerichte grafen met het bijhouden van ouders en in gerichte grafen met DFS-kleurcodering (de drie toestanden wit/grijs/zwart voor bezochte nodes).

Les 4 van 413 stappen

Cycli detecteren in gerichte en ongerichte grafen is een gratis Voorbereiding op programmeerinterviews-les op CoddyKit. Dit is les 4 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Voorbereiding op programmeerinterviews. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Voorbereiding op programmeerinterviews bevat in totaal 4 lessen.

Waarom cyclusdetectie belangrijk is

Een cyclus in een graaf is een pad dat bij hetzelfde knooppunt begint en eindigt. Cyclusdetectie is cruciaal in veel algoritmen: topologische sortering werkt niet op grafen met cycli, bij het oplossen van afhankelijkheden moet je circulaire afhankelijkheden detecteren en voor deadlockdetectie in de planning van besturingssystemen moet je cycli vinden in grafen voor resourcetoewijzing. De aanpak verschilt voor ongerichte en gerichte grafen — daarvoor zijn fundamenteel verschillende algoritmen nodig.

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')

Cyclusdetectie in ongerichte grafen met DFS

In een ongerichte graaf bestaat een cyclus als DFS een knooppunt bezoekt dat al in het huidige pad staat (en niet alleen bezocht is). De uitdaging is dat elke verbinding in beide richtingen voorkomt. Wanneer we een kindknooppunt bezoeken, bevat de lijst met buren dus ook ons huidige knooppunt (de ouder). We moeten de ouder van elk knooppunt bijhouden om te voorkomen dat de verbinding terug naar de ouder ten onrechte als cyclus wordt gemarkeerd. Als we een bezocht knooppunt tegenkomen dat niet de ouder is, hebben we een cyclus gevonden.

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

Cyclus in een ongerichte graaf met BFS

Bij cyclusdetectie met BFS in een ongerichte graaf houd je ook de ouder van elk bezocht knooppunt bij. Als je de buren van een knooppunt verwerkt en een buur al bezocht is maar niet de ouder van het huidige knooppunt, bestaat er een cyclus. Gebruik een woordenboek om ouders op te slaan. Deze aanpak met O(V + E) vermijdt problemen met de recursielimiet en is het aanbevolen iteratieve alternatief voor grote grafen.

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

Gerichte cyclus: waarom het bijhouden van ouders niet werkt

In een gerichte graaf is het bijhouden van ouders onvoldoende. Neem A→C en B→C: knooppunt C heeft twee 'ouders', maar geen cyclus. De juiste aanpak gebruikt kleuring met drie toestanden: wit (niet bezocht), grijs (in het huidige DFS-pad of op de stapel) en zwart (volledig verwerkt). Er bestaat een cyclus als we tijdens DFS ooit een grijs knooppunt tegenkomen — dat betekent dat we een terugverbinding naar een voorouder in het huidige pad hebben gevonden.

# 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)')

Cyclusdetectie in gerichte grafen met DFS met drie toestanden

Gebruik een array state[] met de waarden 0 (wit/niet bezocht), 1 (grijs/op de stapel) en 2 (zwart/klaar). Start DFS en markeer het knooppunt bij binnenkomst als grijs en bij vertrek als zwart. Als DFS ooit een grijs knooppunt bereikt, is er een terugverbinding gevonden — er bestaat dus een cyclus. Als DFS een zwart knooppunt bereikt, is dat pad al volledig onderzocht en cyclusvrij, dus sla je het over.

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

Cursusplanning: cyclus in een DAG

Cursusplanning (LeetCode #207) vraagt of alle cursussen kunnen worden afgerond als je de vereisten kent. Modelleer cursussen als knooppunten en vereisten als gerichte verbindingen. Alle cursussen kunnen worden afgerond dan en slechts dan als de graaf een DAG is (zonder cycli). Gebruik cyclusdetectie met DFS met drie toestanden — geef False terug als er een cyclus wordt gevonden en anders True.

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

Cyclusdetectie met het algoritme van Kahn (BFS)

Een alternatief voor cyclusdetectie in gerichte grafen is topologische sortering met BFS volgens Kahn. Tel de inkomende graden van alle knooppunten. Plaats knooppunten met inkomende graad 0 in een wachtrij. Verwerk elk knooppunt: verlaag de inkomende graad van de buren en plaats buren die 0 bereiken in de wachtrij. Als het aantal verwerkte knooppunten gelijk is aan V, is er geen cyclus; anders bestaat er wel een cyclus (de niet-verwerkte knooppunten vormen cycli). Deze aanpak met O(V + E) is intuïtief en gemakkelijker te onthouden dan DFS met drie toestanden.

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

De cyclus vinden: cyclusknooppunten verzamelen

Soms moet je bepalen welke knooppunten deel uitmaken van een cyclus, en niet alleen vaststellen of er een bestaat. Wanneer je tijdens DFS met drie toestanden een terugverbinding vindt, loop je via de aanroepstapel (of een padstapel) terug om alle knooppunten tussen de voorouder en het huidige knooppunt te verzamelen. Met een padstapel die je naast de toestandsarray bijhoudt, leg je het huidige DFS-pad vast en kun je de cyclus reconstrueren in O(cycle_length).

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]

Uiteindelijk veilige toestanden vinden

Uiteindelijk veilige toestanden vinden (LeetCode #802) vraagt welke knooppunten uiteindelijk naar een eindknooppunt leiden (zonder uitgaande verbindingen) zonder in een cyclus vast te lopen. Een knooppunt is 'veilig' als alle paden vanaf dat knooppunt naar eindknooppunten leiden. Gebruik DFS met drie toestanden: zwarte knooppunten (volledig verwerkt zonder dat een cyclus is gevonden) zijn veilig. Knooppunten die deel uitmaken van een cyclus of naar een cyclus leiden, zijn niet veilig.

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]

Overbodige verbinding in een ongerichte graaf

Overbodige verbinding (LeetCode #684) vindt de verbinding die een cyclus veroorzaakt wanneer deze wordt toegevoegd aan een verder acyclische ongerichte graaf. Hoewel je dit met cyclusdetectie via DFS kunt oplossen, gebruikt de eenvoudigste oplossing Union-Find (DSU): verwerk de verbindingen één voor één. Als beide eindpunten al met elkaar verbonden zijn (dezelfde component), veroorzaakt de huidige verbinding een cyclus en is dit het antwoord. DSU geeft O(alpha(n)) per bewerking — praktisch 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]

Samenvatting: strategieën voor cyclusdetectie

Samengevat bestaat de gereedschapskist voor cyclusdetectie uit het volgende: gebruik voor ongerichte grafen DFS met het bijhouden van ouders of Union-Find. Gebruik voor gerichte grafen DFS met drie toestanden (wit/grijs/zwart) of topologische sortering met BFS volgens Kahn. Kies Union-Find wanneer je verbindingen één voor één toevoegt (online). Kies Kahn wanneer je ook de topologische volgorde nodig hebt. Kies DFS met drie toestanden wanneer je de specifieke cyclusknooppunten moet identificeren. Benoem bij cyclusdetectie in technische sollicitatiegesprekken altijd het onderscheid tussen gerichte en ongerichte grafen.

# 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')

Korte toets

Toets je begrip van de concepten uit Data Structures & Algorithms — Coding Interview Prep uit deze les.

Samenvatting van de les

In deze les heb je geleerd hoe je cycli in ongerichte grafen detecteert met DFS en het bijhouden van ouders, hoe je cycli in gerichte grafen detecteert met kleuring met drie toestanden (wit/grijs/zwart), hoe je BFS volgens Kahn als alternatief gebruikt voor gerichte grafen en hoe je dit toepast op cursusplanning, overbodige verbindingen en uiteindelijk veilige toestanden. Hierna duiken we in de basis van dynamisch programmeren.

Gratis beginnen

Leer Voorbereiding op programmeerinterviews met een AI-tutor — gratis

Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.

Cursussen
90
Lessen
360

Veelgestelde vragen

Is de les “Cycli detecteren in gerichte en ongerichte grafen” gratis?

Ja — de volledige tekst van “Cycli detecteren in gerichte en ongerichte grafen” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Voorbereiding op programmeerinterviews wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Voorbereiding op programmeerinterviews bevat in totaal 4 lessen.

Wat leer ik in “Cycli detecteren in gerichte en ongerichte grafen”?

Detecteer cycli in ongerichte grafen met het bijhouden van ouders en in gerichte grafen met DFS-kleurcodering (de drie toestanden wit/grijs/zwart voor bezochte nodes). Je oefent met Voorbereiding op programmeerinterviews door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.

Heb ik ervaring nodig om met Voorbereiding op programmeerinterviews te beginnen?

Ervaring vooraf is niet nodig. Voorbereiding op programmeerinterviews op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 4 van 4.

Hoe lang duurt de les “Cycli detecteren in gerichte en ongerichte grafen”?

De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.

Kan ik code schrijven en uitvoeren in deze les over Voorbereiding op programmeerinterviews?

Ja. Elke les over Voorbereiding op programmeerinterviews bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.

Alle lessen in deze cursus

  1. Graafrepresentaties en voorbereiding van traversals
  2. BFS: kortste pad en traversal per niveau
  3. DFS: verbonden componenten en flood fill
  4. Cycli detecteren in gerichte en ongerichte grafen
← Terug naar Voorbereiding op programmeerinterviews