Förberedelse inför kodningsintervjuer · Lektion

Cykeldetektering i riktade och oriktade grafer

Detektera cykler i oriktade grafer med spårning av föräldrar och i riktade grafer med DFS-färgkodning, där besökta noder har tre tillstånd: vit, grå eller svart.

Lektion 4 av 413 steg

Cykeldetektering i riktade och oriktade grafer är en gratis lektion i Förberedelse inför kodningsintervjuer på CoddyKit. Detta är lektion 4 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Förberedelse inför kodningsintervjuer, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Förberedelse inför kodningsintervjuer innehåller totalt 4 lektioner.

Varför cykeldetektering är viktig

En cykel i en graf är en väg som börjar och slutar i samma nod. Cykeldetektering är avgörande i många algoritmer: topologisk sortering misslyckas på grafer med cykler, beroendehantering måste upptäcka cirkulära beroenden och identifiering av dödlägen vid schemaläggning i operativsystem kräver att cykler hittas i grafer för resursallokering. Metoden skiljer sig mellan oriktade och riktade grafer — de kräver fundamentalt olika 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')

Cykeldetektering med DFS i oriktade grafer

I en oriktad graf finns det en cykel om DFS besöker en nod som redan finns i den aktuella sökvägen (inte bara är besökt). Utmaningen är att varje kant finns i båda riktningarna, så när vi besöker en underordnad nod innehåller dess grannlista vår aktuella nod (föräldern). Vi måste hålla reda på föräldern till varje nod för att inte felaktigt flagga kanten tillbaka till föräldern som en cykel. Om vi stöter på en besökt nod som inte är vår förälder har vi hittat en cykel.

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

Cykel i oriktad graf med BFS

Cykeldetektering med BFS i en oriktad graf håller också reda på föräldern till varje besökt nod. När ni behandlar en nods grannar finns det en cykel om en granne redan är besökt och inte är den aktuella nodens förälder. Använd en ordbok för att lagra föräldrar. Denna O(V + E)-metod undviker problemet med rekursionsgränsen och är det föredragna iterativa alternativet för stora 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

Cykel i riktad graf: varför spårning av förälder inte fungerar

I en riktad graf räcker det inte att hålla reda på föräldrar. Tänk på A→C och B→C: noden C har två 'föräldrar', men ingen cykel. Den korrekta metoden använder färgning i tre tillstånd: vit (obesökt), grå (i den aktuella DFS-sökvägen/stacken) och svart (helt behandlad). Det finns en cykel om vi under DFS någonsin stöter på en grå nod — det betyder att vi har hittat en bakåtkant till en förfader i den aktuella sökvägen.

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

Riktad cykeldetektering med DFS i tre tillstånd

Använd en array state[] med värdena 0 (vit/obesökt), 1 (grå/i stacken) och 2 (svart/färdigbehandlad). Starta DFS och markera noden som grå när den besöks och som svart när den lämnas. Om DFS någonsin når en grå nod har en bakåtkant hittats — det finns en cykel. Om den når en svart nod är den vägen redan helt utforskad och cykelfri, så hoppa över den.

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

Kursschema: cykel i en DAG

Kursschema (LeetCode #207) frågar om alla kurser kan slutföras utifrån givna förkunskapskrav. Modellera kurser som noder och förkunskapskrav som riktade kanter. Alla kurser kan slutföras om och endast om grafen är en DAG (utan cykler). Använd cykeldetektering med DFS i tre tillstånd — om en cykel hittas, returnera False; annars returnera 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

Cykeldetektering med Kahns algoritm (BFS)

En alternativ metod för cykeldetektering i riktade grafer använder Kahns topologiska BFS-sortering. Räkna ingraderna för alla noder. Lägg noder med ingrad 0 i en kö. Behandla varje nod: minska grannarnas ingrader och lägg dem vars ingrad når 0 i kön. Om antalet behandlade noder är lika med V finns ingen cykel; annars finns en cykel (de obehandlade noderna bildar cykler). Denna O(V + E)-metod är intuitiv och lättare att komma ihåg än DFS i tre tillstånd.

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

Hitta cykeln: samla cykelnoder

Ibland behöver ni identifiera vilka noder som ingår i en cykel, inte bara upptäcka att den finns. Under DFS i tre tillstånd ska ni, när en bakåtkant hittas, följa tillbaka genom anropsstacken (eller en sökvägsstack) för att samla alla noder mellan förfadern och den aktuella noden. En sökvägsstack som underhålls parallellt med state-arrayen fångar den aktuella DFS-sökvägen och möjliggör rekonstruktion av cykeln på 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]

Hitta slutligen säkra tillstånd

Hitta slutligen säkra tillstånd (LeetCode #802) frågar vilka noder som till slut leder till en terminalnod (utan utgående kanter) utan att fastna i en cykel. En nod är 'säker' om alla vägar från den leder till terminalnoder. Använd DFS i tre tillstånd: noder som är svarta (helt behandlade utan att någon cykel upptäcktes) är säkra. Noder som ingår i eller leder till en cykel är inte säkra.

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]

Överflödig förbindelse i oriktad graf

Överflödig förbindelse (LeetCode #684) hittar den kant som skapar en cykel när den läggs till i en annars acyklisk oriktad graf. Detta kan lösas med cykeldetektering via DFS, men den renaste lösningen använder Union-Find (DSU): behandla kanterna en i taget; om båda ändpunkterna redan är sammanbundna (finns i samma komponent) skapar den aktuella kanten en cykel och är svaret. DSU ger O(alpha(n)) per operation — i praktiken 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]

Sammanfattning: strategier för cykeldetektering

För att sammanfatta verktygen för cykeldetektering: i oriktade grafer använder ni DFS med spårning av föräldrar eller Union-Find. I riktade grafer använder ni DFS i tre tillstånd (vit/grå/svart) eller Kahns topologiska BFS-sortering. Välj Union-Find när ni lägger till kanter en i taget (online). Välj Kahn när ni också behöver den topologiska ordningen. Välj DFS i tre tillstånd när ni behöver identifiera de specifika cykelnoderna. Ange alltid skillnaden mellan riktade och oriktade grafer när ni diskuterar cykeldetektering under intervjuer.

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

Snabbtest

Testa era kunskaper i begreppen Data Structures & Algorithms — Coding Interview Prep från den här lektionen.

Lektionssammanfattning

I den här lektionen lärde ni er: cykeldetektering i oriktade grafer med DFS och spårning av föräldrar, cykeldetektering i riktade grafer med färgning i tre tillstånd — vit/grå/svart —, Kahns BFS-alternativ för riktade grafer samt tillämpningar som kursschema, överflödig förbindelse och slutligen säkra tillstånd. Härnäst går vi igenom grunderna i dynamisk programmering.

Gratis att börja

Lär dig Förberedelse inför kodningsintervjuer med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
90
Lektioner
360

Vanliga frågor

Är lektionen ”Cykeldetektering i riktade och oriktade grafer” gratis?

Ja – hela texten till ”Cykeldetektering i riktade och oriktade grafer” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Förberedelse inför kodningsintervjuer, kan Ni uppgradera till CoddyKit PRO. Kursen i Förberedelse inför kodningsintervjuer innehåller totalt 4 lektioner.

Vad lär jag mig i ”Cykeldetektering i riktade och oriktade grafer”?

Detektera cykler i oriktade grafer med spårning av föräldrar och i riktade grafer med DFS-färgkodning, där besökta noder har tre tillstånd: vit, grå eller svart. Ni övar på Förberedelse inför kodningsintervjuer med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig Förberedelse inför kodningsintervjuer?

Du behöver inga förkunskaper. Utbildningen i Förberedelse inför kodningsintervjuer på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 4 av 4.

Hur lång tid tar lektionen ”Cykeldetektering i riktade och oriktade grafer”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här Förberedelse inför kodningsintervjuer-lektionen?

Ja. Varje Förberedelse inför kodningsintervjuer-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Grafrepresentationer och förberedelser för genomgång
  2. BFS: kortaste väg och nivågenomgång
  3. DFS: sammanhängande komponenter och flood fill
  4. Cykeldetektering i riktade och oriktade grafer
← Tillbaka till Förberedelse inför kodningsintervjuer