งูต้นฉบับตรวจแผนผังวิชาบนการ์ดที่มีวงจรอำพันหนึ่งวงตัดกับเส้นทางสีมิ้นต์

LeetCode #207 Course Schedule: ตรวจวงจรด้วย Kahn BFS หรือ DFS สีสามสถานะ

พี่โชว์Engineering

Course Schedule ถามว่าเรียนครบทุกวิชาได้ไหมเมื่อคู่ [course, prerequisite] กำหนด dependency ปัญหานี้เท่ากับตรวจว่า directed graph มีวงจรหรือไม่ ถ้าไม่มีวงจรย่อมมี topological order

วิธี 1: Kahn BFS (แนะนำเป็น default)

นับ indegree ของแต่ละวิชา เริ่ม queue ด้วยวิชาที่ไม่มี prerequisite แล้วค่อยลด indegree ของวิชาถัดไป ถ้าประมวลผลได้ครบ num_courses แปลว่าไม่มีวงจร จุดเด่นคือเปลี่ยนต่อให้คืนลำดับวิชาได้ง่าย

วิธี 2: DFS สีสามสถานะ

สถานะ 0 คือยังไม่เห็น, 1 คืออยู่ใน recursion path, 2 คือจบแล้ว ถ้าเดินไปเจอโหนดสี 1 แปลว่าพบ back edge และวงจร โค้ดสะท้อนนิยามวงจรตรงๆ แต่ graph ยาวมากอาจชน recursion limit ของ Python

from __future__ import annotations

import unittest
from collections import deque


def can_finish_kahn(n: int, prerequisites: list[list[int]]) -> bool:
    graph = [[] for _ in range(n)]
    indegree = [0] * n
    for course, before in prerequisites:
        graph[before].append(course)
        indegree[course] += 1

    queue = deque(i for i, degree in enumerate(indegree) if degree == 0)
    visited = 0
    while queue:
        node = queue.popleft()
        visited += 1
        for nxt in graph[node]:
            indegree[nxt] -= 1
            if indegree[nxt] == 0:
                queue.append(nxt)
    return visited == n


def can_finish_dfs(n: int, prerequisites: list[list[int]]) -> bool:
    graph = [[] for _ in range(n)]
    for course, before in prerequisites:
        graph[before].append(course)
    state = [0] * n

    def acyclic(node: int) -> bool:
        if state[node] == 1:
            return False
        if state[node] == 2:
            return True
        state[node] = 1
        if not all(acyclic(nxt) for nxt in graph[node]):
            return False
        state[node] = 2
        return True

    return all(acyclic(node) for node in range(n))


class CourseScheduleTests(unittest.TestCase):
    def test_both_methods(self):
        cases = [
            (2, [[1, 0]], True),
            (2, [[1, 0], [0, 1]], False),
            (1, [], True),
            (3, [[1, 0], [2, 1], [0, 2]], False),
        ]
        for solve in (can_finish_kahn, can_finish_dfs):
            for n, edges, expected in cases:
                self.assertEqual(solve(n, edges), expected)


if __name__ == "__main__":
    unittest.main()

Decision table

เงื่อนไขKahn BFSDFS สามสี
เวลา / พื้นที่O(V+E) / O(V+E)O(V+E) / O(V+E)
คืน topological orderตรงไปตรงมาต้องเก็บ postorder
หา cycle pathต้องเพิ่ม parent logicต่อได้ง่ายกว่า
graph ลึกมากปลอดภัยกว่าเสี่ยง recursion limit
input ต้อง sortไม่ต้องไม่ต้อง
mutation inputไม่มีไม่มี
streaming edgesต้องสร้าง adjacency/indegreeต้องสร้าง adjacency

ใช้ Kahn เป็น default สำหรับ Python และงานที่อยากได้ลำดับต่อ ใช้ DFS เมื่อทีมคุ้นกับ traversal และต้องต่อยอดหาเส้นทางวงจร

Recap

  • วางทิศ edge จาก prerequisite ไป course ให้ถูก
  • เทียบจำนวนโหนดที่ประมวลผล ไม่ใช่จำนวน edge
  • เทสต์ graph ว่าง, self/cycle และโหนดโดดเดี่ยว
  • เลือก iterative BFS เมื่อ graph อาจลึกมาก