Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah
Kesan kitaran dalam graf tidak berarah dengan menjejak ibu bapa dan dalam graf berarah dengan pewarnaan DFS (tiga keadaan lawatan putih/kelabu/hitam).
Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah ialah pelajaran DSA Interview Prep percuma di CoddyKit. Ini ialah pelajaran 4 daripada 4. Sebanyak 3 pelajaran dalam laluan pembelajaran ini boleh dibaca sepenuhnya secara percuma — selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan praktikal dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Pelajaran ini merupakan sebahagian daripada laluan pembelajaran DSA Interview Prep, dan kemajuan anda disegerakkan merentas web serta aplikasi CoddyKit. Kursus DSA Interview Prep merangkumi sejumlah 4 pelajaran.
Mengapa Pengesanan Kitaran Penting
Kitaran dalam graf ialah laluan yang bermula dan berakhir pada nod yang sama. Pengesanan kitaran amat penting dalam banyak algoritma: pengisihan topologi gagal pada graf berkitaran, penyelesaian kebergantungan mesti mengesan kebergantungan bulat, dan pengesanan kebuntuan dalam penjadualan OS memerlukan pencarian kitaran dalam graf peruntukan sumber. Pendekatannya berbeza antara graf tidak berarah dan berarah — kedua-duanya memerlukan algoritma yang berbeza secara asasnya.
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')Pengesanan Kitaran Tidak Berarah dengan DFS
Dalam graf tidak berarah, kitaran wujud jika DFS melawati nod yang sudah berada dalam laluan semasa (bukan sekadar nod yang telah dilawati). Cabarannya: setiap sisi muncul dalam kedua-dua arah, jadi apabila kita melawati nod anak, senarai jirannya turut mengandungi nod semasa kita (induknya). Kita mesti menjejaki induk setiap nod untuk mengelakkan sisi kembali kepada induk daripada ditandai secara palsu sebagai kitaran. Jika kita menemui nod yang telah dilawati tetapi bukan induk kita, kita telah menemui kitaran.
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)])) # FalseKitaran Tidak Berarah dengan BFS
Pengesanan kitaran BFS dalam graf tidak berarah juga menjejaki induk bagi setiap nod yang telah dilawati. Semasa memproses jiran sesuatu nod, jika jiran itu telah dilawati dan bukan induk nod semasa, maka wujud kitaran. Gunakan kamus untuk menyimpan induk. Pendekatan O(V + E) ini mengelakkan kebimbangan tentang had rekursi dan merupakan alternatif lelaran yang lebih sesuai untuk graf besar.
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)])) # TrueKitaran Berarah: Mengapa Penjejakan Induk Gagal
Dalam graf berarah, penjejakan induk tidak mencukupi. Pertimbangkan A→C dan B→C: nod C mempunyai dua 'induk' tetapi tiada kitaran. Pendekatan yang betul menggunakan pewarnaan tiga keadaan: putih (belum dilawati), kelabu (dalam laluan/tindanan DFS semasa), dan hitam (telah diproses sepenuhnya). Kitaran wujud jika kita menemui nod kelabu semasa DFS — ini bermakna kita telah menemui sisi belakang kepada leluhur dalam laluan semasa.
# 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)')Pengesanan Kitaran Berarah dengan DFS Tiga Keadaan
Gunakan tatasusunan state[] dengan nilai 0 (putih/belum dilawati), 1 (kelabu/dalam tindanan), dan 2 (hitam/selesai). Mulakan DFS, tandakan nod sebagai kelabu semasa masuk dan hitam semasa keluar. Jika DFS sampai ke nod kelabu, sisi belakang telah ditemui — terdapat kitaran. Jika ia sampai ke nod hitam, laluan itu telah diterokai sepenuhnya dan bebas kitaran, jadi langkaunya.
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)])) # FalseJadual Kursus: Kitaran dalam DAG
Jadual Kursus (LeetCode #207) meminta anda menentukan sama ada semua kursus boleh diselesaikan berdasarkan prasyarat yang diberikan. Modelkan kursus sebagai nod dan prasyarat sebagai sisi berarah. Semua kursus boleh diselesaikan jika dan hanya jika graf itu ialah DAG (tiada kitaran). Gunakan pengesanan kitaran DFS tiga keadaan — jika kitaran ditemui, kembalikan palsu; jika tidak, kembalikan benar.
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 dependencyPengesanan Kitaran dengan Algoritma Kahn (BFS)
Pendekatan alternatif untuk mengesan kitaran dalam graf berarah menggunakan pengisihan topologi BFS Kahn. Kira darjah masuk semua nod. Masukkan nod yang mempunyai darjah masuk 0 ke dalam baris gilir. Proses setiap nod: kurangkan darjah masuk jiran, kemudian masukkan nod yang mencapai 0 ke dalam baris gilir. Jika bilangan nod yang diproses sama dengan V, tiada kitaran; jika tidak, wujud kitaran (nod yang belum diproses membentuk kitaran). Pendekatan O(V + E) ini mudah difahami dan diingati berbanding DFS tiga keadaan.
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)])) # FalseCari Kitaran: Mengumpulkan Nod Kitaran
Kadangkala anda perlu mengenal pasti nod yang menjadi sebahagian daripada kitaran, bukan sekadar mengesan kewujudannya. Semasa DFS tiga keadaan, apabila sisi belakang ditemui, jejak semula tindanan panggilan (atau tindanan laluan) untuk mengumpulkan semua nod antara leluhur dengan nod semasa. Tindanan laluan yang dikekalkan bersama tatasusunan keadaan merekodkan laluan DFS semasa, lalu membolehkan pembinaan semula kitaran dalam O(panjang kitaran).
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]Cari Nod Selamat Akhir
Cari Nod Selamat Akhir (LeetCode #802) meminta anda menentukan nod yang akhirnya membawa kepada nod terminal (tiada sisi keluar) tanpa tersekat dalam kitaran. Sesuatu nod adalah 'selamat' jika semua laluan daripadanya membawa kepada nod terminal. Gunakan DFS tiga keadaan: nod yang berwarna hitam (diproses sepenuhnya tanpa mengesan kitaran) adalah selamat. Nod yang menjadi sebahagian daripada kitaran atau membawa kepada kitaran tidak selamat.
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]Sambungan Berlebihan dalam Graf Tidak Berarah
Sambungan Berlebihan (LeetCode #684) mencari sisi yang mewujudkan kitaran apabila ditambahkan pada graf tidak berarah yang pada asalnya bebas kitaran. Walaupun masalah ini boleh diselesaikan dengan pengesanan kitaran DFS, penyelesaian paling kemas menggunakan Gabung-Cari (DSU): proses sisi satu demi satu; jika kedua-dua titik hujungnya sudah bersambung (komponen yang sama), sisi semasa mewujudkan kitaran dan itulah jawapannya. DSU memberikan O(alpha(n)) bagi setiap operasi — secara berkesan 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]Ringkasan: Strategi Pengesanan Kitaran
Untuk meringkaskan himpunan alat pengesanan kitaran: bagi graf tidak berarah, gunakan DFS dengan penjejakan induk atau Gabung-Cari. Bagi graf berarah, gunakan DFS tiga keadaan (putih/kelabu/hitam) atau pengisihan topologi BFS Kahn. Pilih Gabung-Cari apabila anda menambahkan sisi satu demi satu (dalam talian). Pilih Kahn apabila anda juga memerlukan susunan topologi. Pilih DFS tiga keadaan apabila anda perlu mengenal pasti nod kitaran tertentu. Sentiasa nyatakan perbezaan antara graf berarah dengan tidak berarah apabila membincangkan pengesanan kitaran dalam temu duga.
# 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')Semakan Ringkas
Uji pemahaman anda tentang konsep Struktur Data & Algoritma — Persediaan Temu Duga Pengekodan daripada pelajaran ini.
Imbas Kembali Pelajaran
Dalam pelajaran ini anda mempelajari: pengesanan kitaran tidak berarah dengan DFS yang menjejaki induk, pengesanan kitaran berarah dengan pewarnaan tiga keadaan putih/kelabu/hitam, alternatif BFS Kahn untuk graf berarah, serta aplikasi termasuk jadual kursus, sambungan berlebihan dan nod selamat akhir. Seterusnya kita akan menyelami asas pengaturcaraan dinamik.
Pelajari Python dengan tutor kecerdasan buatan — percuma
Tulis dan jalankan kod sebenar dalam pelayar anda, dapatkan bantuan segera daripada tutor kecerdasan buatan yang tersedia 24/7, dan sambung semula dari tempat anda berhenti di web atau dalam aplikasi.
- Kursus
- 30
- Pelajaran
- 120
Soalan Lazim
Adakah pelajaran “Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah” percuma?
Ya — sebanyak 3 pelajaran dalam laluan pembelajaran DSA Interview Prep, termasuk “Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah”, boleh dibaca sepenuhnya secara percuma di web ini. Selepas itu, CoddyKit PRO membuka akses kepada semua pelajaran, serta latihan interaktif dengan penyunting kod terbina dalam dan tutor kecerdasan buatan yang tersedia 24/7. Kursus DSA Interview Prep merangkumi sejumlah 4 pelajaran.
Apakah yang akan saya pelajari dalam “Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah”?
Kesan kitaran dalam graf tidak berarah dengan menjejak ibu bapa dan dalam graf berarah dengan pewarnaan DFS (tiga keadaan lawatan putih/kelabu/hitam). Anda berlatih DSA Interview Prep menggunakan kod praktikal yang dijalankan terus dalam pelayar, manakala tutor kecerdasan buatan 24/7 menjawab soalan anda semasa anda mengikuti pelajaran.
Adakah saya memerlukan pengalaman untuk memulakan DSA Interview Prep?
Tiada pengalaman terdahulu diperlukan. Pembelajaran DSA Interview Prep di CoddyKit disusun untuk pelajar daripada peringkat pemula hingga lanjutan, jadi anda boleh bermula di sini atau dari awal dan belajar mengikut kadar anda sendiri. Ini ialah pelajaran 4 daripada 4.
Berapa lamakah pelajaran “Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah” diambil?
Kebanyakan pelajaran CoddyKit mengambil masa kira-kira 5–10 minit. Setiap pelajaran ringkas dan interaktif, jadi anda boleh membuat kemajuan secara berterusan dan menyambung tepat dari tempat anda berhenti di web atau aplikasi.
Bolehkah saya menulis dan menjalankan kod dalam pelajaran DSA Interview Prep ini?
Ya. Setiap pelajaran DSA Interview Prep menyertakan penyunting kod terbina dalam, jadi anda boleh menulis dan menjalankan kod sebenar terus dalam pelayar serta menerima maklum balas kecerdasan buatan serta-merta — tanpa memerlukan persediaan setempat.
Semua pelajaran dalam kursus ini
- Perwakilan Graf dan Persediaan Lintasan
- BFS: Laluan Terpendek dan Lintasan Mengikut Aras
- DFS: Komponen Bersambung dan Isi Banjir
- Pengesanan Kitaran dalam Graf Berarah dan Tidak Berarah