Cyklusdetektion i rettede og ikke-rettede grafer
Find cyklusser i ikke-rettede grafer med sporing af forældre og i rettede grafer med DFS-farvekodning (hvid/grå/sort tretilstandsregistrering af besøgte noder).
Cyklusdetektion i rettede og ikke-rettede grafer er en gratis DSA Interview Prep-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse alle 3 lektioner i dette læringsspor gratis i deres fulde længde — derefter låser CoddyKit PRO alle lektioner op samt praktiske øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. Den er en del af læringsforløbet i DSA Interview Prep, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. DSA Interview Prep-kurset indeholder 4 lektioner i alt.
Hvorfor cyklusdetektion er vigtig
En cyklus i en graf er en sti, der begynder og slutter ved den samme knude. Cyklusdetektion er afgørende i mange algoritmer: Topologisk sortering mislykkes på grafer med cyklusser, løsning af afhængigheder skal opdage cirkulære afhængigheder, og detektion af deadlocks i operativsystemers planlægning kræver, at man finder cyklusser i grafer over ressourcetildeling. Tilgangen er forskellig for urettede og rettede grafer — de kræver grundlæggende forskellige algoritmer.
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')Cyklusdetektion i urettede grafer med DFS
I en urettet graf findes der en cyklus, hvis DFS besøger en knude, der allerede er i den aktuelle sti (ikke blot er besøgt). Udfordringen er, at hver kant optræder i begge retninger, så en underknudes naboliste indeholder den aktuelle knude (forælderen). Du skal registrere hver knudes forælder for at undgå fejlagtigt at markere kanten tilbage til forælderen som en cyklus. Hvis du møder en besøgt knude, der ikke er din forælder, har du fundet en cyklus.
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)])) # FalseCyklus i urettet graf med BFS
Cyklusdetektion med BFS i en urettet graf registrerer også forælderen for hver besøgt knude. Når du behandler en knudes naboer, findes der en cyklus, hvis en nabo allerede er besøgt og ikke er den aktuelle knudes forælder. Brug en ordbog til at gemme forældre. Denne tilgang med O(V + E) undgår problemet med rekursionsgrænsen og er det foretrukne iterative alternativ til store grafer.
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)])) # TrueCyklus i rettet graf: Hvorfor registrering af forældre ikke virker
I en rettet graf er registrering af forældre ikke tilstrækkeligt. Betragt A→C og B→C: Knuden C har to »forældre«, men ingen cyklus. Den korrekte tilgang bruger farvelægning med tre tilstande: hvid (ubesøgt), grå (i den aktuelle DFS-sti/stak) og sort (fuldstændigt behandlet). Der findes en cyklus, hvis du under DFS møder en grå knude — det betyder, at du har fundet en bagudkant til en forfader i den aktuelle sti.
# 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)')Cyklusdetektion i rettede grafer med DFS i tre tilstande
Brug et array state[] med værdierne 0 (hvid/ubesøgt), 1 (grå/i stakken) og 2 (sort/færdig). Start DFS, og markér knuden som grå ved indgangen og sort ved afslutningen. Hvis DFS når en grå knude, er der fundet en bagudkant — der findes en cyklus. Hvis den når en sort knude, er stien allerede udforsket fuldstændigt og uden cyklus, så spring den 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)])) # FalseKursusplan: Cyklus i en DAG
Kursusplan (LeetCode #207) spørger, om alle kurser kan gennemføres ud fra givne forudsætninger. Modellér kurser som knuder og forudsætninger som rettede kanter. Alle kurser kan gennemføres, hvis og kun hvis grafen er en DAG (uden cyklusser). Brug cyklusdetektion med DFS i tre tilstande — returner False, hvis der findes en cyklus, og ellers returner 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 dependencyCyklusdetektion med Kahns algoritme (BFS)
En alternativ metode til cyklusdetektion i rettede grafer bruger Kahns topologiske BFS-sortering. Tæl alle knuders indgrader. Læg knuder med indgrad 0 i en kø. Behandl hver knude: Formindsk naboernes indgrader, og læg dem, der når 0, i køen. Hvis antallet af behandlede knuder er lig med V, findes der ingen cyklus; ellers findes der en cyklus (de ubehandlede knuder indgår i cyklusser). Denne tilgang med O(V + E) er intuitiv og lettere at huske end DFS i tre tilstande.
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)])) # FalseFind cyklussen: Indsaml cyklusknuder
Nogle gange skal du identificere, hvilke knuder der indgår i en cyklus, i stedet for blot at detektere, om den findes. Under DFS i tre tilstande skal du, når der findes en bagudkant, gå tilbage gennem kaldestakken (eller en stak over stien) for at indsamle alle knuder mellem forfaderen og den aktuelle knude. En stak over stien, der vedligeholdes sammen med tilstandsarrayet, indeholder den aktuelle DFS-sti og gør det muligt at rekonstruere cyklussen i 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]Find endeligt sikre tilstande
Find endeligt sikre tilstande (LeetCode #802) spørger, hvilke knuder der på et tidspunkt fører til en terminal knude (ingen udgående kanter) uden at sidde fast i en cyklus. En knude er »sikker«, hvis alle stier fra den fører til terminale knuder. Brug DFS i tre tilstande: Sorte knuder (fuldstændigt behandlet uden fundet cyklus) er sikre. Knuder, der indgår i eller fører til en cyklus, er ikke sikre.
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]Overflødig forbindelse i en urettet graf
Overflødig forbindelse (LeetCode #684) finder den kant, der skaber en cyklus, når den føjes til en ellers cyklusfri urettet graf. Det kan løses med cyklusdetektion via DFS, men den enkleste løsning bruger Union-Find (DSU): Behandl kanterne én ad gangen. Hvis begge endepunkter allerede er forbundet (i samme komponent), skaber den aktuelle kant en cyklus og er svaret. DSU giver O(alpha(n)) pr. operation — i praksis 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]Opsummering: Strategier til cyklusdetektion
For at opsummere værktøjerne til cyklusdetektion: Brug for urettede grafer DFS med registrering af forældre eller Union-Find. Brug for rettede grafer DFS i tre tilstande (hvid/grå/sort) eller Kahns topologiske BFS-sortering. Vælg Union-Find, når du tilføjer kanter én ad gangen (online). Vælg Kahn, når du også har brug for den topologiske rækkefølge. Vælg DFS i tre tilstande, når du skal identificere de konkrete cyklusknuder. Angiv altid forskellen mellem rettede og urettede grafer, når du taler om cyklusdetektion til jobsamtaler.
# 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')Hurtig test
Test din forståelse af begreberne fra Data Structures & Algorithms — Coding Interview Prep i denne lektion.
Opsummering af lektionen
I denne lektion lærte du om cyklusdetektion i urettede grafer med DFS, der registrerer forældre, cyklusdetektion i rettede grafer med farvelægning i tre tilstande — hvid/grå/sort — Kahns BFS-alternativ til rettede grafer samt anvendelser som kursusplan, overflødig forbindelse og endeligt sikre tilstande. Næste emne er grundlaget for dynamisk programmering.
Lær Python med en AI-underviser — gratis
Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.
- Kurser
- 30
- Lektioner
- 120
Ofte stillede spørgsmål
Er lektionen “Cyklusdetektion i rettede og ikke-rettede grafer” gratis?
Ja — alle 3 lektioner i læringssporet DSA Interview Prep, inklusive “Cyklusdetektion i rettede og ikke-rettede grafer”, kan læses gratis i deres fulde længde her på webstedet. Derefter låser CoddyKit PRO alle lektioner op samt interaktive øvelser med en indbygget kodeeditor og en AI-underviser døgnet rundt. DSA Interview Prep-kurset indeholder 4 lektioner i alt.
Hvad lærer jeg i “Cyklusdetektion i rettede og ikke-rettede grafer”?
Find cyklusser i ikke-rettede grafer med sporing af forældre og i rettede grafer med DFS-farvekodning (hvid/grå/sort tretilstandsregistrering af besøgte noder). Du øver dig i DSA Interview Prep med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.
Skal jeg have erfaring for at begynde på DSA Interview Prep?
Der kræves ingen tidligere erfaring. DSA Interview Prep på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.
Hvor lang tid tager lektionen “Cyklusdetektion i rettede og ikke-rettede grafer”?
De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.
Kan jeg skrive og køre kode i denne DSA Interview Prep-lektion?
Ja. Alle DSA Interview Prep-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.
Alle lektioner i dette kursus
- Grafrepræsentationer og opsætning af gennemløb
- BFS: korteste sti og niveaugennemløb
- DFS: sammenhængende komponenter og flood fill
- Cyklusdetektion i rettede og ikke-rettede grafer