0Pricing
Coding Interview Prep · บทเรียน

การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง

ตรวจจับวัฏจักรในกราฟไม่มีทิศทางด้วยการติดตามโหนดแม่ และในกราฟมีทิศทางด้วยการระบายสีสถานะของ 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 ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การแทนกราฟและการตั้งค่าการท่อง
  2. BFS: เส้นทางสั้นที่สุดและการท่องตามระดับ
  3. DFS: องค์ประกอบเชื่อมต่อและการเติมพื้นที่
  4. การตรวจจับวัฏจักรในกราฟมีทิศทางและไม่มีทิศทาง
← กลับไปที่ Coding Interview Prep