การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง
ตรวจจับวัฏจักรในกราฟไม่มีทิศทางด้วยการติดตามโหนดแม่ และในกราฟมีทิศทางด้วยการระบายสีสถานะของ DFS (ขาว/เทา/ดำ สามสถานะของการเยี่ยมชม)
การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง เป็นบทเรียน Coding Interview Prep ฟรีบน CoddyKit นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Coding Interview Prep และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
เหตุใดการตรวจจับวัฏจักรจึงสำคัญ
วัฏจักรในกราฟคือเส้นทางที่เริ่มต้นและสิ้นสุดที่โหนดเดียวกัน การตรวจจับวัฏจักรมีความสำคัญอย่างยิ่งในอัลกอริทึมหลายประเภท: การเรียงลำดับเชิงทอพอโลยีจะล้มเหลวบนกราฟที่มีวัฏจักร การจัดการข้อพึ่งพาต้องตรวจจับการพึ่งพาแบบวนรอบ และการตรวจจับภาวะหยุดชะงักในการจัดตาราง OS ต้องค้นหาวัฏจักรในกราฟการจัดสรรทรัพยากร แนวทางจะแตกต่างกันระหว่างกราฟแบบไม่มีทิศทางและกราฟแบบมีทิศทาง โดยทั้งสองแบบต้องใช้อัลกอริทึมที่แตกต่างกันโดยพื้นฐาน
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')ตรวจจับวัฏจักรในกราฟไม่มีทิศทางด้วย DFS
ในกราฟไม่มีทิศทาง จะมีวัฏจักรหาก DFS เข้าถึงโหนดที่อยู่ในเส้นทางปัจจุบันอยู่แล้ว (ไม่ใช่เพียงแค่เคยเข้าชมแล้ว) ความท้าทายคือเส้นเชื่อมทุกเส้นปรากฏในทั้งสองทิศทาง ดังนั้นเมื่อเราเข้าถึงโหนดลูก รายการโหนดข้างเคียงของโหนดนั้นจะมีโหนดปัจจุบันของเราอยู่ด้วย ซึ่งก็คือโหนดแม่ เราต้องติดตามโหนดแม่ของแต่ละโหนด เพื่อไม่ให้เข้าใจผิดว่าเส้นเชื่อมกลับไปยังโหนดแม่เป็นวัฏจักร หากเราพบโหนดที่เคยเข้าชมแล้วแต่ไม่ใช่โหนดแม่ของเรา แสดงว่าเราพบวัฏจักร
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วัฏจักรในกราฟไม่มีทิศทางด้วย BFS
การตรวจจับวัฏจักรด้วย BFS ในกราฟไม่มีทิศทางก็ติดตามโหนดแม่ของแต่ละโหนดที่เข้าชมแล้วเช่นกัน เมื่อประมวลผลโหนดข้างเคียงของโหนดหนึ่ง หากโหนดข้างเคียงนั้นถูกเข้าชมแล้วและไม่ใช่โหนดแม่ของโหนดปัจจุบัน แสดงว่ามีวัฏจักร ให้ใช้พจนานุกรมเพื่อจัดเก็บโหนดแม่ วิธี O(V + E) นี้ไม่ต้องกังวลเรื่องขีดจำกัดการเรียกซ้ำ และเป็นทางเลือกแบบวนซ้ำที่เหมาะสำหรับกราฟขนาดใหญ่
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วัฏจักรในกราฟมีทิศทาง: เหตุใดการติดตามโหนดแม่จึงใช้ไม่ได้
ในกราฟมีทิศทาง การติดตามโหนดแม่ไม่เพียงพอ ลองพิจารณา A→C และ B→C: โหนด C มี 'โหนดแม่' สองโหนด แต่ไม่มีวัฏจักร แนวทางที่ถูกต้องใช้การระบายสีสามสถานะ: สีขาว (ยังไม่เคยเข้าชม), สีเทา (อยู่ในเส้นทาง/สแตก DFS ปัจจุบัน), สีดำ (ประมวลผลเสร็จสมบูรณ์แล้ว) จะมีวัฏจักรหากเราเข้าถึงโหนดสีเทาระหว่างทำ DFS ซึ่งหมายความว่าเราพบเส้นเชื่อมย้อนกลับไปยังโหนดบรรพบุรุษในเส้นทางปัจจุบัน
# 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)')ตรวจจับวัฏจักรในกราฟมีทิศทางด้วย DFS สามสถานะ
ใช้อาร์เรย์ state[] ที่มีค่า 0 (สีขาว/ยังไม่เคยเข้าชม), 1 (สีเทา/อยู่ในสแตก), 2 (สีดำ/เสร็จแล้ว) เริ่มทำ DFS โดยทำเครื่องหมายโหนดเป็นสีเทาเมื่อเข้าไป และเป็นสีดำเมื่อออก หาก DFS เข้าถึงโหนดสีเทาเมื่อใด แสดงว่าพบเส้นเชื่อมย้อนกลับ ซึ่งหมายความว่ามีวัฏจักร หากเข้าถึงโหนดสีดำ เส้นทางนั้นถูกสำรวจเสร็จสมบูรณ์แล้วและไม่มีวัฏจักร จึงข้ามได้
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ตารางเรียน: วัฏจักรใน DAG
ตารางเรียน (LeetCode #207) ถามว่าสามารถเรียนทุกรายวิชาให้เสร็จได้หรือไม่เมื่อกำหนดวิชาที่ต้องเรียนก่อน ให้จำลองวิชาเป็นโหนดและข้อกำหนดก่อนเรียนเป็นเส้นเชื่อมแบบมีทิศทาง จะเรียนจบได้ทุกรายวิชาก็ต่อเมื่อกราฟเป็นDAG (ไม่มีวัฏจักร) ให้ใช้การตรวจจับวัฏจักรด้วย DFS สามสถานะ หากพบวัฏจักร ให้คืนค่าเท็จ มิฉะนั้นให้คืนค่าจริง
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ตรวจจับวัฏจักรด้วยอัลกอริทึมของคาห์น (BFS)
อีกทางเลือกหนึ่งในการตรวจจับวัฏจักรในกราฟมีทิศทางคือการเรียงลำดับเชิงทอพอโลยีด้วย BFS ของคาห์น ให้นับดีกรีขาเข้าของโหนดทั้งหมด ใส่โหนดที่มีดีกรีขาเข้าเป็น 0 ลงในคิว ประมวลผลแต่ละโหนดโดยลดดีกรีขาเข้าของโหนดข้างเคียง และใส่โหนดที่ลดลงจนถึง 0 ลงในคิว หากจำนวนโหนดที่ประมวลผลแล้วเท่ากับ V แสดงว่าไม่มีวัฏจักร มิฉะนั้นจะมีวัฏจักร (โหนดที่ยังไม่ได้ประมวลผลประกอบกันเป็นวัฏจักร) วิธี O(V + E) นี้เข้าใจง่ายและจดจำได้ง่ายกว่า 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ค้นหาวัฏจักร: รวบรวมโหนดในวัฏจักร
บางครั้งคุณจำเป็นต้องระบุว่าโหนดใดอยู่ในวัฏจักร ไม่ใช่เพียงตรวจว่ามีวัฏจักรหรือไม่ ระหว่างทำ DFS สามสถานะ เมื่อพบเส้นเชื่อมย้อนกลับ ให้ย้อนกลับผ่านสแตกการเรียก (หรือสแตกเส้นทาง) เพื่อรวบรวมโหนดทั้งหมดระหว่างโหนดบรรพบุรุษกับโหนดปัจจุบัน การดูแลสแตกเส้นทางควบคู่ไปกับอาร์เรย์สถานะจะบันทึกเส้นทาง DFS ปัจจุบัน ทำให้สร้างวัฏจักรกลับคืนมาได้ในเวลา O(ความยาวของวัฏจักร)
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]ค้นหาสถานะที่ปลอดภัยในท้ายที่สุด
ค้นหาสถานะที่ปลอดภัยในท้ายที่สุด (LeetCode #802) ถามว่าโหนดใดนำไปสู่โหนดปลายทางในท้ายที่สุด (ไม่มีเส้นเชื่อมขาออก) โดยไม่ติดอยู่ในวัฏจักร โหนดจะอยู่ในสถานะ 'ปลอดภัย' หากทุกเส้นทางจากโหนดนั้นนำไปสู่โหนดปลายทาง ใช้ DFS สามสถานะ: โหนดที่เป็นสีดำ (ประมวลผลเสร็จสมบูรณ์โดยไม่พบวัฏจักร) ถือว่าปลอดภัย ส่วนโหนดที่อยู่ในวัฏจักรหรือเป็นทางนำไปสู่วัฏจักรจะไม่ปลอดภัย
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]การเชื่อมต่อเกินจำเป็นในกราฟไม่มีทิศทาง
การเชื่อมต่อเกินจำเป็น (LeetCode #684) ค้นหาเส้นเชื่อมที่ทำให้เกิดวัฏจักรเมื่อเพิ่มเข้าไปในกราฟไม่มีทิศทางซึ่งเดิมไม่มีวัฏจักร แม้จะแก้ปัญหานี้ด้วยการตรวจจับวัฏจักรด้วย DFS ได้ แต่วิธีที่กระชับที่สุดคือใช้โครงสร้างรวมและค้นหา (DSU): ประมวลผลเส้นเชื่อมทีละเส้น หากปลายทั้งสองของเส้นเชื่อมเชื่อมต่อกันอยู่แล้ว (อยู่ในองค์ประกอบเดียวกัน) เส้นเชื่อมปัจจุบันจะทำให้เกิดวัฏจักรและเป็นคำตอบ DSU ให้ความซับซ้อน O(alpha(n)) ต่อการดำเนินการ ซึ่งมีประสิทธิภาพเทียบเท่า 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]สรุป: กลยุทธ์การตรวจจับวัฏจักร
สรุปเครื่องมือสำหรับการตรวจจับวัฏจักร: สำหรับกราฟไม่มีทิศทาง ให้ใช้ DFS พร้อมติดตามโหนดแม่ หรือใช้โครงสร้างรวมและค้นหา สำหรับกราฟมีทิศทาง ให้ใช้ DFS สามสถานะ (สีขาว/สีเทา/สีดำ) หรือการเรียงลำดับเชิงทอพอโลยีด้วย BFS ของคาห์น ให้เลือกใช้โครงสร้างรวมและค้นหาเมื่อคุณเพิ่มเส้นเชื่อมทีละเส้นแบบต่อเนื่อง ให้เลือกใช้ของคาห์นเมื่อคุณต้องการลำดับเชิงทอพอโลยีด้วย และเลือกใช้ DFS สามสถานะเมื่อคุณต้องการระบุโหนดเฉพาะที่อยู่ในวัฏจักร เมื่อพูดถึงการตรวจจับวัฏจักรในการสัมภาษณ์ ให้ระบุความแตกต่างระหว่างกราฟมีทิศทางและไม่มีทิศทางเสมอ
# 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')ตรวจสอบความเข้าใจอย่างรวดเร็ว
ทดสอบความเข้าใจแนวคิดเรื่องโครงสร้างข้อมูลและอัลกอริทึม — การเตรียมตัวสัมภาษณ์การเขียนโปรแกรมจากบทเรียนนี้
ทบทวนบทเรียน
ในบทเรียนนี้ คุณได้เรียนรู้เรื่อง การตรวจจับวัฏจักรในกราฟไม่มีทิศทางด้วย DFS ที่ติดตามโหนดแม่ การตรวจจับวัฏจักรในกราฟมีทิศทางด้วยการระบายสีสามสถานะ ได้แก่ สีขาว/สีเทา/สีดำ ทางเลือก BFS ของคาห์นสำหรับกราฟมีทิศทาง และการประยุกต์ใช้ต่าง ๆ เช่น ตารางเรียน การเชื่อมต่อเกินจำเป็น และสถานะที่ปลอดภัยในท้ายที่สุด บทถัดไปเราจะเจาะลึกพื้นฐานการเขียนโปรแกรมพลวัต
คำถามที่พบบ่อย
บทเรียน “การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง” ฟรีหรือไม่
ใช่ — ข้อความเต็มของ “การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Coding Interview Prep ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Coding Interview Prep มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง”
ตรวจจับวัฏจักรในกราฟไม่มีทิศทางด้วยการติดตามโหนดแม่ และในกราฟมีทิศทางด้วยการระบายสีสถานะของ DFS (ขาว/เทา/ดำ สามสถานะของการเยี่ยมชม) คุณปฏิบัติ Coding Interview Prep ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Coding Interview Prep หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน Coding Interview Prep บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 4 จากทั้งหมด 4 บทเรียน
บทเรียน “การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน Coding Interview Prep นี้ได้ไหม
ได้ บทเรียน Coding Interview Prep ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การแทนกราฟและการตั้งค่าการท่อง
- BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ
- DFS: องค์ประกอบเชื่อมต่อและการเติมพื้นที่
- การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง