Vibe Engines is free, interactive AI engineering & system design — runs in your browser. Pick a learning path →
Course Schedule (Cycle Detection)
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).
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.
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.
Examples and constraints 4 examples
n = 2, prereqs = [[1, 0]]Truen = 2, prereqs = [[1, 0], [0, 1]]Falsen = 4, prereqs = [[1, 0], [2, 0], [3, 1], [3, 2]]Truen = 1, prereqs = [[0, 0]]False- 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
- Edge
[a, b]meansb → a(b unlocks a). Track each course’s in-degree = number of unmet prerequisites. - Seed a queue with every course whose in-degree is 0 — those can be taken immediately.
- Take a course from the queue, count it, and decrement the in-degree of each course it unlocks; enqueue any that hit 0.
- 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]]).
| step | action | taken | indeg | queue |
|---|---|---|---|---|
| 1 | build: adj = [[1, 2], [3], [3], []] | 0 | [0, 1, 1, 2] | — |
| 2 | seed with in-degree 0 | 0 | [0, 1, 1, 2] | [0] |
| 3 | take 0; 1 and 2 drop to 0 | 1 | [0, 0, 0, 2] | [1, 2] |
| 4 | take 1; 3 drops to 1 | 2 | [0, 0, 0, 1] | [2] |
| 5 | take 2; 3 drops to 0 | 3 | [0, 0, 0, 0] | [3] |
| 6 | take 3; nothing to unlock | 4 | [0, 0, 0, 0] | [] |
| 7 | taken == n | 4 | — | 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 andadj[0]lists 1 twice, so both decrements happen. Deduplicatingadjbut notindegleaves course 1 stuck at 1 and returnsFalse. - 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, sotakenstops 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
adjandindegin inconsistent directions, such asadj[a].append(b)withindeg[a] += 1: thencan_finish(2, [[1, 0]])returnsFalse. 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
uto anorderlist each time it is popped; iflen(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 stayO(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 goVdeep, 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
indegstill 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 smalln.
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]])) # -> FalseReference 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]])); // -> falseQuestions 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.
Explore the topic
See this alongside everything else on the same subject — handbooks, system designs, challenges and tools, in one place.
More Challenges
- Two SumThe classic warm-up: find the two numbers that add up to a target. Brute force is O(n²) — a hash map gets you to one pass, O(n). Solve it in Python or TypeScript, right in your browser, with hidden tests and a reveal-solution button.Read →
- Valid ParenthesesThe canonical stack problem: decide whether every bracket is closed by the right type, in the right order. A stack turns nested matching into a single pass. Solve it in Python or TypeScript with hidden tests.Read →
- Fizz BuzzThe famous screening question. Print 1…n, but multiples of 3 become "Fizz", of 5 become "Buzz", and of both become "FizzBuzz". Easy — the catch is testing divisibility in the right order. Solve it in Python or TypeScript.Read →
- Number of IslandsThe canonical connected-components question. Flood-fill each unvisited patch of land with BFS or DFS, count how many floods you started — grid traversal, visited bookkeeping, and the classic diagonal trap. Solve it in Python or TypeScript.Read →
- Merge IntervalsCalendar apps and memory allocators run on this: sort by start, sweep once, grow or close the current block. Touching endpoints merge, nested intervals vanish — the classic that punishes off-by-one thinking. Solve it in Python or TypeScript.Read →
- Top-K Frequent ElementsCount, then rank — the two-step pattern behind trending topics and hot-key detection. A hash map counts; a (-count, value) sort ranks with a clean tie-break; buckets get you to O(n) if you want the flex. Solve it in Python or TypeScript.Read →