210. Course Schedule II - Explanation
Description
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.
There are a total of numCourses courses you are required to take, labeled from 0 to numCourses - 1.
Return a valid ordering of courses you can take to finish all courses. If there are many valid answers, return any of them. If it's not possible to finish all courses, return an empty array.
Example 1:
Input: numCourses = 3, prerequisites = [[1,0]]
Output: [0,1,2]Explanation: We must ensure that course 0 is taken before course 1.
Example 2:
Input: numCourses = 3, prerequisites = [[0,1],[1,2],[2,0]]
Output: []Explanation: It's impossible to finish all courses.
Constraints:
1 <= numCourses <= 10000 <= prerequisites.length <= 1000- All
prerequisitepairs are unique.
Topics
Recommended Time & Space Complexity
You should aim for a solution with O(V + E) time and O(V + E) space, where V is the number of courses (nodes) and E is the number of prerequisites (edges).
Hint 1
Consider the problem as a graph where courses represent the nodes, and prerequisite[i] = [a, b] represents a directed edge from a to b. We need to determine whether the graph contains a cycle. Why? Because if there is a cycle, it is impossible to complete the courses involved in the cycle. Can you think of an algorithm to detect a cycle in a graph and also find the valid ordering if a cycle doesn't exist?
Hint 2
We can use DFS to detect a cycle in a graph. However, we also need to find the valid ordering of the courses, which can also be achieved using DFS. Alternatively, we can use the Topological Sort algorithm to find the valid ordering in this directed graph, where the graph must be acyclic to complete all the courses, and the prerequisite of a course acts as the parent node of that course. How would you implement this?
Hint 3
We compute the indegrees of all the nodes. Then, we perform a BFS starting from the nodes that have no parents (indegree[node] == 0). At each level, we traverse these nodes, decrement the indegree of their child nodes, and append those child nodes to the queue if their indegree becomes 0. We only append nodes whose indegree is 0 or becomes 0 during the BFS to our result array. If the length of the result array is not equal to the number of courses, we return an empty array.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Graph Representation - Building adjacency lists from edge pairs to represent directed graphs
- Depth-First Search (DFS) - Recursive graph traversal for exploring all paths and detecting cycles
- Topological Sort - Ordering nodes in a directed acyclic graph (DAG) so dependencies come first
- Cycle Detection - Identifying cycles in directed graphs using visited/visiting state tracking
1. Cycle Detection (DFS)
Intuition
Each course is a node, and each prerequisite is a directed edge.
We want an order of courses such that all prerequisites of a course are taken before it.
Using DFS, we:
- Detect cycles (which make it impossible to finish all courses)
- Add a course to the result after all its prerequisites are processed
(this naturally gives a valid topological order)
Algorithm
- Build a graph where each course points to its prerequisites.
- Use two sets:
cycle- tracks the current DFS path (for cycle detection)visit- tracks fully processed courses
- For each course, run DFS:
- If the course is already in
cycle, a cycle exists - return empty list - DFS all prerequisites first
- After processing prerequisites, add the course to the result
- If the course is already in
- If all DFS calls succeed, return the result list (valid course order)
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
prereq = {c: [] for c in range(numCourses)}
for crs, pre in prerequisites:
prereq[crs].append(pre)
output = []
visit, cycle = set(), set()
def dfs(crs):
if crs in cycle:
return False
if crs in visit:
return True
cycle.add(crs)
for pre in prereq[crs]:
if dfs(pre) == False:
return False
cycle.remove(crs)
visit.add(crs)
output.append(crs)
return True
for c in range(numCourses):
if dfs(c) == False:
return []
return outputTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of courses and is the number of prerequisites.
2. Topological Sort (Kahn's Algorithm)
Intuition
Treat each course as a node and each prerequisite as a directed edge.
A course can be taken only when all its prerequisites are completed.
Kahn's Algorithm works by:
- Always taking courses with no remaining prerequisites (
indegree = 0) - Removing them from the graph
- Gradually unlocking other courses
If at the end some courses are still locked, it means a cycle exists, so no valid order is possible.
Algorithm
- Build a graph and compute
indegreefor each course
(indegree= number of prerequisites). - Add all courses with
indegree = 0to a queue. - While the queue is not empty:
- Remove a course and add it to the result.
- Reduce the
indegreeof its dependent courses. - If any dependent course reaches
indegree = 0, add it to the queue.
- If all courses are processed, return the result (reverse if edges were stored as course - prerequisite).
- Otherwise, return an empty list (cycle detected).
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
indegree = [0] * numCourses
adj = [[] for i in range(numCourses)]
for src, dst in prerequisites:
indegree[dst] += 1
adj[src].append(dst)
q = deque()
for n in range(numCourses):
if indegree[n] == 0:
q.append(n)
finish, output = 0, []
while q:
node = q.popleft()
output.append(node)
finish += 1
for nei in adj[node]:
indegree[nei] -= 1
if indegree[nei] == 0:
q.append(nei)
if finish != numCourses:
return []
return output[::-1]Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of courses and is the number of prerequisites.
3. Topological Sort (DFS)
Intuition
We want an order of courses such that every course appears after its prerequisites.
This approach mixes Topological Sorting with DFS-style traversal.
The idea is:
- Start from courses that have no prerequisites (
indegree = 0) - Once we take a course, we "remove" it by decreasing the
indegreeof courses that depend on it - When a dependent course's
indegreebecomes0, it is now safe to take, so we continue DFS from it
If we can visit all courses this way, a valid order exists.
If not, a cycle is present, making it impossible.
Algorithm
- Build a directed graph from prerequisites and compute
indegreefor each course. - For every course with
indegree = 0, start a DFS. - In DFS:
- Add the current course to the result.
- Decrease
indegreeof its neighbors. - If any neighbor's
indegreebecomes0, recursively DFS on it.
- After traversal:
- If result contains all courses, return it.
- Otherwise, return an empty list (cycle detected).
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
adj = [[] for i in range(numCourses)]
indegree = [0] * numCourses
for nxt, pre in prerequisites:
indegree[nxt] += 1
adj[pre].append(nxt)
output = []
def dfs(node):
output.append(node)
indegree[node] -= 1
for nei in adj[node]:
indegree[nei] -= 1
if indegree[nei] == 0:
dfs(nei)
for i in range(numCourses):
if indegree[i] == 0:
dfs(i)
return output if len(output) == numCourses else []Time & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of courses and is the number of prerequisites.
Common Pitfalls
Confusing Cycle Detection with Visited Tracking
In DFS-based topological sort, you need two separate tracking mechanisms: one for nodes currently in the DFS path (for cycle detection) and one for fully processed nodes. Using only one set leads to either false cycle detection or infinite loops.
# Wrong: Using single visited set
def dfs(course):
if course in visited:
return False # Incorrectly treats revisit as cycle
visited.add(course)
for pre in prereq[course]:
if not dfs(pre):
return False
return True
# Correct: Separate cycle and visit tracking
def dfs(course):
if course in cycle: # Currently in path = cycle
return False
if course in visit: # Already processed = skip
return True
cycle.add(course)
for pre in prereq[course]:
if not dfs(pre):
return False
cycle.remove(course)
visit.add(course)
return TrueBuilding the Graph in the Wrong Direction
The prerequisite pair [a, b] means "to take course a, you must first take course b". Building edges in the wrong direction results in an incorrect topological order.
# Wrong: Edge direction reversed
for crs, pre in prerequisites:
adj[pre].append(crs) # This builds "pre -> crs" but for Kahn's we may need opposite
# The correct direction depends on your algorithm:
# For DFS that adds after processing: course -> prerequisites
# For Kahn's: prerequisite -> course (then reverse) or course -> prerequisiteForgetting to Handle Disconnected Courses
Courses with no prerequisites and no dependents still need to be included in the output. Forgetting to iterate over all courses means some valid courses might be missing from the result.
# Wrong: Only processing courses that appear in prerequisites
for crs, pre in prerequisites:
# Only touches courses mentioned in prerequisites
# Correct: Process all courses from 0 to numCourses-1
for i in range(numCourses):
if indegree[i] == 0:
queue.append(i)
Sign in to join the discussion