2Brute2Best
🔥0

Course Schedule

Medium·LeetCode #207Basic Anchor
LeetCode
30-Second Mental ModelTopological Sort — Kahn's algorithm, cycle check (taken == numCourses)
Complexity:O(V · (V + E))→O(V + E)
Key Insight

Take the courses in waves. A course with no unfinished prerequisites is always safe to take now. Taking it removes one arrow from every course that waited on it. If the queue runs dry before all n courses are taken, the courses left over are all waiting on each other, so there is a loop.

Core Validity Invariant

A course is safe to take exactly when its waiting count is 0: every prerequisite is already taken, so nothing blocks it.

01

Problem

There are numCourses courses, labelled from 0 to numCourses − 1. prerequisites[i] = [a, b] means you must finish course b before you can take course a.

Return true if you can take every course. Return false if some courses block each other, so that none of them can be taken first.

Input

numCourses, and prerequisites, a list of pairs [a, b].

Output

true if every course can be taken in some order, otherwise false.

Example 1
Input: numCourses = 2, prerequisites = [[1, 0]]
Output: true

Take course 0 first, then course 1. Nothing blocks anything.

Example 2
Input: numCourses = 2, prerequisites = [[1, 0], [0, 1]]
Output: false

Course 1 needs course 0, and course 0 needs course 1. Neither one can be taken first, so the schedule is impossible.

Constraints
  • • 1 ≤ numCourses ≤ 2000
  • • 0 ≤ prerequisites.length ≤ 5000
  • • Each prerequisites[i] has exactly 2 numbers.
  • • 0 ≤ aᵢ, bᵢ < numCourses
  • • All the pairs in prerequisites are different.
02

First, think yourself

Before opening any solution, try to answer these on paper. Use hints only when stuck — each hint is counted.

  1. 1.

    For a pair [a, b], which course has to come first: a or b? Which way should the arrow point?

  2. 2.

    If a group of courses all need each other, can any one of them be taken first?

  3. 3.

    Which courses have no prerequisites at all? Could I start there?

  4. 4.

    After I take a course, which other courses get closer to being free?

  5. 5.

    If I manage to take every course one at a time, how many courses should I have taken at the end?

03

Brute force approach

05

Optimized approach

06

Why does this algorithm work?

07

Dry run

08

All approaches compared

ApproachIdeaTimeSpace
Brute ForceFrom each course, follow the prerequisite arrows; if the walk returns to the start, it is stuck in a loopO(V · (V + E))O(V + E)
OptimalKeep a count of unfinished prerequisites; take free courses from a queue; if every course gets taken, there is no loopO(V + E)O(V + E)
09

Why does this belong to this pattern?

Clues in the problem statement
  • “prerequisites [a, b]”Each pair is an arrow: course b must come before course a. The courses form a directed graph.
  • “can you finish all courses?”A yes or no question. It asks whether a loop exists, not for the order itself.
  • “courses that block each other”That is a loop. A loop means the processing order can never start.
directed graph of prerequisites+is there a loop?+Kahn: taken count equals V→Topological Sort — Kahn's algorithm, cycle check (taken == numCourses)

This is the anchor question of the cycle-detection sub-pattern. The sort is a tool here. If every course can be taken, a topological order exists, so there is no loop. If the queue runs dry early, the courses left over form a loop. Only the number of taken courses matters. The next two questions need different output: the cycle itself (GFG) and the safe nodes (LC 802).

10

Don't make this mistake

Using one visited set reports a loop that is not there
✗ bool dfs(int c) { if (visited[c]) return true; // seen before = loop? visited[c] = true; for (int d : next[c]) if (dfs(d)) return true; return false; }
✓ bool dfs(int c) { if (state[c] == ON_PATH) return true; // back on our own path: a real loop if (state[c] == DONE) return false; // finished earlier: safe to skip state[c] = ON_PATH; for (int d : next[c]) if (dfs(d)) return true; state[c] = DONE; return false; }

This is the undirected trick: remember everything seen and treat a revisit as a loop. On the dry-run courses (0 → 1, 0 → 2, 1 → 3, 2 → 3, 3 → 4), the walk goes 0 → 1 → 3 → 4 and finishes 3. Back at 0, it tries 0 → 2 → 3. Course 3 is already visited, so the wrong code says loop. But course 3 is finished, not on the current path. The real answer is true, and this code says false.

Counting the wrong course as waiting
✗ for (auto& p : prerequisites) { next[p[1]].push_back(p[0]); waiting[p[1]]++; // counts the course that is needed, not the one waiting }
✓ for (auto& p : prerequisites) { next[p[1]].push_back(p[0]); // b unlocks a waiting[p[0]]++; // a waits for one more course }

The count belongs to the course that has to wait, which is a (the first number). On the dry run the wrong count gives waiting = [2, 1, 1, 1, 0] instead of [0, 1, 1, 2, 1]. Only course 4 looks free, so the queue takes 4, nothing unlocks, and the code says false. The real answer is true.

Checking the empty queue instead of the count
✗ while (!ready.empty()) { /* take courses */ } return ready.empty(); // always true after the loop
✓ while (!ready.empty()) { /* take courses */ } return taken == numCourses;

When the loop ends, the queue is always empty, so this test always passes, even when courses are stuck. On example 2, [[1, 0], [0, 1]], no course is free at the start, the queue is empty at once, and the code says true. Count the courses you took instead.

11

Want to implement this?

Write it yourself on LeetCode, then come back and compare your code with the solutions above.

Open coding workspace
How did it go?
Beyond The LeetCode Problem

How this concept connects to System Design & C++ Systems

Bridge DSA with production engineering

Top engineering interviews test whether you understand how algorithms like Topological Sort translate into production architectures and low-level memory mechanics.