Course Schedule

Difficulty: Medium

There are numCourses courses, labeled from 0 to numCourses - 1. Some courses have a prerequisite: you're given a list of pairs [a, b], each meaning "you must finish course b before you're allowed to take course a."

Given numCourses and this list of prerequisite pairs, decide whether it is possible to finish all of the courses.

Examples

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

Take course 0 first, then course 1. No conflict.

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

Course 1 needs course 0 first, but course 0 needs course 1 first - neither can ever go first.

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

One valid order is 0, 1, 2, 3 - course 0 has no prerequisites, and every later course's prerequisites come before it.

Constraints

  • 1 <= numCourses <= 2000

  • 0 <= prerequisites.length <= 5000

  • prerequisites[i].length == 2

  • There are no duplicate prerequisite pairs.

Approach

Build a directed graph where an edge from b to a means "b must come before a." Now finishing all courses is possible exactly when this graph has no cycle - if there were a cycle, every course on it would be stuck waiting for another course on the very same cycle.

One way to check for a cycle is a DFS that tracks each node's state: unvisited, currently being explored (on the active recursion path), or fully done (already confirmed cycle-free). If the DFS ever walks into a node that is "currently being explored," that's a back-edge into the current path - a cycle - so the answer is false. If DFS finishes exploring every node without ever doing that, there's no cycle and the answer is true.

A different, non-recursive way to reach the same conclusion is Kahn's algorithm: repeatedly peel off courses that currently have no remaining prerequisites. If you can eventually peel off every course this way, there's no cycle.

Solutions

Brute Force - Try Every Ordering

Finishing all courses is possible exactly when some ordering of the courses respects every prerequisite pair. The most naive way to check that is to literally generate every possible ordering of the courses and test each one against every prerequisite pair, stopping as soon as one ordering works.

function canFinish(numCourses, prerequisites) {
  const courses = Array.from({ length: numCourses }, (_, i) => i);

  function isValidOrder(order) {
    const position = new Map();
    order.forEach((course, index) => position.set(course, index));

    for (const [a, b] of prerequisites) {
      if (position.get(b) > position.get(a)) return false; // b must come before a
    }
    return true;
  }

  function permute(start) {
    if (start === courses.length) {
      return isValidOrder(courses);
    }
    for (let i = start; i < courses.length; i++) {
      [courses[start], courses[i]] = [courses[i], courses[start]];
      if (permute(start + 1)) return true;
      [courses[start], courses[i]] = [courses[i], courses[start]];
    }
    return false;
  }

  return permute(0);
}

Time: O(n! × (n + p)) - n! orderings, each checked against p prerequisite pairs · Space: O(n)

Optimal - DFS Cycle Detection

Build an adjacency list from the prerequisites, then DFS from every course, tagging each node as visiting (on the current recursion path) or done (fully explored, no cycle found through it). If DFS ever reaches a node already tagged visiting, that's a cycle, so finishing is impossible.

function canFinish(numCourses, prerequisites) {
  const graph = Array.from({ length: numCourses }, () => []);
  for (const [a, b] of prerequisites) {
    graph[b].push(a); // b must be taken before a
  }

  const UNVISITED = 0, VISITING = 1, DONE = 2;
  const state = new Array(numCourses).fill(UNVISITED);

  function hasCycle(course) {
    if (state[course] === VISITING) return true; // back edge into current path
    if (state[course] === DONE) return false; // already confirmed safe

    state[course] = VISITING;
    for (const next of graph[course]) {
      if (hasCycle(next)) return true;
    }
    state[course] = DONE;
    return false;
  }

  for (let course = 0; course < numCourses; course++) {
    if (hasCycle(course)) return false;
  }
  return true;
}

Time: O(V + E) - every course and every prerequisite edge is visited once · Space: O(V + E) for the adjacency list, plus O(V) for the state array and recursion stack