Vibe Engines
YouTube

Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path

CODING CHALLENGE · N°43

Course Schedule (Cycle Detection)

Medium GraphsTopological SortBFS

Can you finish every course given its prerequisites? It’s the classic "is this dependency graph acyclic?" question — the same check a build system or package manager runs before scheduling work. Detect a cycle with a topological sort. Solve it in Python or TypeScript, with hidden tests.

The problem

There are n courses labeled 0…n-1. Each pair [a, b] in prereqs means you must take course b before course a. Return True if it is possible to finish all courses, or False if the prerequisites contain a cycle (a deadlock where nothing can start).

SOLVE IT YOURSELF

Your turn — write it

Edit the stub, hit Run (or ⌘/Ctrl + Enter), and watch the hidden tests. Stuck? open the hints under the problem, or Reveal solution is one click away.

YOUR TASK

Implement can_finish(n, prereqs): build the dependency graph and each course’s in-degree, start from the courses with in-degree 0, and repeatedly "take" a course, decrementing its dependents’ in-degrees. If you can take all n, return True; otherwise a cycle blocked you.

CPython · WebAssembly
Examples and constraints 4 examples
EXAMPLE 1
Input n = 2, prereqs = [[1, 0]]
Output True
take 0, then 1
EXAMPLE 2
Input n = 2, prereqs = [[1, 0], [0, 1]]
Output False
each needs the other first — a cycle
EXAMPLE 3
Input n = 4, prereqs = [[1, 0], [2, 0], [3, 1], [3, 2]]
Output True
a diamond: course 3 waits for both 1 and 2, and is released when the second one is taken
EXAMPLE 4
Input n = 1, prereqs = [[0, 0]]
Output False
edge case: a course that is its own prerequisite is a cycle of length one
CONSTRAINTS
  • A schedule exists iff the prerequisite graph is a DAG (has no directed cycle).
  • Use a topological sort: Kahn’s algorithm (repeatedly remove a course with no remaining prerequisites) or DFS cycle detection.
  • If every course is eventually removable (all get scheduled), return True; if some remain stuck, there is a cycle.
Hints 4 ideas
HINTS — 4 IDEAS
  1. Edge [a, b] means b → a (b unlocks a). Track each course’s in-degree = number of unmet prerequisites.
  2. Seed a queue with every course whose in-degree is 0 — those can be taken immediately.
  3. Take a course from the queue, count it, and decrement the in-degree of each course it unlocks; enqueue any that hit 0.
  4. If the number taken equals n, all courses were schedulable. If fewer, a cycle left some permanently blocked.
Approach, complexity & discussion — open after you solve

Start from brute force

The obvious approach simulates semesters. Make a pass over every course and take any whose prerequisites are all already taken, by scanning prereqs for that course; repeat until a pass takes nothing, then check whether all n were taken. It is correct, because a course is taken exactly when nothing it depends on is still pending. But each pass costs O(V + E), and a chain listed in the worst order takes only one course per pass: with [[0, 1], [1, 2], [2, 3], [3, 4]] it needs five passes plus an empty one. That is O(V · (V + E)) time with O(V) space for the taken set, around 1010 steps at 105 courses.

The approach

Kahn’s algorithm keeps the bookkeeping the brute force recomputes. The invariant is that indeg[v] is the number of v’s prerequisites not yet taken, so a course is takeable exactly when its count reaches 0, and nothing ever needs rescanning. A course on a cycle, or downstream of one, waits on a course that waits on it, so its count never reaches 0 and it is never taken.

The reference solution reads each pair [a, b] as an edge b → a: it appends a to adj[b] and adds 1 to indeg[a]. It seeds a deque with every course whose in-degree is 0. Each popleft() takes course u, adds 1 to taken, and decrements indeg[v] for every v in adj[u], appending v to the queue the moment its count hits 0, so each course is queued at most once. When the queue empties, taken == n is the answer: every course was released, or a cycle held some back.

Walk through an example

The first two examples are too small to show a course waiting on two prerequisites, so trace the diamond in example 3, can_finish(4, [[1, 0], [2, 0], [3, 1], [3, 2]]).

stepactiontakenindegqueue
1build: adj = [[1, 2], [3], [3], []]0[0, 1, 1, 2]—
2seed with in-degree 00[0, 1, 1, 2][0]
3take 0; 1 and 2 drop to 01[0, 0, 0, 2][1, 2]
4take 1; 3 drops to 12[0, 0, 0, 1][2]
5take 2; 3 drops to 03[0, 0, 0, 0][3]
6take 3; nothing to unlock4[0, 0, 0, 0][]
7taken == n4—returns True

Complexity

Optimal (Kahn’s): O(V + E) time, since each course is queued and popped once and each edge is decremented once, and O(V + E) space for adj, indeg and the queue. The TypeScript reference uses Array.shift(), which can cost O(V) per call; a read index keeps it linear. Brute force (repeated passes): O(V · (V + E)) time, O(V) space.

Edge cases to test

  • A course that requires itself: n = 1, prereqs = [[0, 0]] → False. indeg[0] is 1, and only course 0 could ever lower it.
  • Duplicate pairs: n = 2, prereqs = [[1, 0], [1, 0]] → True. indeg[1] counts 2 and adj[0] lists 1 twice, so both decrements happen. Deduplicating adj but not indeg leaves course 1 stuck at 1 and returns False.
  • No prerequisites: n = 3, prereqs = [] → True. Every course starts in the queue.
  • A cycle in one component only: n = 4, prereqs = [[1, 0], [2, 3], [3, 2]] → False. Courses 0 and 1 are taken, 2 and 3 never are, so taken stops at 2.
  • A course downstream of a cycle but not on it: n = 3, prereqs = [[1, 0], [0, 1], [2, 1]] → False. Course 2 is never released either.
  • n = 0 → True: zero courses taken out of zero.

Common mistakes

  • Not detecting the cycle — a circular prerequisite is unsatisfiable, and that is the whole question.
  • Building adj and indeg in inconsistent directions, such as adj[a].append(b) with indeg[a] += 1: then can_finish(2, [[1, 0]]) returns False. Reversing both consistently is harmless for this yes/no question, because a graph and its reverse have the same cycles; it only matters when you return the order.
  • In a DFS version, treating any already-visited node as a cycle. In the diamond 0 → 1 → 3, 0 → 2 → 3, course 3 is reached twice with no cycle; only a node still on the current path (gray) closes one.

Follow-ups an interviewer asks next

Can you return an actual order to take the courses in?
Yes, that is Course Schedule II. Append u to an order list each time it is popped; if len(order) == n, return it, otherwise return an empty list because a cycle exists. The pop order is a valid topological order, since a course enters the queue only after all its prerequisites were popped. Time and space stay O(V + E).
How would you solve it with DFS instead?
Color each node white (unvisited), gray (on the current path) or black (finished). Reaching a gray node means a back edge, which is a cycle. Reversed post-order gives a topological order. It is also O(V + E), but the recursion can go V deep, and Python’s default recursion limit is 1000, so a long chain needs an explicit stack.
How would you report which courses form the cycle?
After Kahn’s algorithm, the courses with indeg still above 0 are on a cycle or downstream of one. To print an actual cycle, run a DFS restricted to those leftover courses and keep the current path; the first edge back to a node on the path gives the loop, which is the message a package manager prints for a circular dependency.
What is the minimum number of semesters if you can take any number of courses at once?
Process Kahn’s queue level by level: everything in the queue at the start of a round is one semester. The number of rounds equals the number of courses on the longest prerequisite chain, and it is still O(V + E). Capping how many courses fit in a semester makes the problem much harder, which is why that variant is usually posed with very small n.

Where this shows up

This is topological sort — the “valid order under dependencies” behind build systems (make, Bazel), task schedulers, package managers resolving install order, and spreadsheet recalculation. Cycle detection is exactly why a circular dependency errors out instead of hanging forever.

Reference solution

Reference solution — Python
from collections import deque

def can_finish(n, prereqs):
    indeg = [0] * n
    adj = [[] for _ in range(n)]
    for a, b in prereqs:          # b must come before a  (b -> a)
        adj[b].append(a)
        indeg[a] += 1

    queue = deque(i for i in range(n) if indeg[i] == 0)
    taken = 0
    while queue:
        u = queue.popleft()
        taken += 1
        for v in adj[u]:          # u unlocks v
            indeg[v] -= 1
            if indeg[v] == 0:
                queue.append(v)
    return taken == n             # all scheduled -> no cycle


print(can_finish(2, [[1, 0]]))          # -> True
print(can_finish(2, [[1, 0], [0, 1]]))  # -> False
Reference solution — TypeScript
function canFinish(n: number, prereqs: number[][]): boolean {
  const indeg = new Array(n).fill(0);
  const adj: number[][] = Array.from({ length: n }, () => []);
  for (const [a, b] of prereqs) {   // b must come before a  (b -> a)
    adj[b].push(a);
    indeg[a]++;
  }

  const queue: number[] = [];
  for (let i = 0; i < n; i++) if (indeg[i] === 0) queue.push(i);

  let taken = 0;
  while (queue.length) {
    const u = queue.shift()!;
    taken++;
    for (const v of adj[u]) {        // u unlocks v
      if (--indeg[v] === 0) queue.push(v);
    }
  }
  return taken === n;                // all scheduled -> no cycle
}

console.log(canFinish(2, [[1, 0]]));         // -> true
console.log(canFinish(2, [[1, 0], [0, 1]])); // -> false

Questions about Course Schedule (Cycle Detection)

Why does Kahn’s algorithm detect a cycle in Course Schedule?

Kahn’s algorithm only takes a course once its in-degree, the count of prerequisites not yet taken, has dropped to zero. Every course on a cycle depends, through the cycle, on another course on the same cycle, so none of them can be the first to reach zero: each is waiting for one of the others. They are never queued, and neither is anything that depends on them. So when the queue runs dry, the number of courses taken is less than n exactly when a cycle exists, and equal to n exactly when the graph is acyclic.

Should I use BFS or DFS for Course Schedule?

Both run in O(V + E) time and both are accepted answers. BFS with Kahn’s algorithm is iterative, so it never hits a recursion limit, and its pop order is already a valid course order if a follow-up asks for one. DFS uses three colors, white for unvisited, gray for on the current path and black for finished, and reports a cycle when it reaches a gray node; it is natural if you already think recursively, but in Python a long prerequisite chain can exceed the default recursion limit of 1000 unless you use an explicit stack.

Does edge direction matter when detecting a cycle in a prerequisite graph?

For the yes or no answer, no: reversing every edge turns each cycle into a cycle in the other direction, so a graph has a cycle exactly when its reverse does. Direction does matter for consistency. The adjacency list and the in-degree counts must describe the same direction, or the algorithm decrements the wrong counts and reports cycles that do not exist. It also matters as soon as you return an order, because the reversed graph gives the order backwards, with each course before its prerequisites.

What is the time complexity of the Course Schedule problem?

O(V + E), where V is the number of courses and E the number of prerequisite pairs. Building the adjacency list and in-degree array touches each pair once, each course is added to and removed from the queue at most once, and each edge is used for exactly one decrement. Space is also O(V + E) for the adjacency list, plus O(V) for the in-degrees and the queue. The naive alternative, repeatedly scanning every course for one whose prerequisites are all done, can take O(V times (V + E)) when the dependencies form a long chain.

Finished this one? 0 / 76 Challenges done
NextWord Ladder Next on the “Interview classics” ladder · rung 10 of 10

Explore the topic

See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.

More Challenges