189. Course Schedule
๐ LeetCode Problem: 207. Course Schedule
๐ Difficulty: Medium
๐ท๏ธ Topics: Graph, DFS, BFS, Topological Sort, Cycle Detection
Problem Statement
There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [aแตข, bแตข] indicates that you must take course bแตข first if you want to take course aแตข.
- For example, the pair
[0, 1]indicates that to take course0you have to first take course1.
Return true if you can finish all courses. Otherwise, return false.
Example 1:
Input: numCourses = 2, prerequisites = [[1,0]]
Output: true
Explanation:
Total 2 courses: 0 and 1
To take course 1, you need to take course 0 first
Schedule: 0 โ 1
Possible! โ
Example 2:
Input: numCourses = 2, prerequisites = [[1,0],[0,1]]
Output: false
Explanation:
To take course 1, need course 0
To take course 0, need course 1
Circular dependency! โ
Impossible!
Constraints:
- 1 <= numCourses <= 2000
- 0 <= prerequisites.length <= 5000
- prerequisites[i].length == 2
- 0 <= aแตข, bแตข < numCourses
- All the pairs prerequisites[i] are unique.
๐ณ Visual Understanding - The Complete Picture
The Problem We're Solving ๐ค
Courses with prerequisites form a dependency graph: ๐โก๏ธ๐
Example:
numCourses = 4
prerequisites = [[1,0], [2,0], [3,1], [3,2]]
Meaning:
[1,0]: To take 1, must take 0 first (0 โ 1)
[2,0]: To take 2, must take 0 first (0 โ 2)
[3,1]: To take 3, must take 1 first (1 โ 3)
[3,2]: To take 3, must take 2 first (2 โ 3)
Graph visualization:
0
/ \
โ โ
1 2
\ /
โ
3
Question: Can we complete all courses? ๐ค
Answer: YES! โ
Schedule: 0 โ 1 โ 2 โ 3
(or 0 โ 2 โ 1 โ 3)
Building Understanding Through Examples ๐
Example 1: Simple chain (possible)
prerequisites = [[1,0]]
Graph:
0 โ 1
Schedule:
Step 1: Take course 0 โ
Step 2: Take course 1 โ
All courses completed! ๐
Answer: true
Example 2: Circular dependency (impossible)
prerequisites = [[1,0], [0,1]]
Graph:
0 โ 1
Dependency cycle:
Course 0 needs 1
Course 1 needs 0
โณ Circular! โ
Can't start anywhere!
Answer: false
Example 3: Complex DAG (possible)
prerequisites = [[1,0], [2,1], [3,2], [1,3]]
Graph:
0
โ
1 โโโ
โ โ
2 โ
โ โ
3 โโโ
Wait... 1 โ 3 โ 1? That's a cycle! โ
Actually:
[1,0]: 0 โ 1
[2,1]: 1 โ 2
[3,2]: 2 โ 3
[1,3]: 3 โ 1 (creates cycle!)
Cycle detected: 1 โ 2 โ 3 โ 1 โณ
Answer: false
Example 4: No prerequisites (always possible)
numCourses = 3
prerequisites = []
No dependencies at all!
Take in any order: 0, 1, 2 or 2, 1, 0, etc.
Answer: true โ
The Pattern Emerges ๐ก
Key observations: ๐
1๏ธโฃ Directed Graph Problem ๐ฏ
Courses = nodes
Prerequisites = directed edges
[a,b] means b โ a (b must come before a)
2๏ธโฃ Cycle Detection! ๐
Cycle = impossible schedule
DAG (no cycle) = possible schedule
3๏ธโฃ Topological Sort Connection ๐
Valid schedule = topological ordering
Exists only if graph is DAG!
4๏ธโฃ Two Approaches ๐ ๏ธ
DFS: Detect cycles directly
BFS (Kahn's): Topological sort attempt
5๏ธโฃ Return Boolean ๐ฒ
Just need true/false
Don't need actual schedule!
๐ง The Journey From Brute Force to Optimal
Part 1: The Obvious Try - "Generate all orderings?" ๐ค
๐ญ First thought: "Try all possible course orderings!"
Brute force:
Generate all permutations of courses
For each permutation:
Check if prerequisites satisfied
If yes: return true
If no permutation works: return false
Example:
numCourses = 3
prerequisites = [[1,0], [2,1]]
Permutations to try:
[0,1,2]: 0โ1 โ
, 1โ2 โ
โ Valid! โ
[0,2,1]: 0โ1 โ (1 comes after 2)
[1,0,2]: 1 needs 0, but 1 comes first โ
[1,2,0]: Same problem โ
[2,0,1]: 2 needs 1, but 1 comes last โ
[2,1,0]: 2 needs 1 โ
, 1 needs 0 โ
Found valid: [0,1,2] โ return true
Problem: EXPONENTIAL! ๐ฑ
Time complexity:
n! permutations
Each takes O(E) to verify (E = prerequisites)
Total: O(n! ร E) ๐
For 10 courses: 3,628,800 permutations! โ
For 20 courses: 2.4 ร 10^18 permutations! ๐
Completely infeasible! ๐ซ
Part 2: The Realization - "It's cycle detection!" ๐ญ
๐ค Key insight: Cycles make completion impossible!
Why cycles are the problem:
A โ B โ C โ A
To take A, need C
To take C, need B
To take B, need A
โณ Infinite loop! Can't start!
Reframe the problem:
"Can finish all courses?"
โบ
"Is the dependency graph acyclic?" (DAG)
If DAG: โ
Can finish (topological order exists)
If has cycle: โ Cannot finish
So we need: CYCLE DETECTION! ๐
This is a GRAPH problem!
Nodes: Courses
Edges: Prerequisites
Question: Is graph a DAG?
Two classic approaches:
1. DFS with cycle detection
2. BFS (Kahn's algorithm) - topological sort
Both: O(V + E) time! โก
Much better than O(n!)! ๐
Part 3: The DFS Approach - "Three states for cycle detection!" ๐ก
๐ค DFS cycle detection using THREE states:
State meanings:
UNVISITED (0): Haven't explored yet โช
VISITING (1): Currently exploring (on path) ๐ก
VISITED (2): Completely explored โ
Why three states?
Two states (visited/unvisited) insufficient!
Need to track "currently on recursion path"
VISITING state = "on current DFS path"
If we reach VISITING node again = CYCLE! ๐
Example trace:
Graph: 0 โ 1 โ 2 โ 3
DFS from 0:
Mark 0 VISITING ๐ก
Explore neighbor 1:
Mark 1 VISITING ๐ก
Explore neighbor 2:
Mark 2 VISITING ๐ก
Explore neighbor 3:
Mark 3 VISITING ๐ก
No neighbors
Mark 3 VISITED โ
Back to 2
Mark 2 VISITED โ
Back to 1
Mark 1 VISITED โ
Back to 0
Mark 0 VISITED โ
No cycles! Return true! โ
Example with cycle:
Graph: 0 โ 1 โ 2 โ 1
DFS from 0:
Mark 0 VISITING ๐ก
Explore neighbor 1:
Mark 1 VISITING ๐ก
Explore neighbor 2:
Mark 2 VISITING ๐ก
Explore neighbor 1:
1 is VISITING! ๐ด
CYCLE DETECTED! โณ
Return false โ
Three states are KEY! ๐
Part 4: The BFS Approach - "Kahn's algorithm!" ๐ก
๐ค Alternative: BFS-based topological sort (Kahn's algorithm)
Key idea: Process nodes with no dependencies first!
Algorithm:
1. Calculate in-degree for each node
(in-degree = number of prerequisites)
2. Start with nodes that have in-degree 0
(no prerequisites - can take immediately)
3. Process these nodes:
- "Take" the course
- Decrease in-degree of dependent courses
- If dependent course now has in-degree 0, add to queue
4. If processed all courses: DAG! โ
If some remain: cycle exists! โ
Example:
Graph: 0 โ 1 โ 2
In-degrees: [0: 0, 1: 1, 2: 1]
Step 1: Queue = [0] (in-degree 0)
Process 0, decrease in-degree of 1
In-degrees: [0: -, 1: 0, 2: 1]
Queue = [1]
Step 2: Process 1, decrease in-degree of 2
In-degrees: [0: -, 1: -, 2: 0]
Queue = [2]
Step 3: Process 2
Queue = []
Processed 3 courses = all! โ
Return true!
Example with cycle:
Graph: 0 โ 1 โ 0 (cycle)
In-degrees: [0: 1, 1: 1]
Queue = [] (no in-degree 0!)
Can't process anything!
Processed 0 courses โ total
Return false! โ
Both approaches work! Choose based on preference! ๐ฏ
Part 5: The Pattern - "Topological sort problems!" ๐ฏ
โจ This is a TOPOLOGICAL SORT problem!
Pattern recognition:
โ
Dependencies between items
โ
Need to order items respecting dependencies
โ
Check if valid ordering exists
โ
Detect circular dependencies
Related problems with same pattern:
- Course Schedule (this one)
- Course Schedule II (return actual order)
- Alien Dictionary (deduce alphabet order)
- Sequence Reconstruction (verify unique order)
Two standard approaches:
1. DFS + Cycle Detection:
- Three-state tracking
- If cycle: return false
- If no cycle: return true
- Time: O(V + E) โก
2. BFS (Kahn's Algorithm):
- In-degree tracking
- Process zero in-degree nodes
- Count processed nodes
- Time: O(V + E) โก
Both have same complexity!
DFS: More intuitive for cycle detection
BFS: Naturally gives topological order
For this problem (just true/false):
Either works perfectly! ๐ฏ
Time: O(V + E) where V = courses, E = prerequisites
Space: O(V + E) for graph representation
Pattern mastered! ๐
๐ฏ Approach 1: DFS with Cycle Detection
The Key Insight ๐ก
Use DFS with three-state tracking to detect cycles ๐. States: UNVISITED (0), VISITING (1), VISITED (2) ๐กโ . Start DFS from each unvisited course ๐. If reach VISITING node = cycle found! โ If all DFS complete without cycle = DAG! โ Return true if no cycles, false otherwise ๐ฏ. Time: O(V+E) โก, Space: O(V+E) ๐ฆ!
Direct cycle detection! โจ
Complete Implementation
class Solution {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// THREE STATES FOR CYCLE DETECTION ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private static final int UNVISITED = 0; // Not explored โช
private static final int VISITING = 1; // On current path ๐ก
private static final int VISITED = 2; // Fully explored โ
public boolean canFinish(int numCourses, int[][] prerequisites) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DFS CYCLE DETECTION APPROACH ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Build graph and detect cycles using DFS
// If cycle found: return false โ
// If no cycle: return true โ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// BUILD ADJACENCY LIST ๐๏ธ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Add edges: [a,b] means b โ a
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int[] prereq : prerequisites) {
int course = prereq[0];
int prerequisite = prereq[1];
graph.get(prerequisite).add(course); // prereq โ course
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// STATE TRACKING ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int[] state = new int[numCourses]; // All start UNVISITED (0)
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DFS FROM EACH UNVISITED COURSE ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int course = 0; course < numCourses; course++) {
if (state[course] == UNVISITED) {
if (hasCycle(graph, state, course)) {
return false; // Cycle detected! โ
}
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// NO CYCLES FOUND! โ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
return true; // Can finish all courses! ๐
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DFS HELPER: DETECT CYCLE ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
private boolean hasCycle(List<List<Integer>> graph, int[] state, int course) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// MARK AS VISITING (on current path) ๐ก
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
state[course] = VISITING;
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// EXPLORE ALL NEIGHBORS ๐งญ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int neighbor : graph.get(course)) {
if (state[neighbor] == VISITING) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// BACK EDGE! Cycle detected! ๐ด
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
return true; // Found cycle! โณ
}
if (state[neighbor] == UNVISITED) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Continue DFS ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (hasCycle(graph, state, neighbor)) {
return true; // Cycle in subtree!
}
}
// If VISITED: already fully explored, skip โ
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// MARK AS VISITED (fully explored) โ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
state[course] = VISITED;
return false; // No cycle from this course!
}
}
Why This Works ๐ฏ
Three states enable cycle detection: ๐ - UNVISITED: Haven't explored yet - VISITING: Currently on DFS path (KEY!) - VISITED: Completely explored
VISITING state detects cycles: ๐ - If we reach VISITING node = back edge - Back edge = cycle in directed graph! - Return false immediately!
VISITED prevents re-exploration: โก - Already processed, no cycle from here - Skip without recursing - Saves time!
๐ฏ Approach 2: BFS (Kahn's Algorithm)
The Key Insight ๐ก
Use Kahn's algorithm for topological sort attempt ๐. Calculate in-degree for each course (count prerequisites) ๐. Start with courses having in-degree 0 (no prerequisites) โ . Process them: "take" course, decrease neighbors' in-degrees, add newly-zero to queue ๐. Count processed courses ๐. If count = total, DAG! โ Otherwise cycle exists! โ Time: O(V+E) โก, Space: O(V+E) ๐ฆ!
Topological sort approach! โจ
Complete Implementation
class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// KAHN'S ALGORITHM (BFS TOPOLOGICAL SORT) ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Try to build topological order
// If successful (all processed): DAG โ
// If incomplete: cycle exists โ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// BUILD ADJACENCY LIST ๐๏ธ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// CALCULATE IN-DEGREES ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int[] inDegree = new int[numCourses];
for (int[] prereq : prerequisites) {
int course = prereq[0];
int prerequisite = prereq[1];
graph.get(prerequisite).add(course); // prereq โ course
inDegree[course]++; // course has one more prerequisite
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// FIND COURSES WITH NO PREREQUISITES ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Queue<Integer> queue = new LinkedList<>();
for (int course = 0; course < numCourses; course++) {
if (inDegree[course] == 0) {
queue.offer(course); // Can take immediately! โ
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// BFS: PROCESS COURSES ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
int processedCount = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
processedCount++; // "Take" this course โ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// DECREASE IN-DEGREE OF DEPENDENT COURSES ๐
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
for (int dependent : graph.get(course)) {
inDegree[dependent]--;
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// IF NOW READY, ADD TO QUEUE ๐ฏ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
if (inDegree[dependent] == 0) {
queue.offer(dependent);
}
}
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// CHECK IF ALL COURSES PROCESSED โ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// If processed all: no cycle (DAG) โ
// If some remain: cycle exists โ
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
return processedCount == numCourses;
}
}
Why This Works ๐ฏ
In-degree tracks prerequisites: ๐ - in-degree[i] = number of courses needed before i - When in-degree = 0, course is ready!
BFS processes in topological order: ๐ - Start with zero in-degree (no dependencies) - Process course, "remove" edges - Newly-zero courses become ready - Natural topological ordering!
Count reveals cycles: ๐ - If DAG: can process all courses - If cycle: some courses never reach zero in-degree - processedCount < total = cycle! โ
๐ Detailed Dry Run
Let's trace Approach 1 (DFS) on a complex example:
numCourses = 4
prerequisites = [[1,0], [2,0], [3,1], [3,2]]
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SETUP โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Build graph from prerequisites:
[1,0]: 0 โ 1
[2,0]: 0 โ 2
[3,1]: 1 โ 3
[3,2]: 2 โ 3
Adjacency list:
0: [1, 2]
1: [3]
2: [3]
3: []
Graph visualization:
0
/ \
โ โ
1 2
\ /
โ
3
State array: [0, 0, 0, 0] (all UNVISITED)
DFS Exploration ๐
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ DFS FROM COURSE 0 โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
โ state[0] = UNVISITED โ Start DFS โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ hasCycle(graph, state, 0): โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ Mark 0 as VISITING ๐ก โ
โ state = [1, 0, 0, 0] โ
โ โ
โ Explore neighbors of 0: [1, 2] โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Neighbor 1: โโ
โ โ state[1] = UNVISITED โ Recurse โโ
โ โ โโ
โ โ hasCycle(graph, state, 1): โโ
โ โ โโ
โ โ Mark 1 as VISITING ๐ก โโ
โ โ state = [1, 1, 0, 0] โโ
โ โ โโ
โ โ Explore neighbors of 1: [3] โโ
โ โ โโ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโ
โ โ โ Neighbor 3: โ โโ
โ โ โ state[3] = UNVISITED โ Recurse โ โโ
โ โ โ โ โโ
โ โ โ hasCycle(graph, state, 3): โ โโ
โ โ โ โ โโ
โ โ โ Mark 3 as VISITING ๐ก โ โโ
โ โ โ state = [1, 1, 0, 1] โ โโ
โ โ โ โ โโ
โ โ โ Neighbors of 3: [] (empty) โ โโ
โ โ โ No neighbors to explore โ โโ
โ โ โ โ โโ
โ โ โ Mark 3 as VISITED โ
โ โโ
โ โ โ state = [1, 1, 0, 2] โ โโ
โ โ โ โ โโ
โ โ โ Return false (no cycle) โ โโ
โ โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โโ
โ โ โโ
โ โ Back to course 1 โโ
โ โ No cycle found from 3 โ
โโ
โ โ โโ
โ โ Mark 1 as VISITED โ
โโ
โ โ state = [1, 2, 0, 2] โโ
โ โ โโ
โ โ Return false (no cycle) โโ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ Back to course 0 โ
โ No cycle from neighbor 1 โ
โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ Neighbor 2: โโ
โ โ state[2] = UNVISITED โ Recurse โโ
โ โ โโ
โ โ hasCycle(graph, state, 2): โโ
โ โ โโ
โ โ Mark 2 as VISITING ๐ก โโ
โ โ state = [1, 2, 1, 2] โโ
โ โ โโ
โ โ Explore neighbors of 2: [3] โโ
โ โ โโ
โ โ Neighbor 3: โโ
โ โ state[3] = VISITED โ
โโ
โ โ Already fully explored, skip! โโ
โ โ โโ
โ โ Mark 2 as VISITED โ
โโ
โ โ state = [1, 2, 2, 2] โโ
โ โ โโ
โ โ Return false (no cycle) โโ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ โ
โ Back to course 0 โ
โ No cycle from neighbor 2 โ
โ
โ โ
โ All neighbors explored, no cycles! โ
โ โ
โ Mark 0 as VISITED โ
โ
โ state = [2, 2, 2, 2] โ
โ โ
โ Return false (no cycle from course 0) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ CHECK REMAINING COURSES โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
โ Course 1: state[1] = VISITED โ
Skip โ
โ Course 2: state[2] = VISITED โ
Skip โ
โ Course 3: state[3] = VISITED โ
Skip โ
โ โ
โ All courses checked! โ
โ No cycles found anywhere! โ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
FINAL RESULT ๐
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ๐ FINAL RESULT: true โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
โ โ
โ Graph is a DAG (no cycles)! โ
โ
โ Can finish all courses! ๐ โ
โ โ
โ Valid schedule exists: โ
โ 0 โ 1 โ 2 โ 3 โ
โ or 0 โ 2 โ 1 โ 3 โ
โ โ
โ State progression: โ
โ Start: [0, 0, 0, 0] (all UNVISITED) โ
โ End: [2, 2, 2, 2] (all VISITED) โ
โ โ
โ No VISITING state encountered twice! ๐ โ
โ No cycles detected! โณโ โ
โ โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Key Observations:
-
Three-state tracking worked perfectly: ๐กโ VISITING โ VISITED progression
-
DFS explored all paths: ๐ Complete graph traversal
-
VISITED optimization: โก Course 3 skipped second time (already explored)
-
No back edges detected: ๐ฏ Never reached VISITING node from VISITING path
-
Result: DAG confirmed: โ Can complete all courses!
๐ Complexity Analysis
Time Complexity: O(V + E) โก
Where:
V = numCourses (vertices)
E = prerequisites.length (edges)
Both approaches (DFS and BFS):
Build graph: O(E)
- Process each prerequisite once
- Add to adjacency list
Approach 1 (DFS):
- Visit each course once: O(V)
- Explore each edge once: O(E)
- Total: O(V + E) โ
Approach 2 (BFS):
- Calculate in-degrees: O(E)
- BFS processes each course once: O(V)
- Process each edge once: O(E)
- Total: O(V + E) โ
Overall: O(V + E) for both! โก
Example: 2000 courses, 5000 prerequisites
Time: 2000 + 5000 = 7000 operations
Very efficient! ๐
Space Complexity: O(V + E) ๐ฆ
Space usage:
1. Graph (adjacency list): O(V + E) ๐๏ธ
- V lists (one per course)
- Total E entries across all lists
2. Approach 1 (DFS):
- State array: O(V)
- Recursion stack: O(V) worst case
- Total: O(V + E) โ
3. Approach 2 (BFS):
- In-degree array: O(V)
- Queue: O(V) worst case
- Total: O(V + E) โ
Both approaches: O(V + E) ๐ฆ
Example: 2000 courses, 5000 prerequisites
Graph: ~7000 space
Additional: 2000 space
Total: ~9000 space units
Reasonable! โ
๐ Related Problems & Patterns
Core Pattern: Topological Sort & Cycle Detection ๐
// Template for topological sort problems
class Solution {
// DFS approach with cycle detection
public boolean topologicalSortDFS(int n, int[][] edges) {
List<List<Integer>> graph = buildGraph(n, edges);
int[] state = new int[n]; // Three states
for (int i = 0; i < n; i++) {
if (state[i] == UNVISITED) {
if (hasCycle(graph, state, i)) {
return false; // Cycle found
}
}
}
return true; // DAG
}
// BFS approach (Kahn's algorithm)
public boolean topologicalSortBFS(int n, int[][] edges) {
List<List<Integer>> graph = buildGraph(n, edges);
int[] inDegree = new int[n];
// Calculate in-degrees
for (int[] edge : edges) {
inDegree[edge[0]]++;
}
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < n; i++) {
if (inDegree[i] == 0) queue.offer(i);
}
int count = 0;
while (!queue.isEmpty()) {
int node = queue.poll();
count++;
for (int neighbor : graph.get(node)) {
if (--inDegree[neighbor] == 0) {
queue.offer(neighbor);
}
}
}
return count == n;
}
}
When to Use This Pattern ๐ฏ
โ
Dependencies between items
โ
Check if ordering is possible
โ
Detect circular dependencies
โ
Topological ordering needed
โ
Prerequisite relationships
Recognition triggers: ๐ - "Prerequisites" or "dependencies" ๐ - "Order" or "sequence" ๐ - "Can finish all" or "is possible" โ - "Circular dependency" detection ๐ - Directed graph with constraints ๐ฏ
Key insight: Topological sort exists โบ DAG (no cycles)! ๐ก
Related LeetCode Problems ๐
Direct Applications:
-
Course Schedule II (LC 210) - Medium ๐ โข What's similar: Same prerequisites, cycle detection ๐ โข What's different: Return actual order, not just boolean ๐ โข Key insight: Add ordering during DFS/BFS! โจ
-
Alien Dictionary (LC 269) - Hard ๐ฝ โข What's similar: Build graph, topological sort ๐ โข What's different: Deduce graph from word order ๐ โข Key insight: Compare adjacent words for edges! ๐ก
-
Sequence Reconstruction (LC 444) - Medium ๐ข โข What's similar: Topological sort, unique ordering ๐ฏ โข What's different: Verify if sequences give unique order โ โข Key insight: Check uniqueness during Kahn's! ๐
-
Minimum Height Trees (LC 310) - Medium ๐ณ โข What's similar: Graph, remove leaves (like in-degree 0) ๐ โข What's different: Undirected graph, find centers ๐ฏ โข Key insight: Topological sort variation! โก
Pattern Evolution: ๐
Level 1: Basic Cycle Detection ๐
โโ Course Schedule (LC 207) โ HERE ๐ฏ
โโ Detect cycle in directed graph
Level 2: Topological Sort Output ๐
โโ Course Schedule II (LC 210)
โโ Alien Dictionary (LC 269)
โโ Sequence Reconstruction (LC 444)
Level 3: Advanced Variations ๐
โโ Parallel Courses (LC 1136, 1494)
โโ Sort Items by Groups (LC 1203)
โโ Find All Recipes (LC 2115)
๐ Quick Revision Notes
๐ฏ Core Concept
Course Schedule detects if courses can be completed using cycle detection ๐. Prerequisites form directed graph (course dependencies) ๐โก๏ธ๐. Cycle = impossible (circular dependency) โ. DAG = possible (valid schedule exists) โ . Two approaches: DFS with three states (UNVISITED, VISITING, VISITED) or BFS (Kahn's) with in-degree tracking ๐. Both O(V+E) time โก. Return true if DAG, false if cycle! This is topological sort pattern! ๐ฏ
โก Quick Implementation (DFS)
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.Queue;
public class Solution {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// // Approach 1 - BFS (kahn)
// return bfs(numCourses, prerequisites);
// Approach 2 - DFS (plus stack)
return dfs(numCourses, prerequisites);
}
private boolean dfs(int numCourses, int[][] prerequisites) {
// Step 1: create graph (list of list) - no indegrees
ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
for (int i = 0; i < prerequisites.length; i++) {
graph.get(prerequisites[i][0]).add(prerequisites[i][1]);
}
// State 2: Create and track status of nodes (0, 1, 2) with 0 being default.
// 0 - UNVISITED, 1 - VISITING and 2 - VISITED
int[] state = new int[numCourses];
// State 3: Loop every course and detch to find cycle.
// If loop found, return false. Else, return true.
for (int i = 0; i < numCourses; i++) {
if (state[i] == 0) {
if (dfsUtil(i, state, graph)) { // has cycle?
return false;
}
}
}
return true;
}
private boolean dfsUtil(int course, int[] state, ArrayList<ArrayList<Integer>> graph) {
// Step 4: change the state of course being visited or considered.
state[course] = 1;
for (int depCourse : graph.get(course)) {
// Step 5: check the state of dependent course.
// If cycle is there, it gets detected here.
if (state[depCourse] == 1) {
return true;
}
// Prevents infinite loop (acts as visited)
if (state[depCourse] == 0) {
// Else, do a DFS from here.
if (dfsUtil(depCourse, state, graph)) {
return true;
}
}
}
// Step 6: Mark the node as visited
state[course] = 2;
return false;
}
private boolean bfs(int numCourses, int[][] prerequisites) {
// Step 1: create graph (list of list) and indegrees (array).
ArrayList<ArrayList<Integer>> graph = new ArrayList<>();
int[] indegrees = new int[numCourses];
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
for (int i = 0; i < prerequisites.length; i++) {
graph.get(prerequisites[i][0]).add(prerequisites[i][1]);
indegrees[prerequisites[i][1]]++;
}
// Step 2: find courses with no dependencies (indegrees with 0 value)
Queue<Integer> queue = new LinkedList<>();
for (int i = 0; i < numCourses; i++) {
if (indegrees[i] == 0) {
queue.offer(i);
}
}
// Step 3: explore queue
int coursesTaken = 0;
while (!queue.isEmpty()) {
int course = queue.poll();
coursesTaken++; // course taken
for (int depCourse : graph.get(course)) {
indegrees[depCourse]--;
if (indegrees[depCourse] == 0) { // no dependancy anymore
queue.offer(depCourse);
}
}
}
return coursesTaken == numCourses;
}
public static void main(String[] args) {
Solution s = new Solution();
System.out.println(s.canFinish(2, new int[][] { { 1, 0 } })); // true
System.out.println(s.canFinish(2, new int[][] { { 1, 0 }, { 0, 1 } })); // false
}
}
๐ Key Insights
Three States are Essential: ๐
Why three states for DFS cycle detection?
UNVISITED (0): Haven't explored โช
VISITING (1): Currently on DFS path ๐ก
VISITED (2): Completely explored โ
Example showing why two states fail:
Graph: 0 โ 1 โ 2, 0 โ 2
With only two states (visited/unvisited):
DFS from 0:
Visit 0 (mark visited)
Go to 1:
Visit 1 (mark visited)
Go to 2:
Visit 2 (mark visited) โ
Back to 0, try other neighbor 2:
2 is visited!
Think it's a cycle? โ WRONG!
The problem: Can't distinguish:
- Back edge (cycle): VISITING โ VISITING
- Cross edge (no cycle): VISITING โ VISITED
With three states:
When at 0, going to 2:
2 is VISITED (not VISITING)
Not a cycle! โ
VISITING = "on current path" ๐
Only VISITING โ VISITING is a cycle! ๐
Edge Direction Matters: โก๏ธ
CRITICAL: [a, b] means b โ a!
Example: [1, 0]
Means: "To take 1, need 0 first"
Edge: 0 โ 1 (not 1 โ 0!)
When building graph:
prerequisites[i] = [course, prerequisite]
graph.get(prerequisite).add(course) โ
NOT: graph.get(course).add(prerequisite) โ
Getting direction wrong = wrong answer! ๐ซ
DFS vs BFS Trade-offs: โ๏ธ
DFS Approach (Three States):
โ
More intuitive for cycle detection
โ
Natural recursion
โ
Easier to understand "back edge"
โ Recursion stack space (deep graphs)
BFS Approach (Kahn's Algorithm):
โ
Iterative (no stack overflow risk)
โ
Naturally gives topological order
โ
Clear in-degree logic
โ Slightly more code
Both: O(V + E) time, O(V + E) space
For Course Schedule (just true/false):
Either works perfectly! ๐ฏ
For Course Schedule II (need order):
BFS slightly easier (order is processing order)
DFS needs post-order collection
Topological Sort Existence: ๐
KEY THEOREM:
Topological sort exists โบ Graph is DAG
Why cycles prevent topological sort:
A โ B โ C โ A (cycle)
In topological order:
A must come before B (A โ B)
B must come before C (B โ C)
C must come before A (C โ A)
Contradiction! Can't satisfy all! โ
This problem leverages this theorem:
Can finish courses โบ DAG โบ No cycles
Just check for cycles! ๐ฏ
๐ช Memory Aid
"Three states track the path we roam" ๐กโ โช
"VISITING marks the journey home" ๐
"If VISITING found again, there's a cycle shown" ๐ด
"No cycles? DAG! Courses can be known" ๐ โจ
โ ๏ธ Don't Forget
- Three states in DFS - UNVISITED, VISITING, VISITED! ๐กโ
- Edge direction - [a,b] means bโa (prerequisite first)! โก๏ธ
- VISITING โ VISITING - that's the cycle! ๐ด
- Count in BFS - if count < total, cycle exists! ๐
- Both O(V+E) - same complexity for DFS and BFS! โก
- Topological sort - exists only for DAGs! ๐ฏ
You've mastered Course Schedule - topological sort and cycle detection pattern! ๐๐โจ