questions C
This commit is contained in:
212
backend/data/questions/course-schedule-ii.yaml
Normal file
212
backend/data/questions/course-schedule-ii.yaml
Normal file
@@ -0,0 +1,212 @@
|
||||
title: Course Schedule II
|
||||
slug: course-schedule-ii
|
||||
difficulty: medium
|
||||
leetcode_id: 210
|
||||
leetcode_url: https://leetcode.com/problems/course-schedule-ii/
|
||||
categories:
|
||||
- graphs
|
||||
patterns:
|
||||
- bfs
|
||||
- dfs
|
||||
|
||||
description: |
|
||||
There are a total of `numCourses` courses you have to take, labelled from `0` to `numCourses - 1`. You are given an array `prerequisites` where `prerequisites[i] = [a_i, b_i]` indicates that you **must** take course `b_i` first if you want to take course `a_i`.
|
||||
|
||||
For example, the pair `[0, 1]` indicates that to take course `0` you have to first take course `1`.
|
||||
|
||||
Return *the ordering of courses you should take to finish all courses*. If there are many valid answers, return **any** of them. If it is impossible to finish all courses, return **an empty array**.
|
||||
|
||||
constraints: |
|
||||
- `1 <= numCourses <= 2000`
|
||||
- `0 <= prerequisites.length <= numCourses * (numCourses - 1)`
|
||||
- `prerequisites[i].length == 2`
|
||||
- `0 <= a_i, b_i < numCourses`
|
||||
- `a_i != b_i`
|
||||
- All the pairs `[a_i, b_i]` are **distinct**
|
||||
|
||||
examples:
|
||||
- input: "numCourses = 2, prerequisites = [[1,0]]"
|
||||
output: "[0,1]"
|
||||
explanation: "There are a total of 2 courses to take. To take course 1 you should have finished course 0. So the correct course order is [0,1]."
|
||||
- input: "numCourses = 4, prerequisites = [[1,0],[2,0],[3,1],[3,2]]"
|
||||
output: "[0,2,1,3]"
|
||||
explanation: "There are a total of 4 courses to take. To take course 3 you should have finished both courses 1 and 2. Both courses 1 and 2 should be taken after you finished course 0. So one correct course order is [0,1,2,3]. Another correct ordering is [0,2,1,3]."
|
||||
- input: "numCourses = 1, prerequisites = []"
|
||||
output: "[0]"
|
||||
explanation: "There is only one course with no prerequisites, so the order is simply [0]."
|
||||
|
||||
explanation:
|
||||
intuition: |
|
||||
Imagine you're planning which university courses to take each semester. Some courses have prerequisites — you can't take Advanced Algorithms until you've completed Data Structures. This creates a natural ordering constraint.
|
||||
|
||||
The problem is asking us to find a **topological ordering** of the courses. Think of it like stacking books: you can only place a book on top of another if all its "dependencies" are already in the stack below. If there's a circular dependency (Book A requires Book B, and Book B requires Book A), it's impossible to stack them — we'd return an empty array.
|
||||
|
||||
The key insight is recognising this as a **directed graph problem**. Each course is a node, and each prerequisite `[a, b]` creates an edge from `b` to `a` (meaning "b must come before a"). We need to find an ordering where every edge points "forward" — that's exactly what topological sort does.
|
||||
|
||||
There are two classic approaches: **Kahn's Algorithm (BFS)** processes courses with no remaining prerequisites first, while **DFS-based** approach explores deeply and adds courses to the result in reverse post-order.
|
||||
|
||||
approach: |
|
||||
We'll use **Kahn's Algorithm (BFS)** — it's intuitive and naturally handles cycle detection:
|
||||
|
||||
**Step 1: Build the graph and calculate in-degrees**
|
||||
|
||||
- Create an adjacency list to represent the graph: for each prerequisite `[a, b]`, add `a` to the list of courses that depend on `b`
|
||||
- Calculate the **in-degree** of each course: how many prerequisites it has
|
||||
- In-degree tells us how many courses must be completed before we can take this one
|
||||
|
||||
|
||||
|
||||
**Step 2: Initialise the queue with "ready" courses**
|
||||
|
||||
- Add all courses with in-degree `0` to a queue — these have no prerequisites and can be taken immediately
|
||||
- These are our starting points
|
||||
|
||||
|
||||
|
||||
**Step 3: Process courses level by level (BFS)**
|
||||
|
||||
- Dequeue a course, add it to the result
|
||||
- For each course that depends on the dequeued course, decrement its in-degree
|
||||
- If any course's in-degree becomes `0`, it's now "unlocked" — add it to the queue
|
||||
- Repeat until the queue is empty
|
||||
|
||||
|
||||
|
||||
**Step 4: Check for cycles**
|
||||
|
||||
- If the result contains all `numCourses` courses, return it
|
||||
- If not, a cycle exists (some courses could never reach in-degree `0`) — return an empty array
|
||||
|
||||
|
||||
|
||||
This approach works because we only process a course when all its prerequisites have been processed, naturally building a valid ordering.
|
||||
|
||||
common_pitfalls:
|
||||
- title: Cycle Detection Failure
|
||||
description: |
|
||||
The most critical edge case is detecting **cycles** in the prerequisites. If course A requires B, B requires C, and C requires A, no valid ordering exists.
|
||||
|
||||
With Kahn's Algorithm, cycles are detected automatically: courses in a cycle never reach in-degree `0`, so they're never added to the queue. If our result has fewer than `numCourses` entries, we know there's a cycle.
|
||||
|
||||
Forgetting this check would return an incomplete ordering instead of an empty array.
|
||||
wrong_approach: "Return partial ordering without checking length"
|
||||
correct_approach: "Verify result length equals numCourses"
|
||||
|
||||
- title: Confusing Edge Direction
|
||||
description: |
|
||||
The prerequisite `[a, b]` means "b must come before a" — the edge goes FROM `b` TO `a`. Getting this backwards inverts the entire dependency graph.
|
||||
|
||||
Think of it as: "to take course `a`, you need course `b` first." So `b` points to `a` (b enables a).
|
||||
wrong_approach: "Add edge from a to b"
|
||||
correct_approach: "Add edge from b to a (prerequisite points to dependent)"
|
||||
|
||||
- title: Handling Disconnected Components
|
||||
description: |
|
||||
Some courses might have no prerequisites AND no courses depending on them (isolated nodes). These must still appear in the result.
|
||||
|
||||
Initialising in-degrees for all courses from `0` to `numCourses - 1` (not just those in prerequisites) ensures isolated courses start with in-degree `0` and get processed.
|
||||
wrong_approach: "Only track courses mentioned in prerequisites"
|
||||
correct_approach: "Initialise in-degree array for all numCourses courses"
|
||||
|
||||
- title: Multiple Valid Orderings
|
||||
description: |
|
||||
The problem states any valid ordering is acceptable. Don't over-engineer to find a "canonical" order — the first valid topological sort you find works.
|
||||
|
||||
If courses 1 and 2 both have in-degree 0 at the same time, either can come first in the result.
|
||||
|
||||
key_takeaways:
|
||||
- "**Topological sort** produces a linear ordering of vertices in a DAG where every edge points from earlier to later in the sequence"
|
||||
- "**Kahn's Algorithm** uses BFS with in-degrees: start with in-degree 0 nodes, decrement neighbours' in-degrees, repeat"
|
||||
- "**Cycle detection** is built-in: if the result has fewer nodes than the graph, a cycle exists"
|
||||
- "This pattern applies to **dependency resolution** problems: build systems, task scheduling, package managers"
|
||||
|
||||
time_complexity: "O(V + E). We process each course (vertex) once and examine each prerequisite (edge) once when decrementing in-degrees. Here V = `numCourses` and E = `len(prerequisites)`."
|
||||
space_complexity: "O(V + E). The adjacency list stores all edges, and we use arrays of size V for in-degrees and the result. The queue holds at most V courses."
|
||||
|
||||
solutions:
|
||||
- approach_name: Kahn's Algorithm (BFS)
|
||||
is_optimal: true
|
||||
code: |
|
||||
from collections import deque
|
||||
|
||||
def find_order(num_courses: int, prerequisites: list[list[int]]) -> list[int]:
|
||||
# Build adjacency list and calculate in-degrees
|
||||
graph = [[] for _ in range(num_courses)]
|
||||
in_degree = [0] * num_courses
|
||||
|
||||
for course, prereq in prerequisites:
|
||||
graph[prereq].append(course) # prereq -> course edge
|
||||
in_degree[course] += 1 # course has one more prerequisite
|
||||
|
||||
# Start with courses that have no prerequisites
|
||||
queue = deque()
|
||||
for course in range(num_courses):
|
||||
if in_degree[course] == 0:
|
||||
queue.append(course)
|
||||
|
||||
result = []
|
||||
|
||||
# Process courses in topological order
|
||||
while queue:
|
||||
current = queue.popleft()
|
||||
result.append(current)
|
||||
|
||||
# "Unlock" courses that depended on current
|
||||
for next_course in graph[current]:
|
||||
in_degree[next_course] -= 1
|
||||
if in_degree[next_course] == 0:
|
||||
queue.append(next_course)
|
||||
|
||||
# If we processed all courses, return the order; otherwise cycle exists
|
||||
return result if len(result) == num_courses else []
|
||||
explanation: |
|
||||
**Time Complexity:** O(V + E) — Each course is enqueued/dequeued once, and each prerequisite edge is processed once.
|
||||
|
||||
**Space Complexity:** O(V + E) — Adjacency list stores all edges, plus O(V) for in-degrees, queue, and result.
|
||||
|
||||
Kahn's Algorithm processes courses in dependency order. By tracking in-degrees, we know exactly when a course becomes "ready" (all prerequisites completed). The BFS naturally produces a valid topological ordering.
|
||||
|
||||
- approach_name: DFS with Cycle Detection
|
||||
is_optimal: true
|
||||
code: |
|
||||
def find_order(num_courses: int, prerequisites: list[list[int]]) -> list[int]:
|
||||
# Build adjacency list
|
||||
graph = [[] for _ in range(num_courses)]
|
||||
for course, prereq in prerequisites:
|
||||
graph[prereq].append(course)
|
||||
|
||||
# States: 0 = unvisited, 1 = visiting (in current path), 2 = visited
|
||||
state = [0] * num_courses
|
||||
result = []
|
||||
|
||||
def dfs(course: int) -> bool:
|
||||
"""Returns True if no cycle detected, False if cycle found."""
|
||||
if state[course] == 1: # Currently visiting -> cycle!
|
||||
return False
|
||||
if state[course] == 2: # Already processed
|
||||
return True
|
||||
|
||||
state[course] = 1 # Mark as visiting
|
||||
|
||||
for next_course in graph[course]:
|
||||
if not dfs(next_course):
|
||||
return False
|
||||
|
||||
state[course] = 2 # Mark as fully processed
|
||||
result.append(course) # Add in reverse post-order
|
||||
return True
|
||||
|
||||
# Run DFS from each unvisited course
|
||||
for course in range(num_courses):
|
||||
if state[course] == 0:
|
||||
if not dfs(course):
|
||||
return []
|
||||
|
||||
# Result is in reverse order (we added nodes after processing children)
|
||||
return result[::-1]
|
||||
explanation: |
|
||||
**Time Complexity:** O(V + E) — Each course is visited once, and each edge is traversed once.
|
||||
|
||||
**Space Complexity:** O(V + E) — Adjacency list plus O(V) recursion stack in worst case.
|
||||
|
||||
DFS explores each course deeply, adding it to the result only after all its dependents are processed. The three-state system (unvisited, visiting, visited) detects cycles: if we revisit a node that's currently in our path, we've found a cycle. The final result is reversed because we add nodes in post-order (after children).
|
||||
Reference in New Issue
Block a user