Algorithm

Course Schedule

Graphs Pattern

Course Schedule

You are given numCourses courses labelled 0 to numCourses-1, and a list of prerequisite pairs. Each pair [a, b] means course b must be taken before course a.

Return true if it is possible to take every course, and false otherwise.

Only a boolean is required — not an actual schedule. A schedule fails to exist exactly when the prerequisites form a circular chain, since each course in such a chain would have to be taken before itself.

CONSTRAINTS
  • 1 <= numCourses <= 2000
  • 0 <= prerequisites.length <= 5000
  • prerequisites[i] = [a, b] means b is a prerequisite of a
  • All prerequisite pairs are distinct
  • A course may have any number of prerequisites, including none
EXAMPLE 1
Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Course 0 has no prerequisites, so it can be taken first, and then course 1 becomes available. A valid schedule exists, so the answer is true.
EXAMPLE 2
Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Course 1 needs course 0 first, and course 0 needs course 1 first. Neither can ever be the first course taken, so neither can be taken at all. The two block each other permanently.
EXAMPLE 3
Input: numCourses = 3, prerequisites = []
Output: true
With no prerequisites at all, the courses may be taken in any order. An empty constraint list is always satisfiable — a common edge case to check explicitly.
EXAMPLE 4
Input: numCourses = 4, prerequisites = [[1,0],[2,1],[3,2],[1,3]]
Output: false
Courses 1, 2 and 3 form a loop: 1 needs 0 and 3, 2 needs 1, 3 needs 2, and 1 needs 3 again. Course 0 is takeable, but the remaining three wait on each other forever. A cycle among a subset is enough to make the whole thing impossible, even when some courses are perfectly fine.
In the pair [a, b], which course comes first?
b comes first — b is the prerequisite of a. This is reversed from how most people read the pair, and building the edge the wrong way round is the single most common failure here. Say it back to the interviewer before writing code.
Do I need to return the actual order of courses, or just whether one exists?
Just the boolean for this version. Returning the order is the follow-up known as Course Schedule II, and the same algorithm answers both — you either report the length of the ordering you built or the ordering itself.
Can a course be its own prerequisite?
A pair like [1,1] is a cycle of length one and makes the answer false. Standard test sets rarely include it, but the algorithms below handle it correctly with no special case.
Must the prerequisite graph be connected?
No. Courses often form several independent groups, and every group must be schedulable. That is why the search cannot start from a single course — it must consider all of them.

Strip the story away first. Courses are things, prerequisites are one-way constraints between them, and the question is whether all the constraints can be satisfied at once. That is a graph question, and identifying the graph correctly is most of the battle.

Building the graph, carefully

Each course becomes a vertex. Each prerequisite pair becomes a directed edge, and the direction is where people go wrong.

The pair [a, b] means b must come before a. So the useful edge is b → a, read as "finishing b unlocks a". Building it as a → b instead reverses every relationship — and here is the nasty part: on many test cases a reversed graph still gives the right answer, because a graph has a cycle exactly when its reversal has a cycle. It passes, then fails on the ordering follow-up. Get the direction right on principle, not by testing.

With the graph built, the question becomes: is this directed graph free of cycles? A schedule exists precisely when it is. If courses x, y and z each require the next around a loop, none can ever be first, so none can ever be taken — and no amount of scheduling cleverness fixes it.

The insight: count what you can actually finish

Rather than hunting for a cycle directly, simulate the student's experience and see how far you get.

Give each course a counter of how many prerequisites it still needs — its in-degree. A course with a count of zero can be taken immediately. Take it, and every course that listed it as a prerequisite loses one requirement. Any that drop to zero have just been unlocked and can be taken next. Keep going until nothing is available.

Then compare: if the number of courses you managed to take equals the total, every course was reachable and the answer is true. If you fall short, the leftovers are stuck.

Why is falling short a proof of a cycle rather than just a symptom? Every unfinished course still has at least one prerequisite that was never finished. Walk backwards from any unfinished course to one of its unfinished prerequisites, then to one of its, and so on. Each step lands on another unfinished course, and there are finitely many, so the walk must eventually revisit one — that repetition is exactly a cycle. So the count is a complete test, not a heuristic.

python
from collections import deque

def can_finish(num_courses, prerequisites):
    adj = [[] for _ in range(num_courses)]
    in_degree = [0] * num_courses

    for course, prereq in prerequisites:      # [a, b] means b before a
        adj[prereq].append(course)            # finishing prereq unlocks course
        in_degree[course] += 1

    queue = deque(c for c in range(num_courses) if in_degree[c] == 0)
    finished = 0

    while queue:
        course = queue.popleft()
        finished += 1
        for nxt in adj[course]:
            in_degree[nxt] -= 1
            if in_degree[nxt] == 0:
                queue.append(nxt)

    return finished == num_courses
Crucial Notethe queue is seeded with every zero-prerequisite course before the loop starts, not just one. The prerequisite graph is frequently disconnected — several unrelated groups of courses — and starting from a single course would only ever explore its own group, leaving the rest wrongly counted as unfinishable.
Crucial Notea course is enqueued only at the moment its count reaches exactly 0, never on the way past. Using <= 0 instead of == 0 would enqueue a course repeatedly as further prerequisites are cleared, inflating finished past the true value and returning true on a cyclic graph.

Each course enters the queue at most once and each edge is examined once, so this is O(V + E) time — with V up to 2000 and E up to 5000, entirely comfortable — and O(V + E) space for the adjacency list.

The other route: watch the active path

A depth-first search can detect the cycle directly, and it is worth knowing because the same machinery reconstructs the cycle when asked.

The naive idea — flag a cycle whenever you reach an already-visited course — is wrong, and understanding why is the valuable part. Consider courses where 0 unlocks both 1 and 2, and both 1 and 2 unlock 3. Exploring from 0 through 1 reaches 3. Backtracking and exploring through 2 reaches 3 again — already visited, yet there is no cycle here at all. Every path runs forward; 3 simply has two prerequisites.

The fix is to distinguish two different meanings of "seen". A course may be currently in progress, meaning it sits on the path you are exploring right now, or finished, meaning it and everything beyond it were fully checked and cleared. Reaching an in-progress course means the path has looped back onto its own ancestor — a genuine cycle. Reaching a finished course means nothing at all; skip it.

python
def can_finish(num_courses, prerequisites):
    adj = [[] for _ in range(num_courses)]
    for course, prereq in prerequisites:
        adj[prereq].append(course)

    UNVISITED, IN_PROGRESS, DONE = 0, 1, 2
    state = [UNVISITED] * num_courses

    def has_cycle(u):
        state[u] = IN_PROGRESS
        for v in adj[u]:
            if state[v] == IN_PROGRESS:       # looped back onto the current path
                return True
            if state[v] == UNVISITED and has_cycle(v):
                return True
        state[u] = DONE                       # cleared: safe forever
        return False

    return not any(has_cycle(c) for c in range(num_courses) if state[c] == UNVISITED)

The DONE state is what keeps this linear. Without it, a course reachable by many routes would be re-explored once per route, and the running time would blow up exponentially even though the answer stayed correct.

Worked example

Take numCourses = 4 with prerequisites [[1,0], [2,1], [3,2], [1,3]].

Reading each pair as [course, prereq], the edges are 0→1, 1→2, 2→3 and 3→1. In-degrees: course 0 has none, so 0. Course 1 is pointed at by 0 and 3, so 2. Course 2 is pointed at by 1, so 1. Course 3 is pointed at by 2, so 1.

- The queue is seeded with course 0 alone — the only one with no prerequisites.
- Pop 0. Finished is 1. It unlocks course 1, whose count drops from 2 to 1. Still blocked, so nothing is enqueued.
- The queue is now empty and the loop ends.

Finished is 1, but there are 4 courses, so the answer is false. And the leftover set {1, 2, 3} is exactly the cycle: 1 needs 3, 3 needs 2, 2 needs 1. Course 0 was perfectly takeable; one bad loop elsewhere still sinks the whole schedule.

Contrast with numCourses = 2 and prerequisites [[1,0]]. In-degrees are 0 for course 0 and 1 for course 1. Pop 0, finished is 1, course 1 drops to 0 and is enqueued. Pop 1, finished is 2. Two of two, so true.

The takeaway

This problem is a directed cycle detection question with a story attached, and recognising that is the whole skill. The signals recur constantly: prerequisites, dependencies, build order, task requirements, "must happen before" — all of it means a directed graph, and asking whether the work is completable means asking whether that graph is acyclic.

The two solutions are worth holding onto for different reasons. Kahn's counting version is easier to get right under pressure and generalises immediately to producing the actual schedule, which is the standard follow-up: collect the popped courses instead of just counting them. The DFS version generalises in a different direction — the in-progress states on the stack at the moment a cycle is found are the cycle, so it is the one to reach for when asked which courses are deadlocked.

Train the reflex: whenever a problem asks whether a set of "X before Y" requirements can all be satisfied, do not look for a clever scheduling rule. Build the directed graph and test it for a cycle.

O(V × E) Repeated Scan For A Free Course
O(V + E) Kahn's In-Degree Peeling
O(V + E) DFS With Path Marking