title: Course Schedule II slug: course-schedule-ii difficulty: medium leetcode_id: 210 leetcode_url: https://leetcode.com/problems/course-schedule-ii/ categories: - graphs patterns: - bfs - dfs function_signature: "def find_order(num_courses: int, prerequisites: list[list[int]]) -> list[int]:" test_cases: visible: - input: { num_courses: 2, prerequisites: [[1, 0]] } expected: [0, 1] - input: { num_courses: 4, prerequisites: [[1, 0], [2, 0], [3, 1], [3, 2]] } expected: [0, 1, 2, 3] - input: { num_courses: 1, prerequisites: [] } expected: [0] hidden: - input: { num_courses: 2, prerequisites: [[1, 0], [0, 1]] } expected: [] - input: { num_courses: 3, prerequisites: [] } expected: [0, 1, 2] - input: { num_courses: 3, prerequisites: [[0, 1], [0, 2], [1, 2]] } expected: [2, 1, 0] - input: { num_courses: 4, prerequisites: [[3, 0], [0, 1]] } expected: [1, 2, 0, 3] - input: { num_courses: 2, prerequisites: [] } expected: [0, 1] description: | There are a total of `numCourses` courses you have to take, labelled from `0` to `numCourses - 1`. You are given an array `prerequisites` where `prerequisites[i] = [a_i, b_i]` indicates that you **must** take course `b_i` first if you want to take course `a_i`. For example, the pair `[0, 1]` indicates that to take course `0` you have to first take course `1`. Return *the ordering of courses you should take to finish all courses*. If there are many valid answers, return **any** of them. If it is impossible to finish all courses, return **an empty array**. constraints: | - `1 <= numCourses <= 2000` - `0 <= prerequisites.length <= numCourses * (numCourses - 1)` - `prerequisites[i].length == 2` - `0 <= a_i, b_i < numCourses` - `a_i != b_i` - All the pairs `[a_i, b_i]` are **distinct** examples: - input: "numCourses = 2, prerequisites = [[1,0]]" output: "[0,1]" explanation: "There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1]." - input: "numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]" output: "[0,2,1,3]" explanation: "There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3]." - input: "numCourses = 1, prerequisites = []" output: "[0]" explanation: "There is only one course with no prerequisites, so the order is simply [0]." explanation: intuition: | Imagine you're planning which university courses to take each semester. Some courses have prerequisites — you can't take Advanced Algorithms until you've completed Data Structures. This creates a natural ordering constraint. The problem is asking us to find a **topological ordering** of the courses. Think of it like stacking books: you can only place a book on top of another if all its "dependencies" are already in the stack below. If there's a circular dependency (Book A requires Book B, and Book B requires Book A), it's impossible to stack them — we'd return an empty array. The key insight is recognising this as a **directed graph problem**. Each course is a node, and each prerequisite `[a, b]` creates an edge from `b` to `a` (meaning "b must come before a"). We need to find an ordering where every edge points "forward" — that's exactly what topological sort does. There are two classic approaches: **Kahn's Algorithm (BFS)** processes courses with no remaining prerequisites first, while **DFS-based** approach explores deeply and adds courses to the result in reverse post-order. approach: | We'll use **Kahn's Algorithm (BFS)** — it's intuitive and naturally handles cycle detection: **Step 1: Build the graph and calculate in-degrees** - Create an adjacency list to represent the graph: for each prerequisite `[a, b]`, add `a` to the list of courses that depend on `b` - Calculate the **in-degree** of each course: how many prerequisites it has - In-degree tells us how many courses must be completed before we can take this one   **Step 2: Initialise the queue with "ready" courses** - Add all courses with in-degree `0` to a queue — these have no prerequisites and can be taken immediately - These are our starting points   **Step 3: Process courses level by level (BFS)** - Dequeue a course, add it to the result - For each course that depends on the dequeued course, decrement its in-degree - If any course's in-degree becomes `0`, it's now "unlocked" — add it to the queue - Repeat until the queue is empty   **Step 4: Check for cycles** - If the result contains all `numCourses` courses, return it - If not, a cycle exists (some courses could never reach in-degree `0`) — return an empty array   This approach works because we only process a course when all its prerequisites have been processed, naturally building a valid ordering. common_pitfalls: - title: Cycle Detection Failure description: | The most critical edge case is detecting **cycles** in the prerequisites. If course A requires B, B requires C, and C requires A, no valid ordering exists. With Kahn's Algorithm, cycles are detected automatically: courses in a cycle never reach in-degree `0`, so they're never added to the queue. If our result has fewer than `numCourses` entries, we know there's a cycle. Forgetting this check would return an incomplete ordering instead of an empty array. wrong_approach: "Return partial ordering without checking length" correct_approach: "Verify result length equals numCourses" - title: Confusing Edge Direction description: | The prerequisite `[a, b]` means "b must come before a" — the edge goes FROM `b` TO `a`. Getting this backwards inverts the entire dependency graph. Think of it as: "to take course `a`, you need course `b` first." So `b` points to `a` (b enables a). wrong_approach: "Add edge from a to b" correct_approach: "Add edge from b to a (prerequisite points to dependent)" - title: Handling Disconnected Components description: | Some courses might have no prerequisites AND no courses depending on them (isolated nodes). These must still appear in the result. Initialising in-degrees for all courses from `0` to `numCourses - 1` (not just those in prerequisites) ensures isolated courses start with in-degree `0` and get processed. wrong_approach: "Only track courses mentioned in prerequisites" correct_approach: "Initialise in-degree array for all numCourses courses" - title: Multiple Valid Orderings description: | The problem states any valid ordering is acceptable. Don't over-engineer to find a "canonical" order — the first valid topological sort you find works. If courses 1 and 2 both have in-degree 0 at the same time, either can come first in the result. key_takeaways: - "**Topological sort** produces a linear ordering of vertices in a DAG where every edge points from earlier to later in the sequence" - "**Kahn's Algorithm** uses BFS with in-degrees: start with in-degree 0 nodes, decrement neighbours' in-degrees, repeat" - "**Cycle detection** is built-in: if the result has fewer nodes than the graph, a cycle exists" - "This pattern applies to **dependency resolution** problems: build systems, task scheduling, package managers" time_complexity: "O(V + E). We process each course (vertex) once and examine each prerequisite (edge) once when decrementing in-degrees. Here V = `numCourses` and E = `len(prerequisites)`." space_complexity: "O(V + E). The adjacency list stores all edges, and we use arrays of size V for in-degrees and the result. The queue holds at most V courses." solutions: - approach_name: Kahn's Algorithm (BFS) is_optimal: true code: | from collections import deque def find_order(num_courses: int, prerequisites: list[list[int]]) -> list[int]: # Build adjacency list and calculate in-degrees graph = [[] for _ in range(num_courses)] in_degree = [0] * num_courses for course, prereq in prerequisites: graph[prereq].append(course) # prereq -> course edge in_degree[course] += 1 # course has one more prerequisite # Start with courses that have no prerequisites queue = deque() for course in range(num_courses): if in_degree[course] == 0: queue.append(course) result = [] # Process courses in topological order while queue: current = queue.popleft() result.append(current) # "Unlock" courses that depended on current for next_course in graph[current]: in_degree[next_course] -= 1 if in_degree[next_course] == 0: queue.append(next_course) # If we processed all courses, return the order; otherwise cycle exists return result if len(result) == num_courses else [] explanation: | **Time Complexity:** O(V + E) — Each course is enqueued/dequeued once, and each prerequisite edge is processed once. **Space Complexity:** O(V + E) — Adjacency list stores all edges, plus O(V) for in-degrees, queue, and result. Kahn's Algorithm processes courses in dependency order. By tracking in-degrees, we know exactly when a course becomes "ready" (all prerequisites completed). The BFS naturally produces a valid topological ordering. - approach_name: DFS with Cycle Detection is_optimal: true code: | def find_order(num_courses: int, prerequisites: list[list[int]]) -> list[int]: # Build adjacency list graph = [[] for _ in range(num_courses)] for course, prereq in prerequisites: graph[prereq].append(course) # States: 0 = unvisited, 1 = visiting (in current path), 2 = visited state = [0] * num_courses result = [] def dfs(course: int) -> bool: """Returns True if no cycle detected, False if cycle found.""" if state[course] == 1: # Currently visiting -> cycle! return False if state[course] == 2: # Already processed return True state[course] = 1 # Mark as visiting for next_course in graph[course]: if not dfs(next_course): return False state[course] = 2 # Mark as fully processed result.append(course) # Add in reverse post-order return True # Run DFS from each unvisited course for course in range(num_courses): if state[course] == 0: if not dfs(course): return [] # Result is in reverse order (we added nodes after processing children) return result[::-1] explanation: | **Time Complexity:** O(V + E) — Each course is visited once, and each edge is traversed once. **Space Complexity:** O(V + E) — Adjacency list plus O(V) recursion stack in worst case. DFS explores each course deeply, adding it to the result only after all its dependents are processed. The three-state system (unvisited, visiting, visited) detects cycles: if we revisit a node that's currently in our path, we've found a cycle. The final result is reversed because we add nodes in post-order (after children).