Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa
Tunnistakaa syklit suuntaamattomissa graafeissa seuraamalla vanhempia ja suunnatuissa graafeissa DFS:n väritetyllä tilamallilla (valkoinen/harmaa/musta).
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)])) # FalseSuuntaamaton 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)])) # TrueSuunnatun 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)])) # FalseKurssiaikataulu: 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 dependencySyklien 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)])) # FalseSyklin 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.
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
- Graafien esitystavat ja läpikäyntien valmistelu
- BFS: lyhin polku ja tasoläpikäynti
- DFS: yhtenäiset komponentit ja flood fill
- Syklien tunnistaminen suunnatuissa ja suuntaamattomissa graafeissa