DSA sheet · Graphs · Topological sort pattern

Course Schedule II

This is a small variation of Course Schedule, and the teacher says that problem is the prerequisite for this video. Last time we only answered "can all courses be finished?". Now we must return the order in which to take them. That order is a topological order. The code is almost identical: where we used to count each popped course, we now write it down. She also shows a tempting but wrong way to detect failure, and why we must compare against numCourses.

Every problem below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember

Part 0 · Before starting

Graph basics for this page

building the graph and indegrees from the pairs
numCourses = 3
prerequisites = [[1, 0], [2, 1]]       # 1 needs 0, 2 needs 1
graph = [[] for _ in range(numCourses)]
indegree = [0] * numCourses
for course, pre in prerequisites:
    graph[pre].append(course)          # pre -> course
    indegree[course] += 1
print(graph)                           # [[1], [2], []]
print(indegree)                        # [0, 1, 1]

Topological order

A topological order lists every node so that each arrow points forward: for every edge b → a, b appears before a. It's exactly a valid timetable of courses. It exists only when there is no cycle. In a cycle every course waits for another one in the same cycle, so none of them can be placed first. A graph can have more than one valid order, and LeetCode accepts any of them.

Part A · Kahn's algorithm, recording the order

LeetCode 210 · Medium

1The question in simple words

The same setup as Course Schedule: numCourses courses, and pairs [a, b] meaning "take b before a". This time, return a list of all courses in an order you could take them. If it's impossible (there's a cycle), return an empty list [].

The teacher reuses her 5-course example: [[1,0],[0,2],[2,3],[2,4],[3,4]].

 (4) ──→ (3)
  │       │
  ↓       │
 (2) ←────┘
  │
  ↓
 (0) ──→ (1)

graph = {0:[1], 1:[], 2:[0], 3:[2], 4:[2, 3]}, and indegree for courses [0, 1, 2, 3, 4] = [1, 1, 2, 1, 0].

Doubt: she says to return "an array with zero" when no order exists. Does she mean [0]?
→ No. In Java she returns new int[0], which is an array of size zero, i.e. an empty array. In Python that's []. Returning [0] would be wrong (it claims "take course 0").

2What the constraints tell us

3Intuition: write down courses in the order you take them

In Course Schedule we already took the courses one by one: each pop from the queue was "I take this course now". That sequence of pops is a valid order, because a course only enters the queue after everything it needs has been popped earlier.

So the only change: instead of count += 1, append the course to order. At the end, if all n courses made it into order, return it. Otherwise return [].

4Building the logic, and the trap she shows

Recording the order

In Java she makes an int array of size n plus an index pointer starting at 0. Each pop does order[index] = course; index++. In Python we just keep a list and append. Then len(order) plays the role of her index.

How do we know it failed?

Fully stuck (her first cycle example): [[0,1],[1,0]]. Both indegrees are 1, nothing has indegree 0, the queue starts empty, the loop never runs, and order stays empty. So "index is still 0" means failure here.

That gives a tempting rule: "if at least one course was taken (index > 0), return the order". She shows why it's wrong:

n = 4, [[0,1],[1,2],[2,1],[1,3]]: 0 needs 1, 1 needs 2 and 3, 2 needs 1.

 (3) ──→ (1) ──→ (0)
          ↑ │
          │ ↓
          (2)        cycle 1 ⇄ 2

indegree = [1, 2, 1, 0]. Queue = [3]. Pop 3 → order = [3]. Lower 1: 2 → 1. Not 0. The queue is empty, so we stop with order = [3]. The index is 1, which is > 0, but we took only 1 of 4 courses. Courses 1 and 2 wait for each other, and 0 waits behind them.

The correct checkReturn the order only if it contains all the courses: len(order) == numCourses (her index == numCourses). Otherwise return [].
Doubt 1: why does "fewer than n in the order" mean there's a cycle?
→ (a) A cycle blocks itself: every course on a cycle needs the previous course on the cycle to be popped first, so none of them can be the first to reach indegree 0. They never enter the queue, and neither does any course that depends on them.
(b) Without a cycle we never get stuck: if all remaining courses had indegree ≥ 1, each would wait for another remaining course. Following "waits for" backwards, we'd eventually repeat a course (there are only n), which would be a cycle. So with no cycle there's always a free course left, and all n get popped.
So len(order) == n ⇔ no cycle, and when it's true the order is valid.
Doubt 2: why is the pop order guaranteed to be a valid order?
→ A course is pushed only when its indegree reaches 0, which means every one of its prerequisites has already been popped (each pop lowered it by one). So every prerequisite sits earlier in order than the course itself.
Doubt 3: do we need a visited array?
→ No, for the same reason as in Course Schedule: a course's indegree hits 0 only once, so it's pushed at most once.

5Approach steps

  1. Build graph (pre → course) and indegree.
  2. Push every course with indegree 0 into the queue.
  3. While the queue isn't empty: pop a course and append it to order.
  4. For each child: lower its indegree. If it reaches 0, push it.
  5. If len(order) == numCourses, return order. Otherwise return [].

6Code (Python)

Course Schedule II with BFS (Kahn's algorithm)
from collections import deque

class Solution:
    def findOrder(self, numCourses, prerequisites):
        graph = [[] for _ in range(numCourses)]
        indegree = [0] * numCourses
        for course, pre in prerequisites:
            graph[pre].append(course)
            indegree[course] += 1

        queue = deque()
        for i in range(numCourses):
            if indegree[i] == 0:
                queue.append(i)

        order = []                                 # was "count" in Course Schedule
        while queue:
            course = queue.popleft()
            order.append(course)                   # was "count += 1"
            for child in graph[course]:
                indegree[child] -= 1
                if indegree[child] == 0:
                    queue.append(child)

        if len(order) == numCourses:               # every course was taken
            return order
        return []                                  # a cycle blocked some courses

7Code line by line (what changed from Course Schedule)

linewhat it means
graph / indegree / first queue loopExactly as in Course Schedule: the edge goes pre → course, indegree[course] += 1, and every free course is queued.
order = []Replaces the counter. It's the timetable we're building.
order.append(course)Taking a course = writing it down next. Its position is len(order) before the append (her index).
indegree[child] -= 1 ...Unchanged: unlock the children.
if len(order) == numCourses: return orderAll courses were placed, so the order is complete and valid.
return []Some courses never became free (cycle), so it's impossible. Not "if order is non-empty", because of the trap above.

8Dry run: indegree, queue and order after every step

Her 5-course example. graph = {0:[1], 1:[], 2:[0], 3:[2], 4:[2, 3]}.

steppoppedchildren checkedqueue AFTERindegree AFTER [0,1,2,3,4]order so far
start––[4][1, 1, 2, 1, 0][]
142: 2→1 · 3: 1→0 push[3][1, 1, 1, 0, 0][4]
232: 1→0 push[2][1, 1, 0, 0, 0][4, 3]
320: 1→0 push[0][0, 1, 0, 0, 0][4, 3, 2]
401: 1→0 push[1][0, 0, 0, 0, 0][4, 3, 2, 0]
51no children[][0, 0, 0, 0, 0][4, 3, 2, 0, 1]
endlen 5 == 5 → return [4, 3, 2, 0, 1]
order43201every arrow points forward ✓

Check the arrows: 4→3 (4 before 3 ✓), 4→2 ✓, 3→2 ✓, 2→0 ✓, 0→1 ✓.

The trap example, step by step

n = 4, [[0,1],[1,2],[2,1],[1,3]] → graph = {1:[0, 2], 2:[1], 3:[1], 0:[]}.

steppoppedchildren checkedqueue AFTERindegree AFTER [0,1,2,3]order so far
start––[3][1, 2, 1, 0][]
131: 2→1 (still waits for 2)[][1, 1, 1, 0][3]
endlen 1 ≠ 4 → return [] (the "index > 0" rule would wrongly return [3])
stuck state (✓ = taken, number = indegree)
 (3✓) ──→ (1:1) ──→ (0:1)
           ↑ │
           │ ↓
           (2:1)
her first cycle example
 (0:1) ──→ (1:1)
   ↑          │
   └──────────┘
 queue starts empty → []

9Complexity & remember

Remember Course Schedule IICourse Schedule with count += 1 replaced by order.append(course). Return order only if len(order) == numCourses, else []. "Non-empty" is not enough.

Part B · Extra: the DFS way (reverse finishing order)

Not in this video. It continues the DFS homework from Course Schedule, because interviewers often ask for both.

1The question

The same: return a valid order, or [] if there's a cycle.

2What the constraints tell us

Up to 2000 courses in a chain → recursion up to 2000 deep, which is more than Python's default limit of ~1000. Raise it with sys.setrecursionlimit.

3Intuition: a course finishes after everything that depends on it

Run DFS along the arrows pre → course. A DFS call on course u finishes only after it has finished every course reachable from u, i.e. every course that needs u, directly or indirectly. So if we write u down at the moment it finishes, u lands after all the courses that need it. Reverse that list and u comes before all of them, which is exactly a topological order.

At the same time we detect cycles with the 3-state marker from Course Schedule (Part C): 0 = new, 1 = on the current path, 2 = done. An arrow into a state-1 course is a cycle → return [].

4Building the logic

Doubt: why not write u down when we enter it (pre-order)?
→ We might enter u before one of its own prerequisites has been visited. In the 5-course example, starting at course 0 would print 0 first, but 0 needs 2, 3 and 4. Only the finishing time is guaranteed to come after every course that depends on u, which is why we reverse the finishing order.

5Approach steps

  1. Build the graph. Set state = [0] * n and post = [].
  2. For each course in state 0: run dfs. If it found a cycle, return [].
  3. Return post[::-1].

6Code (Python)

Course Schedule II with DFS (reverse post-order)
import sys

class Solution:
    def findOrder(self, numCourses, prerequisites):
        sys.setrecursionlimit(10000)
        graph = [[] for _ in range(numCourses)]
        for course, pre in prerequisites:
            graph[pre].append(course)

        state = [0] * numCourses       # 0 = new, 1 = on current path, 2 = done
        post = []                      # courses in the order their DFS finishes

        def dfs(u):                    # False means a cycle was found
            state[u] = 1
            for v in graph[u]:
                if state[v] == 1:
                    return False
                if state[v] == 0 and not dfs(v):
                    return False
            state[u] = 2
            post.append(u)             # everything that needs u is already in post
            return True

        for c in range(numCourses):
            if state[c] == 0 and not dfs(c):
                return []
        return post[::-1]

7Code line by line

linewhat it means
state[u] = 1u is on the current DFS path.
if state[v] == 1: return FalseWe came back to a course on our own path, so it's a cycle and no order exists.
state[u] = 2 post.append(u)u is finished. Every course reachable from u (everything that needs u) finished earlier.
return post[::-1]Reverse it so that each course comes before the courses that need it.

8Dry run (5-course example)

  1. c = 0: dfs(0) → child 1 → dfs(1) has no children → post = [1]. Back in 0 → post = [1, 0].
  2. c = 1: already done.
  3. c = 2: dfs(2) → child 0 is done → post = [1, 0, 2].
  4. c = 3: dfs(3) → child 2 is done → post = [1, 0, 2, 3].
  5. c = 4: dfs(4) → children 2 and 3 are done → post = [1, 0, 2, 3, 4].
  6. Reversed: [4, 3, 2, 0, 1], the same order as Kahn's gave.
deepest stack (step 1)
dfs(0)dfs(1) → post [1]
post at the end
[1, 0, 2, 3, 4]

On the trap example, dfs(1) → dfs(2) sees course 1 in state 1 → cycle → [].

9Complexity & remember

RememberDFS topological order = reverse of the finishing order. Use the 3 states to catch cycles (an edge into state 1 → []).

Part C · Revision page

Course Schedule (207)Course Schedule II (210)
askscan all courses be finished? (True/False)give one valid order, or []
on each popcount += 1order.append(course)
final checkcount == numCourseslen(order) == numCourses → order, else []
everything elsesame graph (pre → course), same indegree, same queue of indegree-0 courses
time / spaceO(V + E) / O(V + E)
Kahn's (BFS)DFS
order comes fromthe pop orderthe finishing order, reversed
cycle detected byfewer than n poppedan edge into a state-1 node
Python gotchanoneraise the recursion limit
If you remember only 5 lines 1. Edge pre → course, indegree[course] += 1.
2. Queue all indegree-0 courses.
3. Pop → append to order → lower the children → push the ones that hit 0.
4. len(order) == numCourses → return order, else [].
5. "At least one course taken" doesn't prove anything. A cycle can sit after some free courses.
Mistakes to avoid ✗ returning order when it's non-empty but incomplete (her trap)
✗ returning [0] instead of [] for "impossible"
✗ reversing the edge direction (that gives the order backwards)
✗ in DFS, recording pre-order instead of reversed finishing order
✗ starting only from course 0 instead of every indegree-0 course
test it yourself (paste under either solution above)
s = Solution()
print(s.findOrder(2, [[1, 0]]))                                   # [0, 1]
print(s.findOrder(5, [[1, 0], [0, 2], [2, 3], [2, 4], [3, 4]]))   # [4, 3, 2, 0, 1]
print(s.findOrder(4, [[1, 0], [2, 0], [3, 1], [3, 2]]) in ([0, 1, 2, 3], [0, 2, 1, 3]))  # True
print(s.findOrder(2, [[1, 0], [0, 1]]))                           # []
print(s.findOrder(4, [[0, 1], [1, 2], [2, 1], [1, 3]]))           # []
print(s.findOrder(1, []))                                         # [0]

Based on this video: Course Schedule II | Graph · Topological Sort