LeetCode #207 Medium

Course Schedule

Course Schedule is LeetCode 207 (Medium). You have to take numCourses courses, labelled 0 to numCourses - 1. The list prerequisites holds pairs [a, b], and each pair means course b must be finished before course a can start.

  • Return true if some order of the courses satisfies every prerequisite.
  • Return false if the course prerequisites make that impossible.

You only report whether an order exists; producing it is the follow-up problem. With up to 2,000 courses and 5,000 pairs, the solution should be linear in courses plus pairs.

Constraints
  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i].length == 2
  • 0 <= ai, bi < numCourses
  • All the pairs prerequisites[i] are unique.
graphstopological-sortbfscycle-detection
Open on LeetCode ↗
02

Intuition

Course Schedule is cycle detection in disguise. Draw each course as a node and each prerequisite [a, b] as an arrow b → a. If the arrows form a cycle, each course on it waits on another one on it, so none can ever be taken first and the answer is false.

If there is no cycle, some course always has no unmet prerequisite. Take it, remove its arrows, and another course becomes free; repeating this takes every course. That peeling order is the course schedule topological sort, and Kahn's algorithm performs it by tracking how many prerequisites each course is still waiting on.

How to spot this pattern

Tasks with "must come before" rules are a directed graph, and "can all tasks be done?" is "is the graph acyclic?". Build systems, package installs, spreadsheet formulas and job pipelines all reduce to this. If the question asks for an actual order, it is Course Schedule II (210), the same code returning the order list.

03

Approach

Try it first

Before reading on, decide which way the arrow for [a, b] should point, and what number a course needs to reach before it can be taken. Aim for O(V + E).

1

Build the graph and in-degrees

For each pair [course, pre]:

  • graph[pre].append(course): the arrow goes from the prerequisite to the course it unlocks.
  • indegree[course] += 1: one more prerequisite that course is waiting on.
2

Seed the queue with free courses

Every course with indegree == 0 can be taken immediately. Put all of them in a queue. If none exist, every course waits on another and there must be a cycle. In the course schedule Python code the queue is a collections.deque, so popping from the front is O(1).

3

Take courses and release their dependents

  • Pop a course and add 1 to taken.
  • For each course it unlocks, decrement that course's in-degree.
  • When an in-degree reaches 0, all its prerequisites are done: push it onto the queue.
4

Why counting taken courses detects a cycle

Return taken == numCourses. Courses on a cycle never reach in-degree 0, because the arrow into the cycle's first course comes from a course that is itself stuck. So they are never taken, and the count falls short.

04

Course Schedule solution in Python | C++ | Java

▶1class Solution:
▶2 def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
▶3 graph = [[] for _ in range(numCourses)]
▶4 indegree = [0] * numCourses
▶5 for course, pre in prerequisites:
▶6 graph[pre].append(course)
▶7 indegree[course] += 1
▶8 
▶9 queue = deque(c for c in range(numCourses) if indegree[c] == 0)
▶10 taken = 0
▶11 while queue:
▶12 node = queue.popleft()
▶13 taken += 1
▶14 for nxt in graph[node]:
▶15 indegree[nxt] -= 1
▶16 if indegree[nxt] == 0:
▶17 queue.append(nxt)
▶18 return taken == numCourses
precourse0in 01in 12in 13in 24in 1queueemptytaken0 / 5emptybuild graph + in-degrees
courses5numbered 0 to 4
in-degree[0, 1, 1, 2, 1]prerequisites still unmet
Model the courses as a directed graph. A pair [a, b] means b must come before a, so draw the arrow b → a. A course's in-degree is how many prerequisites it still waits on. The schedule is possible exactly when this graph has no cycle.
precourse0in 01in 12in 13in 24in 1queue0taken0 / 5emptyin-degree 0 → queue
queue[0]can be taken right now
taken0
Courses with in-degree 0 have no unmet prerequisites, so they can be taken first: 0. Only these seed the queue; every other course has to wait for its in-degree to fall to 0.
precourse01in 12in 13in 24in 1queueemptytaken1 / 50take course 0
node0popped from the front
taken1of 5
Take course 0: it was in the queue, so all its prerequisites are done. The count rises to 1. It is a prerequisite of 1, 2, so those are next to update.
precourse01in 02in 03in 24in 1queue12taken1 / 50remove 0's arrows · 1, 2 → queue
decremented1, 2in-degree − 1 each
queue[1, 2]added 1, 2
Course 0 is done, so erase its outgoing arrows: 1 drops to 0, 2 drops to 0. 1, 2 reached 0 and join the queue.
precourse012in 03in 24in 1queue2taken2 / 501take course 1
node1popped from the front
taken2of 5
Take course 1: it was in the queue, so all its prerequisites are done. The count rises to 2. It is a prerequisite of 3, so those are next to update.
precourse012in 03in 14in 1queue2taken2 / 501remove 1's arrows · none freed
decremented3in-degree − 1 each
queue[2]no change
Course 1 is done, so erase its outgoing arrows: 3 drops to 1. None reached 0, so it still waits on another prerequisite.
precourse0123in 14in 1queueemptytaken3 / 5012take course 2
node2popped from the front
taken3of 5
Take course 2: it was in the queue, so all its prerequisites are done. The count rises to 3. It is a prerequisite of 3, so those are next to update.
precourse0123in 04in 1queue3taken3 / 5012remove 2's arrows · 3 → queue
decremented3in-degree − 1 each
queue[3]added 3
Course 2 is done, so erase its outgoing arrows: 3 drops to 0. 3 reached 0 and joins the queue.
precourse01234in 1queueemptytaken4 / 50123take course 3
node3popped from the front
taken4of 5
Take course 3: it was in the queue, so all its prerequisites are done. The count rises to 4. It is a prerequisite of 4, so those are next to update.
precourse01234in 0queue4taken4 / 50123remove 3's arrows · 4 → queue
decremented4in-degree − 1 each
queue[4]added 4
Course 3 is done, so erase its outgoing arrows: 4 drops to 0. 4 reached 0 and joins the queue.
precourse01234queueemptytaken5 / 501234take course 4
node4popped from the front
taken5of 5
Take course 4: it was in the queue, so all its prerequisites are done. The count rises to 5. No course depends on it, so there is nothing to update.
precourse01234queueemptytaken5 / 501234all 5 taken → return true
taken5numCourses = 5
resulttruetaken == numCourses
All 5 courses taken, in the order 0 → 1 → 2 → 3 → 4. That order is a topological sort: every arrow points forward in it. No cycle, so the answer is true.
05

Common pitfalls

Pointing the arrow the wrong way

✗ Wrong
for course, pre in prerequisites:
    graph[course].append(pre)
    indegree[course] += 1
✓ Right
for course, pre in prerequisites:
    graph[pre].append(course)
    indegree[course] += 1

The edge list and the in-degree must agree on direction. Mixed up, taking a course lowers the in-degree of its own prerequisite, and valid schedules get reported as impossible.

Using a plain visited set in the DFS version

✗ Wrong
if node in visited:
    return False  # treated as a cycle
✓ Right
if state[node] == VISITING:
    return False  # back edge: a real cycle
if state[node] == DONE:
    return True

In a directed graph a node can be reached twice without a cycle, as in a diamond 0→1→3, 0→2→3. Only a node still on the current DFS path means a cycle, so DFS needs three states, not two.

Forgetting courses with no prerequisites

✗ Wrong
graph = {}
for course, pre in prerequisites:
    graph.setdefault(pre, []).append(course)
✓ Right
graph = [[] for _ in range(numCourses)]

Courses that appear in no pair still count. Building nodes only from the pairs leaves them out, so taken can never reach numCourses.

06

Edge cases

Self-loop [a, a]

Course a waits on itself, so its in-degree never drops to 0. The count falls short: false.

Disconnected groups

The seed step picks up the free courses of every group at once; no separate loop per component is needed.

07

Complexity

Time
O(V + E)
Space
O(V + E)
V is numCourses and E is the number of prerequisite pairs. This course schedule LeetCode solution is linear in both: every course enters the queue at most once and every edge is relaxed once. The adjacency list holds E entries.
08

Kahn's BFS vs DFS cycle detection

Both are standard answers to LeetCode 207 and run in the same time. They differ in what state they keep and what they give you for free.

Kahn's algorithm (BFS)DFS with three colours
State per coursein-degree countunvisited / visiting / done
Cycle signaltaken < numCourses at the endreaching a node that is still visiting
Order for freeyes: the pop order (Course Schedule II)yes: reverse post-order
Recursionnonedepth up to V; can overflow on long chains
Easiest to explain in an interviewcounting prerequisitesback edges
09

Course Schedule FAQ

How do you solve Course Schedule (LeetCode 207)?
  • Model: each course is a node; [a, b] is a directed edge b → a.
  • Key fact: all courses can be finished if and only if the graph has no cycle.
  • Algorithm: Kahn's topological sort. Compute in-degrees, queue every course with in-degree 0, pop, count, and decrement each dependent; queue it when it reaches 0.
  • Answer: taken == numCourses.
  • Complexity: O(V + E) time and space.
  • Example: 2 courses with [[1,0],[0,1]] both start at in-degree 1, nothing is ever taken, so the answer is false.
What is the difference between Course Schedule and Course Schedule II?

Course Schedule (207) only asks whether an order exists. Course Schedule II (210) asks for the order itself. The code is the same, except it records each popped course in a list and returns that list, or [] if a cycle leaves courses out.

What is the time complexity of Course Schedule?

O(V + E), where V is the number of courses and E the number of prerequisite pairs. Building the graph touches every pair once, and the BFS processes each course and each edge once.