Course Schedule II (LeetCode 210)
You have numCourses courses to complete, numbered 0 through numCourses - 1, and a list prerequisites where each entry [ai, bi] means course bi must be completed before course ai.
- So an entry
[0, 1]means course1has to be finished before course0.
Produce one valid order in which all courses can be taken; any correct order is acceptable. If no order can complete every course, return an empty list instead.
Worked Examples
numCourses = 2, prerequisites = [[1,0]][0,1]numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]][0,2,1,3]numCourses = 1, prerequisites = [][0][0].⚖️Formal Constraints & Bounds
1 <= numCourses <= 20000 <= prerequisites.length <= numCourses * (numCourses - 1)prerequisites[i].length == 20 <= ai, bi < numCoursesai != biAll the pairs
[ai, bi]are distinct
Core Insight
Kahn's algorithm detects cycles naturally: if the finalized order length is less than numCourses, a cycle prevented some in-degrees from reaching 0.
Real-World Scenario & Production Applications
Build dependency task runners (e.g. Bazel/Make/npm package dependency resolvers) and university course curriculum scheduling.
Subproblems & Recurrence Decomposition3 Phases
Construct directed graph from edge pairs and compute in-degree for every vertex: in_degree[v] = number of prerequisite dependencies.
adj = defaultdict(list)
in_degree = [0] * numCourses
for dest, src in prerequisites:
adj[src].append(dest)
in_degree[dest] += 1Step-by-Step Execution Trace Table
| Step | Queue Before | Dequeued Course | Order Appended | Decremented In-Degrees | Newly Freed (In-degree 0) | Queue After |
|---|---|---|---|---|---|---|
| 1 | [0] | 0 | [0] | in_degree[1]: 1→0, in_degree[2]: 1→0 | 1, 2 | [1, 2] |
| 2 | [1, 2] | 1 | [0, 1] | in_degree[3]: 2→1 | None (3 still waits on 2) | [2] |
| 3 | [2] | 2 | [0, 1, 2] | in_degree[3]: 1→0 | 3 | [3] |
| 4 | [3] | 3 | [0, 1, 2, 3] | — | None | [] |
numCourses = 2, prerequisites = [[1,0]][0,1]| Step | Queue Before | Dequeued Course | Order Appended | Decremented In-Degrees | Newly Freed (In-degree 0) | Queue After |
|---|---|---|---|---|---|---|
| 1 | [0] | 0 | [0] | in_degree[1]: 1→0, in_degree[2]: 1→0 | 1, 2 | [1, 2] |
| 2 | [1, 2] | 1 | [0, 1] | in_degree[3]: 2→1 | None (3 still waits on 2) | [2] |
| 3 | [2] | 2 | [0, 1, 2] | in_degree[3]: 1→0 | 3 | [3] |
| 4 | [3] | 3 | [0, 1, 2, 3] | — | None | [] |
| 1 | for dest, src in prerequisites: |
| 2 | adj[src].append(dest) |
| 3 | in_degree[dest] += 1 |
| 4 | queue = deque(every course with in_degree 0) |
| 5 | order = [] |
| 6 | while queue: |
| 7 | node = queue.popleft() |
| 8 | order.append(node) |
| 9 | for neighbor in adj[node]: |
| 10 | in_degree[neighbor] -= 1 |
| 11 | if in_degree[neighbor] == 0: |
| 12 | queue.append(neighbor) |
| 13 | return order if len(order) == numCourses else [] |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Topological sorting orders vertices in a DAG(Directed Acyclic Graph) such that for every directed edge , vertex comes before . Kahn's algorithm maintains in-degrees for all nodes. Nodes with in-degree 0 have no prerequisites and are enqueued. As each node is scheduled, outgoing edges are removed. If the final ordered list contains all courses, a valid curriculum order is returned; if not, a circular prerequisite deadlock exists.
📋 The Analogy: Peeling Off Tasks With No Blockers
Imagine a to-do list where every task lists which other tasks must finish first. You scan the whole list once and pull out every task that has zero blockers — those can start immediately, in any order. Every time you finish a task, you go tell everyone who was waiting on it "one of your blockers is done" — and the moment a waiting task's blocker count hits zero, it joins the ready pile too. If you ever run out of ready tasks while some are still stuck waiting, those remaining tasks must be blocking each other in a cycle — an impossible schedule.
🔑 The Key Idea in the Code
len(order) == num_nodes at the end is the cycle detector: if every node were eventually reachable, all of them would eventually hit zero in-degree and get processed — anything left unprocessed must be stuck in a mutual dependency loop.
Silent partial order return on directed cycles: Returning the accumulated
orderarray without verifyinglen(order) == numCourses. If a cycle exists, Kahn's algorithm terminates early; returning a partial order fails the problem requirement to return an empty array[].Reversed prerequisite dependency edge: For prerequisite pair
[a, b], course must be taken before . The directed edge is , and the in-degree of must increase. Reversing this () generates an inverted topological order.Disconnected component queue seeding: Only enqueueing course 0 instead of finding all courses with
in_degree[i] == 0. Unconnected courses or multiple entry points will never be processed.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Each pair [ai, bi] says course bi must be completed "before" course ai, and you must produce "one valid order" for all courses, or an empty list when "no order can complete every course". Prerequisites plus an order plus "impossible" point to Topological Sort.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Every node in order or in queue comes after all of its prerequisites, and in_degree[v] counts the prerequisites of v not yet in order. The decision: node joins queue at the moment in_degree[node] reaches 0.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
return order if len(order) == num_courses else []: courses on a cycle never reach 0, so a shorterordermeans no valid order exists; returning the partialorderis wrong.for dest, src in prerequisites:[a, b]meansbbeforea, so the edge isb -> a(adj[src].append(dest)) andin_degree[dest]goes up; reversed, the order comes out backwards.queue = deque([i for i in range(num_courses) if in_degree[i] == 0]): seed every course with no prerequisites, not just course 0, or courses in other groups never start.
The 60-Second Interview Pitch
Say this out loud before you type a single line
This is Topological Sort with Kahn's algorithm. Each pair b before a is an edge from b to a, so I build the adjacency list and count every course's in-degree, the prerequisites it still waits for. I put every course with in-degree zero in a queue. Each time I pop a course, I add it to the order and lower the count of every course it unlocks; a course whose count reaches zero joins the queue. That is correct because a course only enters the queue once all its prerequisites are already in the order. The trap is the cycle: courses on a cycle never reach zero, so if the order is shorter than the number of courses I return an empty list, never the partial order. Every course is queued once and every prerequisite is read twice, so it's O(V plus E) time and space.
Complexity & Mathematical Proof
O(V + E)
Look at the code: for dest, src in prerequisites reads each of the E pairs once to build adj and in_degree, O(E); seeding queue scans the V counts, O(V). In while queue, every course is popped at most once (it is pushed only when its count reaches 0, which happens once), and for neighbor in adj[node] walks each edge once over the whole run. Total O(V + E).
O(V + E)
adj stores every edge, O(V + E); in_degree, queue and order hold at most V entries each, O(V). There is no recursion, so the call stack is O(1). order is also the output, V entries.
T(V, E) = E (build) + V (seed) + V (pops) + E (count updates) = O(V + E)
Derivation Progression
for dest, src in prerequisites: adj[src].append(dest); in_degree[dest] += 1 → E steps
Each prerequisite pair is read once and does O(1) work.
deque([i for i in range(num_courses) if in_degree[i] == 0]) → V checks
One pass over the V counts.
while queue: node = queue.popleft(); for neighbor in adj[node]: in_degree[neighbor] -= 1 → V pops + E decrements
A course is pushed only when its count reaches 0, once, so there are at most V pops; across all pops the inner loop walks each edge once.
Variable Definitions
Number of courses, num_courses (nodes)
Number of prerequisite pairs (edges)
Memory Architecture & Bounds
O(1): no recursion
O(V + E): adj holds every edge; in_degree and queue hold V entries
O(V): order
Boundary Best / Worst Cases
Graph & Tree Traversal Frontier
Senior SWE Deconstruction & Hardware Caveats
Triggers: "must be completed before", "one valid order", "return an empty list" when no order exists. An order that respects every prerequisite, plus a report of impossibility: Topological Sort. At scale the same shape is any dependency graph that has to be processed in order: builds, migrations, task pipelines.
courses, pairs. Budget: steps and memory for adj (every edge) plus three arrays of . On a graph too large for one machine, adj is the part to shard; in_degree is one counter per node.
Nodes left out of order are exactly the nodes on or behind a cycle: report them (not just "impossible") so the cycle can be found. A node listed as a prerequisite but never defined is a separate error to check before sorting.
Core Algorithmic State Invariants
- Any Valid Order Is Accepted
LC 210 accepts any order that respects every prerequisite. Kahn's queue gives one of them; which one depends on the order courses enter queue, so a different but valid answer is still correct.
- Popped Order Is Also a Schedule of Rounds
The courses in queue at one moment have no prerequisite between them. Processing queue one level at a time (as in Level-Order BFS) gives the fewest rounds when courses of one round can be taken together.
Course Schedule II (LeetCode 210)
You have numCourses courses to complete, numbered 0 through numCourses - 1, and a list prerequisites where each entry [ai, bi] means course bi must be completed before course ai.
- So an entry
[0, 1]means course1has to be finished before course0.
Produce one valid order in which all courses can be taken; any correct order is acceptable. If no order can complete every course, return an empty list instead.
Worked Examples
numCourses = 2, prerequisites = [[1,0]][0,1]numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]][0,2,1,3]numCourses = 1, prerequisites = [][0][0].⚖️Formal Constraints & Bounds
1 <= numCourses <= 20000 <= prerequisites.length <= numCourses * (numCourses - 1)prerequisites[i].length == 20 <= ai, bi < numCoursesai != biAll the pairs
[ai, bi]are distinct
Core Insight
Kahn's algorithm detects cycles naturally: if the finalized order length is less than numCourses, a cycle prevented some in-degrees from reaching 0.
Real-World Scenario & Production Applications
Build dependency task runners (e.g. Bazel/Make/npm package dependency resolvers) and university course curriculum scheduling.
Subproblems & Recurrence Decomposition3 Phases
Construct directed graph from edge pairs and compute in-degree for every vertex: in_degree[v] = number of prerequisite dependencies.
adj = defaultdict(list)
in_degree = [0] * numCourses
for dest, src in prerequisites:
adj[src].append(dest)
in_degree[dest] += 1Step-by-Step Execution Trace Table
| Step | Queue Before | Dequeued Course | Order Appended | Decremented In-Degrees | Newly Freed (In-degree 0) | Queue After |
|---|---|---|---|---|---|---|
| 1 | [0] | 0 | [0] | in_degree[1]: 1→0, in_degree[2]: 1→0 | 1, 2 | [1, 2] |
| 2 | [1, 2] | 1 | [0, 1] | in_degree[3]: 2→1 | None (3 still waits on 2) | [2] |
| 3 | [2] | 2 | [0, 1, 2] | in_degree[3]: 1→0 | 3 | [3] |
| 4 | [3] | 3 | [0, 1, 2, 3] | — | None | [] |
numCourses = 2, prerequisites = [[1,0]][0,1]| Step | Queue Before | Dequeued Course | Order Appended | Decremented In-Degrees | Newly Freed (In-degree 0) | Queue After |
|---|---|---|---|---|---|---|
| 1 | [0] | 0 | [0] | in_degree[1]: 1→0, in_degree[2]: 1→0 | 1, 2 | [1, 2] |
| 2 | [1, 2] | 1 | [0, 1] | in_degree[3]: 2→1 | None (3 still waits on 2) | [2] |
| 3 | [2] | 2 | [0, 1, 2] | in_degree[3]: 1→0 | 3 | [3] |
| 4 | [3] | 3 | [0, 1, 2, 3] | — | None | [] |
| 1 | for dest, src in prerequisites: |
| 2 | adj[src].append(dest) |
| 3 | in_degree[dest] += 1 |
| 4 | queue = deque(every course with in_degree 0) |
| 5 | order = [] |
| 6 | while queue: |
| 7 | node = queue.popleft() |
| 8 | order.append(node) |
| 9 | for neighbor in adj[node]: |
| 10 | in_degree[neighbor] -= 1 |
| 11 | if in_degree[neighbor] == 0: |
| 12 | queue.append(neighbor) |
| 13 | return order if len(order) == numCourses else [] |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Topological sorting orders vertices in a DAG(Directed Acyclic Graph) such that for every directed edge , vertex comes before . Kahn's algorithm maintains in-degrees for all nodes. Nodes with in-degree 0 have no prerequisites and are enqueued. As each node is scheduled, outgoing edges are removed. If the final ordered list contains all courses, a valid curriculum order is returned; if not, a circular prerequisite deadlock exists.
📋 The Analogy: Peeling Off Tasks With No Blockers
Imagine a to-do list where every task lists which other tasks must finish first. You scan the whole list once and pull out every task that has zero blockers — those can start immediately, in any order. Every time you finish a task, you go tell everyone who was waiting on it "one of your blockers is done" — and the moment a waiting task's blocker count hits zero, it joins the ready pile too. If you ever run out of ready tasks while some are still stuck waiting, those remaining tasks must be blocking each other in a cycle — an impossible schedule.
🔑 The Key Idea in the Code
len(order) == num_nodes at the end is the cycle detector: if every node were eventually reachable, all of them would eventually hit zero in-degree and get processed — anything left unprocessed must be stuck in a mutual dependency loop.
Silent partial order return on directed cycles: Returning the accumulated
orderarray without verifyinglen(order) == numCourses. If a cycle exists, Kahn's algorithm terminates early; returning a partial order fails the problem requirement to return an empty array[].Reversed prerequisite dependency edge: For prerequisite pair
[a, b], course must be taken before . The directed edge is , and the in-degree of must increase. Reversing this () generates an inverted topological order.Disconnected component queue seeding: Only enqueueing course 0 instead of finding all courses with
in_degree[i] == 0. Unconnected courses or multiple entry points will never be processed.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Each pair [ai, bi] says course bi must be completed "before" course ai, and you must produce "one valid order" for all courses, or an empty list when "no order can complete every course". Prerequisites plus an order plus "impossible" point to Topological Sort.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Every node in order or in queue comes after all of its prerequisites, and in_degree[v] counts the prerequisites of v not yet in order. The decision: node joins queue at the moment in_degree[node] reaches 0.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
return order if len(order) == num_courses else []: courses on a cycle never reach 0, so a shorterordermeans no valid order exists; returning the partialorderis wrong.for dest, src in prerequisites:[a, b]meansbbeforea, so the edge isb -> a(adj[src].append(dest)) andin_degree[dest]goes up; reversed, the order comes out backwards.queue = deque([i for i in range(num_courses) if in_degree[i] == 0]): seed every course with no prerequisites, not just course 0, or courses in other groups never start.
The 60-Second Interview Pitch
Say this out loud before you type a single line
This is Topological Sort with Kahn's algorithm. Each pair b before a is an edge from b to a, so I build the adjacency list and count every course's in-degree, the prerequisites it still waits for. I put every course with in-degree zero in a queue. Each time I pop a course, I add it to the order and lower the count of every course it unlocks; a course whose count reaches zero joins the queue. That is correct because a course only enters the queue once all its prerequisites are already in the order. The trap is the cycle: courses on a cycle never reach zero, so if the order is shorter than the number of courses I return an empty list, never the partial order. Every course is queued once and every prerequisite is read twice, so it's O(V plus E) time and space.
Complexity & Mathematical Proof
O(V + E)
Look at the code: for dest, src in prerequisites reads each of the E pairs once to build adj and in_degree, O(E); seeding queue scans the V counts, O(V). In while queue, every course is popped at most once (it is pushed only when its count reaches 0, which happens once), and for neighbor in adj[node] walks each edge once over the whole run. Total O(V + E).
O(V + E)
adj stores every edge, O(V + E); in_degree, queue and order hold at most V entries each, O(V). There is no recursion, so the call stack is O(1). order is also the output, V entries.
T(V, E) = E (build) + V (seed) + V (pops) + E (count updates) = O(V + E)
Derivation Progression
for dest, src in prerequisites: adj[src].append(dest); in_degree[dest] += 1 → E steps
Each prerequisite pair is read once and does O(1) work.
deque([i for i in range(num_courses) if in_degree[i] == 0]) → V checks
One pass over the V counts.
while queue: node = queue.popleft(); for neighbor in adj[node]: in_degree[neighbor] -= 1 → V pops + E decrements
A course is pushed only when its count reaches 0, once, so there are at most V pops; across all pops the inner loop walks each edge once.
Variable Definitions
Number of courses, num_courses (nodes)
Number of prerequisite pairs (edges)
Memory Architecture & Bounds
O(1): no recursion
O(V + E): adj holds every edge; in_degree and queue hold V entries
O(V): order
Boundary Best / Worst Cases
Graph & Tree Traversal Frontier
Senior SWE Deconstruction & Hardware Caveats
Triggers: "must be completed before", "one valid order", "return an empty list" when no order exists. An order that respects every prerequisite, plus a report of impossibility: Topological Sort. At scale the same shape is any dependency graph that has to be processed in order: builds, migrations, task pipelines.
courses, pairs. Budget: steps and memory for adj (every edge) plus three arrays of . On a graph too large for one machine, adj is the part to shard; in_degree is one counter per node.
Nodes left out of order are exactly the nodes on or behind a cycle: report them (not just "impossible") so the cycle can be found. A node listed as a prerequisite but never defined is a separate error to check before sorting.
Core Algorithmic State Invariants
- Any Valid Order Is Accepted
LC 210 accepts any order that respects every prerequisite. Kahn's queue gives one of them; which one depends on the order courses enter queue, so a different but valid answer is still correct.
- Popped Order Is Also a Schedule of Rounds
The courses in queue at one moment have no prerequisite between them. Processing queue one level at a time (as in Level-Order BFS) gives the fewest rounds when courses of one round can be taken together.