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).
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)])) # FalseCyclus 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)])) # TrueGerichte 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)])) # FalseCursusplanning: 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 dependencyCyclusdetectie 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)])) # FalseDe 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.
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
- Graafrepresentaties en voorbereiding van traversals
- BFS: kortste pad en traversal per niveau
- DFS: verbonden componenten en flood fill
- Cycli detecteren in gerichte en ongerichte grafen