Course Schedule II – Solution & Complexity

Solution Walkthrough

  1. Step 1: Recognize the graph problem

    • Every course is a vertex and every prerequisite pair is a directed edge.
    • We need a topological ordering: every prerequisite must appear before the course that depends on it.
  2. Step 2: Brute-force by repeatedly scanning prerequisites

    • In each round, search for courses whose prerequisites are already taken.
    • This is correct but can revisit the full prerequisite list many times, giving roughly O(V * E) work.
    def find_order(numCourses, prerequisites):
        taken = [False] * numCourses
        order = []
        while len(order) < numCourses:
            progress = False
            for course in range(numCourses):
                if taken[course]:
                    continue
                ready = True
                for target, prerequisite in prerequisites:
                    if target == course and not taken[prerequisite]:
                        ready = False
                        break
                if ready:
                    taken[course] = True
                    order.append(course)
                    progress = True
            if not progress:
                return []
        return order
  3. Step 3: Use indegrees to avoid repeated rescans

    • Precompute each course's indegree and adjacency list once.
    • Then each edge is processed exactly once when its prerequisite is removed from the queue.
  4. Step 4: Run Kahn's algorithm with a FIFO queue

    • Seed the queue with every course whose indegree is zero.
    • Pop courses, append them to the answer, and decrement the indegree of their outgoing neighbors.
    def find_order(numCourses, prerequisites):
        graph = [[] for _ in range(numCourses)]
        indegree = [0] * numCourses
        for course, prerequisite in prerequisites:
            graph[prerequisite].append(course)
            indegree[course] += 1
        queue = [course for course in range(numCourses) if indegree[course] == 0]
        head = 0
        order = []
        while head < len(queue):
            course = queue[head]
            head += 1
            order.append(course)
            for nxt in graph[course]:
                indegree[nxt] -= 1
                if indegree[nxt] == 0:
                    queue.append(nxt)
        return order if len(order) == numCourses else []
  5. Step 5: Dry run / queue trace

    Trace numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[1,2]].

    stepqueue before poppoppedindegree updatesorder
    start[0]-indegrees = [0,2,1,1][]
    1[0]01 -> 1, 2 -> 0, enqueue 2[0]
    2[0,2]21 -> 0, enqueue 1[0,2]
    3[0,2,1]13 -> 0, enqueue 3[0,2,1]
    4[0,2,1,3]3none[0,2,1,3]

    Every course appears exactly once, so the graph is acyclic and the built order is valid.

  6. Step 6: Final solution and complexity

    Kahn's algorithm processes each course and prerequisite edge once, giving O(V + E) time with O(V + E) graph storage.

    def find_order(numCourses: int, prerequisites: list[list[int]]) -> list[int]:
        graph = [[] for _ in range(numCourses)]
        indegree = [0] * numCourses
        for course, prerequisite in prerequisites:
            graph[prerequisite].append(course)
            indegree[course] += 1
        queue = [course for course in range(numCourses) if indegree[course] == 0]
        head = 0
        order: list[int] = []
        while head < len(queue):
            course = queue[head]
            head += 1
            order.append(course)
            for nxt in graph[course]:
                indegree[nxt] -= 1
                if indegree[nxt] == 0:
                    queue.append(nxt)
        return order if len(order) == numCourses else []

FAQ