DSA Interview Prep · Oppitunti

Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa

Tunnistakaa syklit suuntaamattomissa graafeissa seuraamalla vanhempia ja suunnatuissa graafeissa DFS:n väritetyllä tilamallilla (valkoinen/harmaa/musta).

Oppitunti 4/413 vaihetta

Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa on ilmainen DSA Interview Prep-oppitunti CoddyKitissä. Tämä on oppitunti 4/4. Voit lukea tästä oppimispolusta kokonaan mitkä tahansa 3 oppituntia ilmaiseksi — sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä käytännön harjoittelun sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. Oppitunti kuuluu DSA Interview Prep-oppimispolkuun, ja edistymisesi synkronoituu verkon ja CoddyKit-sovelluksen välillä. DSA Interview Prep-kurssilla on yhteensä 4 oppituntia.

Miksi syklien tunnistaminen on tärkeää

Sykli graafissa on polku, joka alkaa ja päättyy samaan solmuun. Syklien tunnistaminen on tärkeää monissa algoritmeissa: topologinen lajittelu epäonnistuu syklisissä graafeissa, riippuvuuksien ratkaisemisessa on tunnistettava kehämäiset riippuvuudet, ja käyttöjärjestelmän ajoituksen lukkiutumisen tunnistaminen edellyttää syklien löytämistä resurssien varausten graafeista. Lähestymistapa on erilainen suuntaamattomissa ja suunnatuissa graafeissa — ne edellyttävät pohjimmiltaan erilaisia algoritmeja.

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

Suuntaamattoman graafin syklin tunnistaminen DFS:llä

Suuntaamattomassa graafissa on sykli, jos DFS käsittelee solmun, joka on jo nykyisellä polulla (eikä vain merkitty vierailluksi). Haasteena on, että jokainen reuna esiintyy molempiin suuntiin, joten lapsisolmun naapuriluettelo sisältää myös nykyisen solmumme eli vanhemman. Jokaisen solmun vanhempi on pidettävä tallessa, jotta reunaa takaisin vanhempaan ei tulkita virheellisesti sykliksi. Jos löydätte vieraillun solmun, joka ei ole vanhempi, olette löytäneet syklin.

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

Suuntaamaton sykli BFS:llä

Suuntaamattoman graafin syklien tunnistaminen BFS:llä seuraa myös jokaisen vieraillun solmun vanhempaa. Kun käsittelette solmun naapureita, sykli on olemassa, jos naapuri on jo vierailluksi merkitty eikä ole nykyisen solmun vanhempi. Tallentakaa vanhemmat sanakirjaan. Tämä O(V + E) -menetelmä välttää rekursiorajaan liittyvät ongelmat ja on suositeltava iteratiivinen vaihtoehto suurille graafeille.

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

Suunnatun graafin sykli: miksi vanhemman seuranta ei riitä

Suunnatussa graafissa vanhemman seuraaminen ei riitä. Ajatelkaa reunoja A→C ja B→C: solmulla C on kaksi 'vanhempaa', mutta sykliä ei ole. Oikea lähestymistapa käyttää kolmen tilan väritystä: valkoinen (vierailematon), harmaa (nykyisessä DFS-polussa/pinossa) ja musta (kokonaan käsitelty). Sykli on olemassa, jos DFS:n aikana kohdataan harmaa solmu — tällöin on löytynyt takareuna nykyisen polun esi-isään.

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

Suunnatun graafin syklien tunnistaminen kolmitilaisella DFS:llä

Käyttäkää taulukkoa state[], jonka arvot ovat 0 (valkoinen/vierailematon), 1 (harmaa/pinossa) ja 2 (musta/valmis). Aloittakaa DFS, merkitkää solmu harmaaksi siihen saavuttaessa ja mustaksi siitä poistuttaessa. Jos DFS saavuttaa harmaan solmun, on löytynyt takareuna — graafissa on sykli. Jos se saavuttaa mustan solmun, kyseinen polku on jo tutkittu kokonaan eikä sisällä sykliä, joten ohittakaa se.

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

Kurssiaikataulu: sykli DAG-graafissa

Kurssiaikataulu (LeetCode #207) kysyy, voidaanko kaikki kurssit suorittaa annettujen esitietojen perusteella. Mallintakaa kurssit solmuina ja esivaatimukset suunnattuina särminä. Kaikki kurssit voidaan suorittaa täsmälleen silloin, kun graafi on DAG (syklitön). Käyttäkää kolmitilaista DFS-syklintunnistusta — jos sykli löytyy, palauttakaa False, muussa tapauksessa 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

Syklien tunnistaminen Kahnin algoritmilla (BFS)

Suunnattujen graafien syklit voidaan tunnistaa vaihtoehtoisesti Kahnin BFS-pohjaisella topologisella lajittelulla. Laskekaa kaikkien solmujen sisäasteet. Asettakaa sisäasteen 0 omaavat solmut jonoon. Käsitelkää solmut yksi kerrallaan: pienentäkää naapureiden sisäasteita ja lisätkää jonoon ne, joiden sisäaste saavuttaa arvon 0. Jos käsiteltyjen solmujen määrä on V, sykliä ei ole; muuten sykli on olemassa (käsittelemättömät solmut muodostavat syklejä). Tämä O(V + E) -menetelmä on havainnollinen ja helpompi muistaa kuin kolmitilainen DFS.

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

Syklin etsiminen: syklisolmujen kerääminen

Joskus on tunnistettava, mitkä solmut kuuluvat sykliin, eikä vain sitä, onko sykli olemassa. Kun kolmitilaisen DFS:n aikana löytyy takareuna, palatkaa kutsupinoa (tai polkupinoa) pitkin ja kerätkää kaikki esi-isän ja nykyisen solmun väliset solmut. Tila-taulukon rinnalla ylläpidettävä polkupino tallentaa nykyisen DFS-polun, joten syklin voi muodostaa ajassa 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]

Lopulta turvallisten solmujen etsiminen

Lopulta turvallisten solmujen etsiminen (LeetCode #802) kysyy, mitkä solmut johtavat lopulta päätössolmuun (solmuun, josta ei lähde reunoja) juuttumatta sykliin. Solmu on 'turvallinen', jos kaikki siitä alkavat polut johtavat päätössolmuihin. Käyttäkää kolmitilaista DFS:ää: mustat solmut (jotka on käsitelty kokonaan ilman syklin havaitsemista) ovat turvallisia. Solmut, jotka kuuluvat sykliin tai johtavat siihen, eivät ole turvallisia.

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]

Ylimääräinen yhteys suuntaamattomassa graafissa

Ylimääräinen yhteys (LeetCode #684) etsii särmän, joka luo syklin, kun se lisätään muuten syklittömään suuntaamattomaan graafiin. Tämän voi ratkaista DFS-syklintunnistuksella, mutta selkein ratkaisu käyttää Union-Find (DSU) -rakennetta: käsitelkää särmät yksi kerrallaan; jos molemmat päätesolmut ovat jo yhteydessä toisiinsa (samassa komponentissa), nykyinen särmä luo syklin ja on vastaus. DSU:n yhden operaation aikavaativuus on O(alpha(n)) — käytännössä 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]

Yhteenveto: syklien tunnistusstrategiat

Syklien tunnistamisen työkalupakki voidaan tiivistää näin: suuntaamattomissa graafeissa käyttäkää DFS:ää vanhemman seurannalla tai Union-Find-rakennetta. Suunnatuissa graafeissa käyttäkää kolmitilaista DFS:ää (valkoinen/harmaa/musta) tai Kahnin BFS-pohjaista topologista lajittelua. Valitkaa Union-Find, kun lisäätte särmiä yksi kerrallaan (online). Valitkaa Kahnin algoritmi, kun tarvitsette myös topologisen järjestyksen. Valitkaa kolmitilainen DFS, kun tarvitsette tietää tarkat sykliin kuuluvat solmut. Mainitkaa syklien tunnistamisesta keskusteltaessa aina ero suunnattujen ja suuntaamattomien graafien välillä työhaastatteluissa.

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

Pikatarkistus

Testatkaa, miten hyvin hallitsette tämän oppitunnin Data Structures & Algorithms — Coding Interview Prep -aiheen käsitteet.

Oppitunnin yhteenveto

Tässä oppitunnissa opitte: suuntaamattomien graafien syklien tunnistamisen vanhempaa seuraavalla DFS:llä, suunnattujen graafien syklien tunnistamisen kolmitilaisella valkoisen/harmaan/mustan värityksellä, Kahnin BFS-vaihtoehdon suunnatuille graafeille sekä sovelluksia, kuten kurssiaikataulun, ylimääräisen yhteyden ja lopulta turvallisten solmujen ongelmat. Seuraavaksi perehdymme dynaamisen ohjelmoinnin perusteisiin.

Aloita maksutta

Opi Python tekoälytuutorin avulla — ilmaiseksi

Kirjoita ja suorita oikeaa koodia selaimessa, saa välitöntä apua tekoälytuutorilta ympäri vuorokauden ja jatka siitä, mihin jäit, verkossa tai sovelluksessa.

Kurssit
30
Oppitunnit
120

Usein kysytyt kysymykset

Onko oppitunti ”Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa” ilmainen?

Kyllä — voit lukea täällä verkossa kokonaan ilmaiseksi mitkä tahansa DSA Interview Prep-oppimispolun 3 oppituntia, myös oppitunnin “Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa”. Sen jälkeen CoddyKit PRO avaa kaikki oppitunnit sekä interaktiiviset harjoitukset sisäänrakennetulla koodieditorilla ja ympäri vuorokauden toimivalla tekoälytuutorilla. DSA Interview Prep-kurssilla on yhteensä 4 oppituntia.

Mitä opin oppitunnilla ”Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa”?

Tunnistakaa syklit suuntaamattomissa graafeissa seuraamalla vanhempia ja suunnatuissa graafeissa DFS:n väritetyllä tilamallilla (valkoinen/harmaa/musta). Harjoittelet DSA Interview Prep-aihetta koodilla, jonka suoritat suoraan selaimessa. Ympäri vuorokauden käytettävissä oleva tekoälytuutori vastaa kysymyksiisi oppitunnin aikana.

Tarvitsenko kokemusta aloittaakseni DSA Interview Prep-opiskelun?

Aiempi kokemus ei ole tarpeen. CoddyKitin DSA Interview Prep-oppimispolku sopii vasta-alkajista edistyneisiin, joten voit aloittaa tästä tai alusta ja edetä omaan tahtiisi. Tämä on oppitunti 4/4.

Kuinka kauan ”Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa”-oppitunnin suorittaminen kestää?

Useimmat CoddyKitin oppitunnit kestävät noin 5–10 minuuttia. Jokainen oppitunti on lyhyt ja interaktiivinen, joten edistyt tasaisesti ja voit jatkaa siitä, mihin jäit – sekä verkossa että sovelluksessa.

Voinko kirjoittaa ja suorittaa koodia tällä DSA Interview Prep-oppitunnilla?

Kyllä. Jokainen DSA Interview Prep-oppitunti sisältää sisäänrakennetun koodieditorin, joten voit kirjoittaa ja suorittaa oikeaa koodia suoraan selaimessa ja saada välitöntä palautetta tekoälyltä – paikallista asennusta ei tarvita.

Kaikki tämän kurssin oppitunnit

  1. Graafien esitystavat ja läpikäyntien valmistelu
  2. BFS: lyhin polku ja tasoläpikäynti
  3. DFS: yhtenäiset komponentit ja flood fill
  4. Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa
← Takaisin: DSA Interview Prep