Skip to content

Coding patternsBlind 75

Course Schedule

Count each course's missing prerequisites and take courses as their count reaches zero; if some are never taken, they sit on a cycle. Kahn's algorithm in O(V + E) time and space, with a step-through, code in five languages and the Big O worked out from the code.

There are numCourses courses, numbered 0 to numCourses - 1. Each pair [course, before] in prerequisites says that course before must be taken before course course. Return true if there is an order that takes every course, and false if there is not.

Example 1

Input: numCourses = 5, prerequisites = [[0, 1], [0, 2], [1, 3], [2, 3], [4, 0]]

Output: true

Each arrow points from a course to one that needs it. Take 3, then 1 and 2, then 0, then 4.

01234

Example 2

Input: numCourses = 2, prerequisites = [[0, 1], [1, 0]]

Output: false

Course 0 needs 1, and 1 needs 0. Neither can go first.

Example 3

Input: numCourses = 3, prerequisites = []

Output: true

No rules at all, so any order works.

  • 1 ≤ numCourses ≤ 2000 (call it V)
  • 0 ≤ prerequisites.length ≤ 5000 (call it E)
  • Every pair is different, and no course needs itself.
Do we need to return the order?
No, only whether one exists. (Course Schedule II asks for the order, and the same method gives it.)
Can a course have several prerequisites?
Yes. Course 0 in Example 1 needs both 1 and 2.
Can the same pair appear twice?
No. Every pair is different.
Can a course have no rules at all?
Yes. It can be taken at any time.

Courses are nodes and each rule is an arrow from the course that comes first to the course that needs it: a directed graph. All courses can be taken exactly when the arrows never form a loop. Putting the nodes of a directed graph in an order that follows every arrow is a topological sort, a kind of graph search, and it fails exactly when there is a cycle.

There is no middle approach worth its own tab. A depth-first search that looks for a cycle is the other common answer, and it costs the same O(V + E) as Kahn's algorithm.

Kahn's algorithm O(V + E)

The brute force rechecks the same waiting courses over and over. Instead, keep a count for each course: how many of its prerequisites are missing. A course is ready exactly when its count is 0.

Put every course with count 0 in a queue. Take courses from the front of the queue. When a course is taken, every course that needs it is missing one less, so lower their counts by 1. A count that drops to 0 means that course is now ready: add it to the queue.

At the end, count the courses taken. If it is all of them, return true.

Why it is correct: a course is taken only when its count is 0, so all its prerequisites were taken before it. A course on a cycle waits for another course on the same cycle, so its count never reaches 0, and it is never taken.

0211213041
ready front →
empty
taken
0
  1. missing[course] = how many prerequisites course has
  2. ready = queue of courses with missing == 0
  3. while ready:
  4. course = ready.pop_front(); taken += 1
  5. for each next_course that needs course:
  6. missing[next_course] -= 1
  7. if missing[next_course] == 0: ready.push(next_course)
  8. return taken == numCourses

Under each course is how many of its prerequisites are still missing. Course 1 needs only 3, course 0 needs 1 and 2.

1 / 13
Solution.javasolution.pySolution.cssolution.gosolution.ts
class Solution {
// Kahn's algorithm: count each course's missing prerequisites, and take
// a course when its count reaches 0. Every course and every rule is handled once.
public boolean canFinish(int numCourses, int[][] prerequisites) {
List<List<Integer>> unlocks = new ArrayList<>();
for (int i = 0; i < numCourses; i++) unlocks.add(new ArrayList<>());
int[] missing = new int[numCourses];
for (int[] prerequisite : prerequisites) {
unlocks.get(prerequisite[1]).add(prerequisite[0]);
missing[prerequisite[0]]++;
}
Deque<Integer> ready = new ArrayDeque<>();
for (int course = 0; course < numCourses; course++)
if (missing[course] == 0) ready.add(course);
int taken = 0;
while (!ready.isEmpty()) {
int course = ready.poll();
taken++;
for (int nextCourse : unlocks.get(course)) {
missing[nextCourse]--;
// Its last missing prerequisite is now taken.
if (missing[nextCourse] == 0) ready.add(nextCourse);
}
}
// Courses on a cycle never reach 0, so they are never taken.
return taken == numCourses;
}
}
Time O(V + E)Space O(V + E)Here is how we get these from the code, in four steps.
  1. 1 Find the work that repeats

    Building the lists and counts reads each rule once. Then the while loop pops one course per run, and the inner for follows each arrow out of that course. Popping and following an arrow are fixed jobs, so each is 1 step.

    1. missing[course] = how many prerequisites course has
    2. ready = queue of courses with missing == 0
    3. while ready:
    4. course = ready.pop_front(); taken += 1
    5. for each next_course that needs course:
    6. missing[next_course] -= 1
    7. if missing[next_course] == 0: ready.push(next_course)
    8. return taken == numCourses
  2. 2 Count the runs on Example 1

    The table has one column per course, in the order they were taken. Each was popped once, and each followed the arrows out of it.

    5 pops + 2 + 1 + 1 + 1 + 0 = 5 arrows, so 10 steps, after 5 steps to build the counts.

    takencourse 3course 1course 2course 0course 4
    pops11111
    arrows followed21110
    total
    5 pops + 5 arrows = 10
  3. 3 Write the count for any n

    A course joins the queue once, when its count first hits 0, so it is popped at most once. Its arrows are followed only then, so every arrow is followed at most once:

    E (build) + V (pops) + E (arrows) = V + 2 × E steps

    Check it with Example 1: 5 + 10 = 15 steps. That matches the 5 rules read plus the 10 steps above.

  4. 4 Keep only the part that grows fastest

    Drop the 2. What is left is V + E: one look at each course and each rule.

    So the time is O(V + E).

  5. S Space: count the extra memory the same way

    The unlocks lists hold every rule once: E entries. The counts and the queue hold at most V each. So the space is O(V + E).

    unlocks lists (E entries)missing counts (V)ready queue (up to V)

2,000 random inputs of up to 6 courses and up to 8 different rules, so loops of every length come up. The repeated passes, Kahn's algorithm and a simple loop check written in the test gave the same answer every time.

Largest inputsResult
2,000 courses and 5,000 rules (the most allowed), with a chain where each course needs the next one. Kahn’s algorithm.Under 0.01 s, and about 0.2 MB of extra memory: the rule lists and the counts, O(V + E). The same when one rule is turned around to close a loop.
The same 2,000 courses and 5,000 rules. Repeated passes.0.4 s. The chain lets each pass take only one course, so it needs 2,000 passes over all the rules.