DSA sheet · Graphs · Topological sort pattern

Course Schedule

This is the first problem of the topological sort pattern, and the teacher calls it one of the most frequently asked graph questions in interviews. Courses depend on other courses, and we must say whether all of them can be finished. The graph idea underneath is: you can finish everything exactly when the "depends on" graph has no cycle. She solves it with BFS using indegrees (Kahn's algorithm), explains how the same idea looks with DFS and why BFS fits better, and leaves the DFS version as homework. I've written that homework too.

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 words we need

Input forms, and building an adjacency list

Graphs come as an edge list (pairs like [1, 0]), an adjacency matrix (m[i][j] = 1 if there's an edge) or an adjacency list (for each node, the list of nodes its arrows point to). Here we get an edge list, and the teacher's first rule for any graph problem is: build the adjacency list first, because traversal keeps asking "where can I go from here?".

edge list → adjacency list (directed)
n = 3
edges = [[0, 1], [1, 2]]          # 0 goes to 1, 1 goes to 2
adj = [[] for _ in range(n)]
for a, b in edges:
    adj[a].append(b)              # directed: only one way
print(adj)                        # [[1], [2], []]

Her checklist for any graph problem: ① make the adjacency list, ② decide whether DFS or BFS fits better, ③ decide whether you need a visited array, or whether something else (the input itself, a counter, a special value) can play that role.

Doubt: a list of lists, or a dictionary (map)?
→ The courses are numbered 0 to n−1, so a list with index = course number is perfect. She says a map is better only when the keys are unknown or aren't neat numbers (like the words in Word Ladder).

Visited, BFS and DFS in one line each

Indegree

The indegree of a node is the number of arrows coming into it. For a course, that's how many prerequisites it is still waiting for. A course with indegree 0 waits for nothing, so it can be taken right now. The teacher calls it the "dependency count".

Topological sort

A topological order is a line-up of all nodes such that every arrow points forward: if a→b, then a comes before b. For courses, that's a valid study plan. It exists only if there is no cycle. In a cycle every course waits for another course in the same cycle, so none of them can go first.

Kahn's algorithm template (topological sort with BFS)
from collections import deque
n = 4
adj = [[1, 2], [3], [3], []]      # 0 before 1 and 2, both before 3
indegree = [0] * n
for u in range(n):
    for v in adj[u]:
        indegree[v] += 1          # one more arrow coming into v
queue = deque(u for u in range(n) if indegree[u] == 0)
order = []
while queue:
    u = queue.popleft()           # u has nothing left to wait for
    order.append(u)
    for v in adj[u]:
        indegree[v] -= 1          # v waits for one thing less
        if indegree[v] == 0:
            queue.append(v)
print(order)                      # [0, 1, 2, 3]
print(len(order) == n)            # True, so there is no cycle

Part A · BFS with indegrees (Kahn's algorithm)

LeetCode 207 · Medium

1The question in simple words

There are numCourses courses, numbered 0 to numCourses − 1. You get a list prerequisites, where each pair [a, b] means "to take course a, you must first finish course b". Return True if it's possible to finish all courses, otherwise False.

Read the pair the right way round[a, b] = b first, then a. So [0, 1] means "0 depends on 1": finish 1 first. As an arrow: (1)→(0), from the prerequisite to the course that needs it.

Her real-life examples: you can't move to class 5 until you've passed the class 4 exam. You can't start DSA until you know some programming language. Those are dependencies.

  Example 1            Example 2 (cycle)
 (0) ──→ (1)          (0) ──→ (1)
                       ↑        │
                       └────────┘

Her bigger example (5 courses)

n = 5, prerequisites = [[1,0], [0,2], [2,3], [2,4], [3,4]]. In words: 1 needs 0, 0 needs 2, 2 needs 3, 2 needs 4, and 3 needs 4. Course 4 needs nothing.

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

2What the constraints tell us

3Intuition: take whatever is free, then unlock the next ones

Picture a to-do board. Next to every course you write how many prerequisites it's still waiting for (its indegree).

4Building the logic from her example

Step 1: indegree = how many courses each one depends on

For each pair [course, pre], the course depends on one more thing, so indegree[course] += 1.

course01234
depends on203, 44nothing
indegree11210

Only course 4 has indegree 0, so we can start with 4.

Step 2: the adjacency list (parent → children)

Once 4 is finished, where can we go next? To every course that was waiting for 4. The pairs don't tell us that directly. They're written from the child's side ("2 needs 4"). So we build the adjacency list the other way round:

node01234
children (graph[node])[1][][0][2][2, 3]
Doubt 1: why point the arrow from prerequisite to course, not the other way?
→ Because the moment we finish a course, we want to quickly find "who was waiting for me?" so we can lower their counts. Storing pre → course gives exactly that list. If we stored course → pre, we'd have to search the whole list every time.
Dependency problems need two thingsThe adjacency list tells you where to go next (parent → children). The indegree array tells you when a node is allowed to go (indegree 0).

Step 3: BFS or DFS?

The teacher picks BFS. When a course is finished, all its children get their count lowered at the same moment, and any that reach 0 become ready together. A queue handles that naturally. (Part B shows the DFS version and why it's a bit clumsier for this.)

Step 4: run it

Count = 5 = numCourses → True. The order we took them in was 4, 3, 2, 0, 1.

Step 5: why "count == numCourses" is the whole test

Every course we pop is a course we really managed to take. So the answer is simply: did we pop all of them? We keep a count that goes up on every pop.

Doubt 2: why does "fewer than n courses taken" mean there's a cycle?
→ Two halves:
(a) A cycle blocks its courses forever. Take any cycle, e.g. 1 → 2 → 1. Course 2 can only reach indegree 0 after 1 is popped, and 1 can only reach 0 after 2 is popped. Neither can be first, so neither ever enters the queue. Every course behind the cycle (one that needs a cycle course, directly or through others) is stuck too. So with a cycle, count < n.
(b) With no cycle, Kahn's never gets stuck. Look at the courses not taken yet. If every one of them still had indegree ≥ 1, each would be waiting for another untaken course. Walk backwards: pick an untaken course, step to a course it waits for, then to one that course waits for, and so on. There are only finitely many courses, so you'd eventually step onto a course you've already visited, which is a cycle. So if there's no cycle, some untaken course always has indegree 0, it's in the queue, and the process continues until all n are taken.
Together: count == n ⇔ no cycle ⇔ all courses can be finished.

Step 6: do we need a visited array?

No. A course enters the queue only at the exact moment its indegree goes from 1 to 0, and that can happen only once (indegree never goes back up). So no course is pushed twice. The indegree reaching 0 is our "visited" signal. Courses that already start at 0 are pushed once at the start, and nothing lowers them again because no arrow points into them.

The cycle example, run through the same code

Example 2: [[1,0],[0,1]] → indegree = [1, 1]. No course has 0, so the queue starts empty, the loop never runs, and count = 0 ≠ 2 → False.

A cycle in the middle (the example she uses in the Course Schedule II video): 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)        1 and 2 wait for each other

indegree = [1, 2, 1, 0]. Queue = [3]. Pop 3 (count 1) → lower 1: 2 → 1. Not 0. The queue is empty → count = 1 ≠ 4 → False. Course 0 isn't in the cycle, but it's stuck behind it.

5Approach steps

  1. Make graph = n empty lists, and indegree = n zeros.
  2. For each [course, pre]: graph[pre].append(course) and indegree[course] += 1.
  3. Push every course with indegree 0 into a queue.
  4. While the queue isn't empty: pop a course and do count += 1.
  5. For each child of that course: indegree[child] -= 1. If it becomes 0, push the child.
  6. Return count == numCourses.

6Code (Python)

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

class Solution:
    def canFinish(self, numCourses, prerequisites):
        graph = [[] for _ in range(numCourses)]   # parent -> children
        indegree = [0] * numCourses                # how many courses each one waits for

        for course, pre in prerequisites:
            graph[pre].append(course)              # finishing pre helps course
            indegree[course] += 1

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

        count = 0
        while queue:
            course = queue.popleft()
            count += 1                             # we really took this course
            for child in graph[course]:
                indegree[child] -= 1               # one requirement done
                if indegree[child] == 0:           # nothing left to wait for
                    queue.append(child)

        return count == numCourses

7Code line by line

linewhat it means
graph = [[] for _ in range(numCourses)]One empty child list per course. (Don't write [[]] * n, because that makes n references to the same list.)
indegree = [0] * numCoursesThe dependency count of each course.
for course, pre in prerequisites:Each pair is [a, b] = "a needs b", so we unpack it as course, pre.
graph[pre].append(course)Edge pre → course: when pre is done, look at course.
indegree[course] += 1The course waits for one more thing.
if indegree[i] == 0: queue.append(i)Start with every free course. There can be several, from different pieces of the graph.
count = 0How many courses we've managed to take.
course = queue.popleft() count += 1Take the course and count it.
indegree[child] -= 1Tell each child "your parent is done".
if indegree[child] == 0: queue.append(child)The child waits for nothing now, so it's ready. This happens once per course, so we need no visited array.
return count == numCoursesAll taken → no cycle → True. Fewer → some courses were blocked by a cycle → False.

8Dry run: indegree and queue after every step

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

steppoppedchildren checkedqueue AFTERindegree AFTER [0,1,2,3,4]count
start––[4][1, 1, 2, 1, 0]0
142: 2→1 (still waits) · 3: 1→0 push[3][1, 1, 1, 0, 0]1
232: 1→0 push[2][1, 1, 0, 0, 0]2
320: 1→0 push[0][0, 1, 0, 0, 0]3
401: 1→0 push[1][0, 0, 0, 0, 0]4
51no children[][0, 0, 0, 0, 0]5
endcount 5 == numCourses 5 → True
start4only indegree 0
after 132 dropped to 1, not ready yet
after 22
after 30
after 41
after 5empty5 popped → True
after step 1 (✓ = taken, number = indegree)
 (4✓) ──→ (3:0)
  │         │
  ↓         │
 (2:1) ←────┘
  │
  ↓
 (0:1) ──→ (1:1)
after step 3
 (4✓) ──→ (3✓)
  │         │
  ↓         │
 (2✓) ←─────┘
  │
  ↓
 (0:0) ──→ (1:1)
cycle example: stuck after step 1
 (3✓) ──→ (1:1) ──→ (0:1)
           ↑ │
           │ ↓
           (2:1)
 queue empty, count 1 < 4

9Complexity & remember

The teacher counts each loop separately. Let V = numCourses and E = number of prerequisite pairs:

Remember Course Schedule[a, b] = b before a → edge b → a, indegree[a] += 1. Queue all indegree-0 courses. Pop → count++ → lower the children → push the ones that reach 0. count == n ⇔ no cycle ⇔ True.

Part B · The same indegree idea with DFS (her comparison)

1The question

The same as Part A. The teacher asks: can we use DFS instead of BFS? Yes. She walks through it to show what changes and why BFS feels more natural here.

2What the constraints tell us

The same numbers, plus one Python-specific danger: DFS uses recursion. If the courses form a straight line 0 → 1 → … → 1999, the recursion goes 2000 calls deep. Python's default limit is about 1000, so it would crash with RecursionError. We raise the limit with sys.setrecursionlimit.

3Intuition

Keep the same rule: you may enter a course only when its indegree hits 0. Instead of putting it in a queue, jump into it immediately (a recursive call), lower its children, and jump into any child that reaches 0.

4Her walkthrough, and the counting trap

The same 5-course example. The free course is 4:

She counts the calls: 4, 2, 3, 2, 0, 1 = 6 calls for 5 courses, because course 2 was called twice (once too early). So "number of DFS calls" isn't "number of courses taken", and we can't compare it with numCourses.

Her fix: count a course only at the moment its indegree becomes 0 (or collect the taken courses in another structure). In my code I simply don't call DFS on a child until its indegree is 0, so every call is a real "taken" course and I count inside the call.

Doubt 1: so why does she prefer BFS here?
→ When a course finishes, all its children get lowered at the same time, and every child that's free becomes ready together. A queue holds that "ready" group naturally. DFS dives into the first free child before even looking at the others, needs care with counting, and in Python needs a bigger recursion limit. Both are correct and both are O(V + E). (At one point in the video she says "mostly we prefer DFS for course schedule", but from everything around it she clearly means BFS.)
Doubt 2: do I still start from every indegree-0 course?
→ Yes. There can be several free courses in different pieces of the graph, so we loop over all courses and start a DFS from each one that has indegree 0 at the start.

5Approach steps

  1. Build graph and indegree exactly as in Part A.
  2. dfs(course): count it, then for each child lower its indegree. If it becomes 0, call dfs(child).
  3. Collect the courses whose indegree is 0 at the start, then call dfs on each.
  4. Return count == numCourses.

6Code (Python)

Course Schedule with indegree + DFS
import sys

class Solution:
    def canFinish(self, numCourses, prerequisites):
        sys.setrecursionlimit(10000)               # a chain can be 2000 calls deep
        graph = [[] for _ in range(numCourses)]
        indegree = [0] * numCourses
        for course, pre in prerequisites:
            graph[pre].append(course)
            indegree[course] += 1

        count = 0

        def dfs(course):
            nonlocal count
            count += 1                             # this course is taken
            for child in graph[course]:
                indegree[child] -= 1
                if indegree[child] == 0:           # only enter when it is free
                    dfs(child)

        starts = [i for i in range(numCourses) if indegree[i] == 0]
        for s in starts:
            dfs(s)
        return count == numCourses

7Code line by line

linewhat it means
sys.setrecursionlimit(10000)Allows deep chains (up to 2000 courses in a line) without a RecursionError.
nonlocal countLets the inner function change the count that belongs to canFinish.
count += 1We only ever enter a course when it's free, so every call is one real course.
if indegree[child] == 0: dfs(child)The same "ready" rule as BFS, but we dive in right away instead of queueing.
starts = [...]We collect the starting zeros before any DFS runs. Courses that only reach 0 later are entered by the DFS itself, so they aren't started twice.

8Dry run: call stack

  1. starts = [4]. dfs(4): count 1. Child 2: 2 → 1, so don't enter. Child 3: 1 → 0 → enter.
  2. dfs(3): count 2. Child 2: 1 → 0 → enter.
  3. dfs(2): count 3. Child 0: 1 → 0 → enter.
  4. dfs(0): count 4. Child 1: 1 → 0 → enter.
  5. dfs(1): count 5. It has no children, so everything returns.
  6. count 5 == 5 → True.
after step 1
dfs(4)dfs(3)
deepest (step 5)
dfs(4)dfs(3)dfs(2)dfs(0)dfs(1)
indegree at the end
[0, 0, 0, 0, 0]

9Complexity & remember

RememberDFS can follow the indegree rule too: enter a child only when its count hits 0, and count inside the call. Don't count raw calls. Raise the recursion limit in Python.

Part C · Homework: DFS cycle detection with a path marker

The teacher ends the video by asking us to solve it with DFS ourselves. This is the classic DFS way, using what we learned in Cycle in Directed Graph (DFS).

1The question, restated

"Can all courses be finished?" is the same as "does the directed graph have no cycle?" (Doubt 2 of Part A). So we just run a directed-cycle check.

2What the constraints tell us

The same as Part B: the recursion can be up to 2000 deep, so raise the limit.

3Intuition: the "currently on my path" marker

Walk along the arrows with DFS. Keep a mark on the courses that are on the current path (the call stack right now). If an arrow leads to a course that's still on the current path, we've come round in a loop, which is a cycle.

Each course has 3 states:

statemeaning
0 = not visitednever touched
1 = on the pathits DFS call has started and hasn't finished, so it's on the stack
2 = donefully explored, and no cycle was found below it
Doubt: why isn't a plain visited set enough in a directed graph?
→ Reaching an already-visited course isn't always a cycle. In the 5-course example, 2 is reached from 4 and also from 3. That's two paths meeting, not a loop. Only reaching a course that's still on the current path (state 1) is a loop. A course in state 2 was already checked and is safe, so we skip it.

4Building the logic

5Approach steps

  1. Build the graph. Set state = [0] * n.
  2. For each course with state 0: if dfs(course) finds a cycle → return False.
  3. If no cycle was found anywhere → return True.

6Code (Python)

Course Schedule with DFS cycle detection (homework)
import sys

class Solution:
    def canFinish(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

        def dfs(u):                    # returns False if a cycle is found
            state[u] = 1
            for v in graph[u]:
                if state[v] == 1:      # back to a course still on the path
                    return False
                if state[v] == 0 and not dfs(v):
                    return False
            state[u] = 2               # fully checked, safe
            return True

        for c in range(numCourses):
            if state[c] == 0 and not dfs(c):
                return False
        return True

7Code line by line

linewhat it means
state[u] = 1u is now on the current path.
if state[v] == 1: return FalseAn arrow back into the current path is a cycle.
if state[v] == 0 and not dfs(v):A new course: explore it. If something below it found a cycle, pass the False up.
state[u] = 2Leaving u: it's off the path and known to be safe.
for c in range(numCourses):Start from every untouched course, so separate pieces are covered.

8Dry run on the cycle example

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

  1. dfs(0): 0 has no children → state 2.
  2. dfs(1): state[1] = 1. Child 0 is already done (state 2), so skip it. Child 2 is new → dfs(2).
  3. dfs(2): state[2] = 1. Child 1 has state 1, so it's on the path → cycle → return False.
  4. The False goes up through dfs(1) to the main loop → False.
stack when the cycle is found
dfs(1) state 1dfs(2) sees 1 → False
state array then [0,1,2,3]
[2, 1, 1, 0]

On the 5-course example there's no cycle: every DFS finishes, each course becomes state 2, and the answer is True.

9Complexity & remember

RememberA directed cycle = an arrow into a node that's still on the current path (state 1). State 2 means already safe, so skip it. Courses can be finished ⇔ no cycle.

Part D · Revision page

A: Kahn's BFSB: indegree DFSC: DFS path marker
key arrayindegreeindegreestate 0/1/2
start fromall indegree-0 courses (queue)all indegree-0 courses (calls)every course still in state 0
move to a child whenits indegree reaches 0its indegree reaches 0it's new (state 0)
cycle found bycount < n at the endcount < n at the endan edge into state 1
visited array?no (indegree hitting 0 happens once)nothe state array is the visited array
time / spaceO(V + E) / O(V + E)
Python gotchanoneraise the recursion limit (up to 2000 deep)
graph ideain this problem
nodea course 0…n−1
edge[a, b] → directed edge b → a (the prerequisite points to the course it unlocks)
indegreehow many prerequisites are still unfinished
answerTrue ⇔ there's a topological order ⇔ there's no directed cycle
If you remember only 5 lines 1. [a, b] means b first, so the edge is b → a and indegree[a] += 1.
2. Queue every course with indegree 0.
3. Pop → count += 1 → lower each child's indegree → push the children that reach 0.
4. Return count == numCourses.
5. Fewer than n taken ⇔ a cycle blocked some courses (they wait for each other forever).
Mistakes to avoid ✗ building the edge the wrong way (a → b instead of b → a)
✗ pushing a child before its indegree reaches 0
✗ starting only from course 0 (start from every indegree-0 course)
✗ in DFS, counting calls instead of courses taken
✗ in cycle-DFS, treating any visited node as a cycle (only state 1 is)
✗ [[]] * n for the graph (all entries share one list)
✗ forgetting sys.setrecursionlimit for the DFS versions
test it yourself (paste under any solution above)
s = Solution()
print(s.canFinish(2, [[1, 0]]))                                   # True
print(s.canFinish(2, [[1, 0], [0, 1]]))                           # False
print(s.canFinish(5, [[1, 0], [0, 2], [2, 3], [2, 4], [3, 4]]))   # True
print(s.canFinish(4, [[0, 1], [1, 2], [2, 1], [1, 3]]))           # False
print(s.canFinish(3, []))                                         # True

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