DSA Interview Prep · Lektion

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

Lektion 4 af 413 trin

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)]))               # False

Cyklus 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)]))  # True

Cyklus 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)]))               # False

Kursusplan: 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 dependency

Cyklusdetektion 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)]))               # False

Find 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.

Gratis at komme i gang

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

  1. Grafrepræsentationer og opsætning af gennemløb
  2. BFS: korteste sti og niveaugennemløb
  3. DFS: sammenhængende komponenter og flood fill
  4. Cyklusdetektion i rettede og ikke-rettede grafer
← Tilbage til DSA Interview Prep