questions C

This commit is contained in:
2025-05-25 10:16:13 +01:00
parent 1e0aebfbfd
commit e6a22f98f8
85 changed files with 16925 additions and 0 deletions

View 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
&nbsp;
**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
&nbsp;
**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
&nbsp;
**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
&nbsp;
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).