Skip to main content

Course Schedule Problem Solution

Complete Java implementation for course scheduling with cycle detection using topological sorting and DFS.

The Course Schedule problem involves determining if it's possible to finish all courses given their prerequisites, which translates to detecting cycles in a directed graph using topological sorting techniques.

1. Problem Restatement

Clarifying questions to ask the interviewer:

  1. Can the input be mutated or should I work with a copy?
  2. What's the expected time complexity constraint for the solution?
  3. Are there any constraints on the number of courses or prerequisites?
  4. Should I return true/false or the actual order of courses?
  5. How should I handle cycles in the dependency graph?

Restatement: Given a number of courses and their prerequisite relationships, determine if it's possible to finish all courses. This is essentially a cycle detection problem in a directed graph, where each course is a node and each prerequisite relationship is a directed edge.

Input/Output:

  • Input: numCourses (integer) and prerequisites (array of [course, prerequisite] pairs)
  • Output: true if all courses can be completed, false otherwise

Constraints:

  • 1 ≤ numCourses ≤ 2000
  • 0 ≤ prerequisites.length ≤ 5000
  • Each prerequisite pair is unique and valid

Assumptions:

  • Courses are numbered from 0 to numCourses - 1
  • A course can be a prerequisite for multiple courses
  • The prerequisites form a directed graph with potential cycles

Data Shape: This is a classic topological sorting problem represented as a directed graph. The structure is a list of edges where each edge [a, b] means course b must be completed before course a can begin. The hidden structure is that we're looking for cycles in this dependency graph.

The problem reduces to detecting whether there's a valid topological ordering of the courses, which happens if and only if the graph has no cycles.

2. Pattern Recognition

The problem exhibits characteristics of a directed graph cycle detection pattern, specifically a topological sorting problem.

The key wording that signals this pattern includes:

  • "prerequisites" - indicates dependencies between courses
  • "must take course A before B" - clearly defines directed edges in a dependency graph
  • "determine if it's possible to finish all courses" - asks about the existence of a valid ordering

This problem fits the topological sorting pattern because:

  1. Each course is a node in a directed graph
  2. Each prerequisite relationship creates a directed edge from the prerequisite to the dependent course
  3. The question asks whether we can find a valid ordering of all courses (a topological sort)
  4. If there's a cycle in the dependency graph, it's impossible to complete all courses

The core insight is that this is essentially asking: "Does the directed graph of course dependencies have a valid topological ordering?" If there's a cycle, no valid ordering exists.

This pattern is commonly solved using either:

  • Kahn's algorithm (BFS-based topological sort)
  • DFS-based cycle detection
  • Course scheduling with dependency tracking

The fundamental structure is that we're dealing with a directed acyclic graph (DAG) where we need to detect cycles or produce a topological ordering.

3. Approach Evolution

My first instinct is to think about this as a dependency problem where courses have prerequisites that must be completed before taking the course. This sounds like a graph cycle detection problem, where we need to detect if there's a circular dependency between courses. But that's O(n²) in the worst case because we'd need to check all course dependencies.

Let me think about how to optimize this. I notice that if I model this as a directed graph where each edge represents a prerequisite relationship, then the problem becomes detecting cycles in a directed graph. This is a classic topological sorting problem.

I can reduce it to O(n + m) where n is the number of courses and m is the number of prerequisites by using a topological sort approach with in-degrees. The key insight is that if I can process all courses in a topological order, there's no cycle and the schedule is possible.

I'll use Kahn's algorithm approach:

  1. Build a graph representation with in-degrees for each course
  2. Start with courses that have no prerequisites (in-degree = 0)
  3. Process these courses and reduce in-degrees of dependent courses
  4. If I can process all courses, return true; otherwise, there's a cycle

The invariant I'm maintaining is that at each step, all courses with in-degree 0 are ready to be taken. If I can take all courses, the schedule is valid.

This approach is optimal because:

  • Each course and prerequisite is processed exactly once
  • We're not doing redundant work or checking dependencies multiple times
  • The time complexity is linear in the input size

The space complexity is also linear since we store the graph and in-degrees for each course.

Let me walk through this more carefully. I'm thinking about using a queue to process courses with zero in-degree, and for each course I process, I reduce the in-degrees of its dependents. If any dependent reaches zero in-degree, I add it to the queue. This continues until either:

  1. The queue is empty and I've processed all courses (valid schedule)
  2. The queue is empty but I haven't processed all courses (cycle exists)

This approach is clean, efficient, and handles the core problem of course dependencies effectively.

4. Data Structure Choice

For this course schedule problem, I'll use a combination of data structures to efficiently detect cycles in the dependency graph:

Primary Data Structures:

  1. Adjacency List (HashMap<Integer, List>) - This is the core representation of our course dependency graph. Each course maps to a list of its prerequisites. This allows O(1) access to all dependencies for any given course and handles sparse graphs efficiently.

  2. Color Array (int[]) or HashSet - I'll use a color marking approach with three states:

    • 0 = unvisited (white)
    • 1 = visiting (gray)
    • 2 = visited (black)
  3. Stack or Recursion - For the DFS traversal, I'll use recursion (which implicitly uses call stack) to traverse dependencies and detect cycles.

Why These Choices:

The adjacency list is optimal for graph representation when we have a sparse dependency structure, as it only stores actual relationships rather than maintaining a full matrix. The color marking approach is perfect for cycle detection in directed graphs - it gives us O(1) state checking and clear indication of when we're in a cycle.

Space vs Time Trade-offs:

  • Using adjacency list instead of adjacency matrix saves space from O(n²) to O(V+E) when the graph is sparse
  • The color array provides O(1) state tracking instead of needing separate visited/processing sets
  • This approach avoids the need for additional data structures like in-degree counters that would be needed for topological sorting approaches

Helper Structures:

I'll use a simple integer array for color tracking rather than a more complex state object, as the three-state system is clean and efficient. The adjacency list serves as both the graph representation and a way to avoid revisiting nodes in the same DFS path.

This combination gives us clean, efficient graph traversal with minimal memory overhead and optimal time complexity.

5. Algorithm Walkthrough

Let me trace through the algorithm with a concrete example to show how it works step by step.

Example: numCourses = 3, prerequisites = [[1,0],[2,1]]

Step 1: Initialize data structures

  • inDegree = [0, 1, 1] (course 0 has 0 prerequisites, course 1 has 1 prerequisite, etc.)
  • adjList = [[], [0], [1]] (course 0 is a prerequisite for course 1, course 1 is a prerequisite for course 2)
  • queue = [] (empty initially)

Step 2: Add courses with zero prerequisites to queue

  • Course 0 has inDegree 0, so add it to queue: queue = [0]

Step 3: Process queue elements

  • Pop course 0: queue = []
  • For course 0, we look at its neighbors in adjList: [1] (course 1 depends on course 0)
  • Reduce inDegree of course 1: inDegree[1] = 1 - 1 = 0
  • Since inDegree[1] is now 0, add course 1 to queue: queue = [1]

Step 4: Continue processing

  • Pop course 1: queue = []
  • For course 1, we look at its neighbors in adjList: [2] (course 2 depends on course 1)
  • Reduce inDegree of course 2: inDegree[2] = 1 - 1 = 0
  • Since inDegree[2] is now 0, add course 2 to queue: queue = [2]

Step 5: Final processing

  • Pop course 2: queue = []
  • For course 2, we look at its neighbors in adjList: [] (no courses depend on course 2)
  • No more courses to add to queue

Step 6: Check completion

  • Processed count = 3 (all courses)
  • processedCount == numCourses → return true

The algorithm correctly identifies that we can complete all courses in order: 0 → 1 → 2.

Edge case handling: If there were a cycle (e.g., prerequisites = [[1,0],[0,1]]), the inDegree of each course would never reach zero, so we'd end up with processedCount < numCourses and return false.

The algorithm terminates because:

  1. We process each course exactly once
  2. Each prerequisite relationship is considered exactly once
  3. The queue size never exceeds the number of courses
  4. We only add courses to the queue when their prerequisites are fully satisfied

6. Java Solution

import java.util.*;

public class CourseSchedule {
    
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // Build adjacency list representation of the graph
        List<List<Integer>> graph = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            graph.add(new ArrayList<>());
        }
        
        // Populate the adjacency list with prerequisites
        for (int[] prereq : prerequisites) {
            int course = prereq[0];
            int prerequisite = prereq[1];
            graph.get(prerequisite).add(course);
        }
        
        // Track the state of each node: 0 = unvisited, 1 = visiting, 2 = visited
        int[] state = new int[numCourses];
        
        // Check each course for cycles
        for (int i = 0; i < numCourses; i++) {
            if (state[i] == 0) {
                if (hasCycle(graph, state, i)) {
                    return false;
                }
            }
        }
        
        return true;
    }
    
    private boolean hasCycle(List<List<Integer>> graph, int[] state, int course) {
        // Mark as currently visiting
        state[course] = 1;
        
        // Check all neighbors (dependent courses)
        for (int neighbor : graph.get(course)) {
            // If we encounter a node that's currently being visited, we found a cycle
            if (state[neighbor] == 1) {
                return true;
            }
            
            // If neighbor hasn't been visited, recursively check it
            if (state[neighbor] == 0 && hasCycle(graph, state, neighbor)) {
                return true;
            }
        }
        
        // Mark as fully visited
        state[course] = 2;
        return false;
    }
}

This solution uses a graph coloring approach to detect cycles in the course dependency graph. Each course is a node, and each prerequisite relationship is a directed edge from the prerequisite to the dependent course. By using three states (unvisited, visiting, visited), we can detect if there's a cycle in the dependency graph, which would make completing all courses impossible.

7. Complexity Analysis

Time Complexity: O(V + E) where V is the number of courses and E is the number of prerequisites

The algorithm performs a topological sort using BFS/DFS, visiting each course (vertex) exactly once and processing each prerequisite relationship (edge) exactly once. The BFS approach processes each node once and examines all outgoing edges, resulting in linear time complexity.

Space Complexity: O(V + E)

The space usage comes from:

  • The adjacency list representation: O(V + E) - storing all prerequisite relationships
  • The in-degree array: O(V) - tracking incoming edges for each course
  • The queue/stack for traversal: O(V) - in worst case, all courses might be in the queue

The total space complexity is O(V + E) which matches the input size.

Why these bounds are correct:

The time complexity is optimal for this problem because we must examine each prerequisite relationship at least once to determine if a valid schedule exists. Any algorithm that solves this problem must process all dependencies, making O(V + E) the theoretical lower bound.

The space complexity is also optimal as we need to store all input relationships and maintain state for the graph traversal algorithm.

This solution is near-optimal because it achieves both the best possible time and space complexity for solving a topological sorting problem, which is the core of this course scheduling challenge. The algorithm avoids any unnecessary operations or redundant data structures that would increase complexity beyond the inherent requirements of the problem.

8. Edge Cases & Pitfalls

Empty and Minimal Inputs

  • Empty input: An empty list of courses should return true (no dependencies to check)
  • Single course: A single course with no prerequisites should return true
  • Two courses with no cycle: Two courses where one depends on the other should return true

Duplicate Values and Repeated States

  • Duplicate prerequisites: Multiple identical prerequisite entries should not affect the result
  • Self-referencing dependencies: A course that depends on itself creates a cycle and should return false
  • Multiple paths to same course: Different prerequisite chains leading to the same course should be handled correctly

Boundary Conditions

  • Large input size: With up to 2000 courses, we must ensure our solution scales efficiently
  • All courses have prerequisites: When every course depends on others, we must detect cycles properly
  • No prerequisites at all: When no courses depend on others, should return true immediately

Common Implementation Mistakes

  • Incorrect cycle detection: Using a simple visited array instead of tracking the current DFS path can miss cycles
  • Not handling multiple dependencies: Assuming each course has only one prerequisite instead of a list
  • Wrong graph representation: Using adjacency lists incorrectly or missing edges in the graph construction
  • State management errors: Not properly resetting state variables between DFS traversals or maintaining incorrect path tracking
  • Off-by-one errors: Miscounting course indices or incorrectly mapping course numbers to array indices

The most critical pitfall is ensuring that we properly track the current DFS path to detect cycles - a simple visited array alone will not correctly identify all cycles in a directed graph.

9. Alternative Approaches

There are several alternative approaches to solving the Course Schedule problem, each with different trade-offs:

1. DFS-based Cycle Detection with Color Coding Instead of using BFS with in-degrees, we can use DFS to detect cycles in the dependency graph. We maintain a color array where:

  • White (0): Unvisited nodes
  • Gray (1): Visited but not fully processed (in current DFS path)
  • Black (2): Fully processed

If we encounter a gray node during DFS, we've found a cycle. This approach is more intuitive for cycle detection but requires maintaining the color state and can be slightly more complex to implement correctly.

2. Topological Sort using DFS with Stack We can perform a DFS-based topological sort where we push nodes to a stack after processing all their dependencies. If we can process all nodes, the schedule is valid. This approach requires careful handling of the DFS order and stack management.

3. Union-Find with Cycle Detection While not directly applicable to this specific problem, we could potentially use Union-Find to detect cycles in a different representation. However, this would be more complex and less efficient than the standard topological sorting approaches.

4. Brute Force with Backtracking We could try all possible orderings of courses and validate each one, but this would be exponential in time complexity O(n! × m) where m is the number of prerequisites, making it completely impractical for large inputs.

The chosen solution using BFS with in-degrees is preferred because it's:

  • More intuitive for the specific problem structure
  • Has optimal time complexity O(V + E)
  • Is easier to reason about and debug
  • Uses a standard algorithm pattern that's well-understood in technical interviews

The DFS-based approaches are more complex to implement correctly and have similar time complexity but higher constant factors due to the recursive call stack overhead.

10. Test Cases

public class CourseScheduleTestCases {
    public static void main(String[] args) {
        // Test Case 1: Basic valid schedule
        // Input: numCourses = 2, prerequisites = [[1,0]]
        // Expected: true
        int[][] prereqs1 = {{1, 0}};
        System.out.println("Test 1: " + new CourseSchedule().canFinish(2, prereqs1)); // true

        // Test Case 2: Basic invalid schedule (cycle)
        // Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
        // Expected: false
        int[][] prereqs2 = {{1, 0}, {0, 1}};
        System.out.println("Test 2: " + new CourseSchedule().canFinish(2, prereqs2)); // false

        // Test Case 3: Multiple courses with valid dependencies
        // Input: numCourses = 4, prerequisites = [[2,0],[2,1],[3,2],[1,3]]
        // Expected: false (cycle: 0->2->3->1->0)
        int[][] prereqs3 = {{2, 0}, {2, 1}, {3, 2}, {1, 3}};
        System.out.println("Test 3: " + new CourseSchedule().canFinish(4, prereqs3)); // false

        // Test Case 4: No prerequisites
        // Input: numCourses = 3, prerequisites = []
        // Expected: true
        int[][] prereqs4 = {};
        System.out.println("Test 4: " + new CourseSchedule().canFinish(3, prereqs4)); // true

        // Test Case 5: Single course
        // Input: numCourses = 1, prerequisites = []
        // Expected: true
        int[][] prereqs5 = {};
        System.out.println("Test 5: " + new CourseSchedule().canFinish(1, prereqs5)); // true

        // Test Case 6: Complex valid schedule
        // Input: numCourses = 5, prerequisites = [[1,0],[2,0],[3,1],[4,1],[3,2]]
        // Expected: true
        int[][] prereqs6 = {{1, 0}, {2, 0}, {3, 1}, {4, 1}, {3, 2}};
        System.out.println("Test 6: " + new CourseSchedule().canFinish(5, prereqs6)); // true
    }
}

Sample Test Cases:

Basic Valid Case: numCourses = 2, prerequisites = [[1,0]]

  • Course 1 requires course 0
  • Valid schedule possible → true

Cycle Detection Case: numCourses = 2, prerequisites = [[1,0],[0,1]]

  • Course 1 requires course 0, and course 0 requires course 1
  • Creates a cycle → false

Complex Valid Case: numCourses = 5, prerequisites = [[1,0],[2,0],[3,1],[4,1],[3,2]]

  • Valid dependency chain: 0 → 1 → 3 and 0 → 2 → 3 and 1 → 4
  • No cycles present → true

Edge Case - Empty Input: numCourses = 3, prerequisites = []

  • No dependencies at all
  • Trivially valid → true

Edge Case - Single Course: numCourses = 1, prerequisites = []

  • Only one course with no dependencies
  • Trivially valid → true

These test cases cover the core scenarios: cycle detection, dependency resolution, edge cases with minimal inputs, and complex valid schedules. The test suite ensures that the solution correctly identifies when a valid course schedule can be constructed versus when circular dependencies prevent completion.

11. Final Summary

The core insight is that this problem reduces to detecting cycles in a directed graph, where courses are nodes and prerequisites are edges. By using topological sorting with BFS/queue-based processing, we efficiently determine if all courses can be completed without circular dependencies. This solution is optimal with O(V + E) time complexity and handles all edge cases including isolated nodes, self-dependencies, and disconnected components through careful graph representation and traversal logic.

Was this article helpful?

Topics in this article

More from DSA Problems

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.