Part 13 · Lesson 51 · read
Topological Sort
Put tasks in order when some depend on others, and find loops (cycles).
On this page
Prerequisites are arrows
Some jobs must happen in a set order. You put on socks before shoes. You take Algebra before Calculus. We draw this as a directed graph (lesson 49). A directed graph is a set of dots (called vertices) joined by one-way arrows. Each arrow b → a means "b must come before a". A topological order (also called topological sort) is a line-up of all the dots where every arrow points forward. For every b → a, b appears earlier than a.
Such an order exists only when the graph has no cycle. A cycle is a loop of arrows that leads back to where it started. Say A needs B, B needs C and C needs A. Nobody can go first. A directed graph with no cycle is called a DAG (directed acyclic graph, which just means "one-way arrows, no loops"). Every DAG has at least one topological order, and often many. Any valid one is fine unless the problem says otherwise.
One more word: the in-degree of a dot is the number of arrows pointing into it. Here it means the number of prerequisites the task still waits for. A task with in-degree 0 is free to start.
[a, b] as "to take a you must first take b". So the arrow is b → a. That is the opposite of the order the numbers appear in. Reading the pair backwards is the most common mistake in this topic.// "To take course a you must first take course b" is the pair [a, b].// We draw it as an arrow b -> a: b must come BEFORE a.function buildGraph(n, prerequisites) { const graph = Array.from({ length: n }, () => []); // graph[b] = courses that b unlocks const indegree = new Array(n).fill(0); // indegree[a] = how many prerequisites a still waits for for (const [a, b] of prerequisites) { graph[b].push(a); indegree[a]++; } return { graph, indegree };}const { graph, indegree } = buildGraph(4, [[1, 0], [2, 0], [3, 1], [3, 2]]);console.log(graph); // [ [ 1, 2 ], [ 3 ], [ 3 ], [] ]console.log(indegree); // [ 0, 1, 1, 2 ]Kahn's algorithm
Kahn's algorithm turns a simple idea into code: "do what is ready, then see what that makes ready." A queue is a line where the first item in is the first item out. The steps:
- Count the in-degree of every dot.
- Put every dot with in-degree 0 into a queue.
- Take one dot from the queue and add it to the answer. Look at each dot it points to and subtract 1 from that dot's in-degree. If the number reaches 0, add that dot to the queue.
- Repeat until the queue is empty.
If the answer has all n dots, you have a valid order. If it is shorter, the dots left over are stuck. Each one is on a cycle, or waits for a dot that is on a cycle, so none of them ever reached in-degree 0. Use the same trick as BFS (breadth-first search): keep a head number for the front of the queue instead of removing items. This keeps the time at O(V + E), where V is the number of dots and E is the number of arrows. Every dot goes into the queue once and every arrow is looked at once.
// Kahn's algorithm: repeatedly take a task with nothing left to wait for.function topoSort(n, edges) { // each edge [from, to] means from comes before to const graph = Array.from({ length: n }, () => []); const indegree = new Array(n).fill(0); for (const [from, to] of edges) { graph[from].push(to); indegree[to]++; } const queue = []; for (let v = 0; v < n; v++) if (indegree[v] === 0) queue.push(v); // ready right now let head = 0; // dequeue by advancing an index (queue.shift() is O(n)) const order = []; while (head < queue.length) { const v = queue[head++]; order.push(v); for (const next of graph[v]) { indegree[next]--; // one of next's prerequisites is now done if (indegree[next] === 0) queue.push(next); } } return order.length === n ? order : []; // fewer than n means a cycle blocked the rest}const edges = [[5, 2], [5, 0], [4, 0], [4, 1], [2, 3], [3, 1]];console.log(topoSort(6, edges)); // [ 4, 5, 2, 0, 3, 1 ]console.log(topoSort(3, [[0, 1], [1, 2], [2, 0]])); // [] (a cycle: nothing can start)Traced: Kahn on six tasks
indegree = [2,2,1,1,0,0]. This counts how many tasks each task still waits for. Task 0 waits for two tasks (5 and 4). Task 1 waits for two tasks (4 and 3). And so on.
▶const queue = [];2for (let v = 0; v < n; v++) if (indegree[v] === 0) queue.push(v);3let head = 0;4const order = [];5while (head < queue.length) {6 const v = queue[head++];7 order.push(v);8 for (const next of graph[v]) {9 indegree[next]--;10 if (indegree[next] === 0) queue.push(next);11 }12}13return order.length === n ? order : [];| indegree | [2, 2, 1, 1, 0, 0] |
DFS-based ordering
There is a second way. DFS (depth-first search) is a way to explore a graph: you follow one path as deep as you can, then go back and try another. Run DFS and write down each dot at the moment it is finished. A dot is finished when every dot it points to is already finished. So a dot always finishes after everything it unlocks. That means the finishing list is a valid order backwards. Reverse it and you have a topological order. The finishing order is called the post-order of the DFS.
To also find cycles, we give every dot one of three states. This is called a "three-colour" DFS.
| State | Meaning | If DFS meets it |
|---|---|---|
| UNSEEN (0) | not visited yet | visit it when we reach it |
| VISITING (1) | on the path we are walking right now | reaching it again means there is a cycle |
| DONE (2) | fully explored, nothing left to do here | safe to skip: it cannot lead back to where we are |
// DFS ordering: a task is finished only after everything it unlocks is finished.// Reverse the finishing order and every prerequisite lands before the tasks that need it.function topoSortDfs(n, edges) { const graph = Array.from({ length: n }, () => []); for (const [from, to] of edges) graph[from].push(to); const UNSEEN = 0, VISITING = 1, DONE = 2; // the three colours const state = new Array(n).fill(UNSEEN); const finished = []; let hasCycle = false; function visit(v) { state[v] = VISITING; // v is on the path we are walking right now for (const next of graph[v]) { if (state[next] === VISITING) hasCycle = true; // we came back to the current path: a cycle else if (state[next] === UNSEEN) visit(next); } state[v] = DONE; // everything v unlocks is already finished finished.push(v); } for (let v = 0; v < n; v++) if (state[v] === UNSEEN) visit(v); return hasCycle ? [] : finished.reverse();}const edges = [[5, 2], [5, 0], [4, 0], [4, 1], [2, 3], [3, 1]];console.log(topoSortDfs(6, edges)); // [ 5, 4, 2, 3, 1, 0 ]console.log(topoSortDfs(3, [[0, 1], [1, 2], [2, 0]])); // []This answer is different from Kahn's ([5, 4, 2, 3, 1, 0] vs [4, 5, 2, 0, 3, 1]). Both are correct. Check any arrow, for example 2 → 3 (2 comes first in both) or 4 → 0.
Finding loops in directed graphs
In an undirected graph (every edge works both ways), meeting a dot you already visited means a cycle. The one exception is the dot you just came from. That rule is wrong for directed graphs. Look at the diamond 0 → 1 → 3 and 0 → 2 → 3. Dot 3 is reached twice, once through 1 and once through 2. Still, there is no cycle, because both arrows go the same way. "Seen before" is a cycle only if the earlier visit is still on the path we are walking now. The VISITING state remembers exactly this. A plain visited set cannot.
// A visited set alone is NOT a cycle detector for directed graphs.const diamond = [[1, 2], [3], [3], []]; // 0 -> 1, 0 -> 2, 1 -> 3, 2 -> 3: no cycle at allfunction wrongHasCycle(graph) { const visited = new Set(); function visit(v) { visited.add(v); for (const next of graph[v]) { if (visited.has(next)) return true; // "seen before" is not the same as "on my current path" if (visit(next)) return true; } return false; } return visit(0);}function hasCycle(graph) { const state = new Array(graph.length).fill(0); // 0 unseen, 1 on the current path, 2 finished function visit(v) { state[v] = 1; for (const next of graph[v]) { if (state[next] === 1) return true; // only an arrow back into the current path is a cycle if (state[next] === 0 && visit(next)) return true; } state[v] = 2; return false; } for (let v = 0; v < graph.length; v++) if (state[v] === 0 && visit(v)) return true; return false;}console.log(wrongHasCycle(diamond)); // true (WRONG: 3 was reached by 1 earlier, then again by 2)console.log(hasCycle(diamond)); // false (correct)console.log(hasCycle([[1], [2], [0]])); // true (0 -> 1 -> 2 -> 0)Course schedule
Course Schedule (LeetCode 207) only asks: can every course be finished? That is the same as asking whether the prerequisite graph is a DAG. Run Kahn, then compare the number of courses you took with the total. Its sibling Course Schedule II (210) wants the actual order. Kahn already gives you that.
// LeetCode 207: can all courses be finished? Yes exactly when the graph has no cycle.function canFinish(numCourses, prerequisites) { const graph = Array.from({ length: numCourses }, () => []); const indegree = new Array(numCourses).fill(0); for (const [course, prereq] of prerequisites) { graph[prereq].push(course); indegree[course]++; } const queue = []; for (let c = 0; c < numCourses; c++) if (indegree[c] === 0) queue.push(c); let head = 0; while (head < queue.length) { const c = queue[head++]; for (const next of graph[c]) { if (--indegree[next] === 0) queue.push(next); } } return head === numCourses; // head counts how many courses we managed to take}console.log(canFinish(2, [[1, 0]])); // true (take 0, then 1)console.log(canFinish(2, [[1, 0], [0, 1]])); // false (each needs the other first)console.log(canFinish(4, [[1, 0], [2, 1], [3, 2], [1, 3]])); // false (1 -> 2 -> 3 -> 1 is a loop)Kahn or DFS: which one?
| Kahn (BFS-style) | DFS post-order | |
|---|---|---|
| Core idea | take away tasks that have no prerequisites left | reverse the order in which DFS finishes tasks |
| Data structure | queue + in-degree array | recursion (or a stack) + 3 states per task |
| Cycle detection | fewer than n tasks come out of the queue | an arrow that points to a VISITING task |
| Gives the order | front to back, while it runs | after reversing the finishing list |
| Level by level (e.g. parallel courses) | easy: process the queue one round at a time | needs extra work |
| Deep chains | no recursion, so no stack overflow (the call stack running out of room) | recursion can overflow on chains of about 10,000 or more |
| Time and space | O(V + E), O(V) | O(V + E), O(V) |
Kahn is the safer first choice in interviews. It uses no recursion, and the cycle check is just a count. Use the DFS version when the problem is already a DFS problem (for example 'is this node safe?').
Practice questions
First ask: what are the "tasks", and what does "must come before" mean here? Sometimes the arrows are given (courses). Sometimes you must find them yourself (alien dictionary, recipes). Sometimes the trick is to remove dots from the outside in (minimum height trees).
There are numCourses courses labelled 0 to numCourses - 1. Each pair [a, b] in prerequisites means you must take course b before course a. Return true if you can finish all courses.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
- Build the graph with arrows prerequisite → course. Count each course's in-degree (how many prerequisites it still waits for).
- Put every course with in-degree 0 into a queue (a line: first in, first out).
- Take a course. Lower the in-degree of the courses it unlocks. Add any course that reaches 0 to the queue.
- Answer
trueif the number of courses you took equalsnumCourses.
function canFinish(numCourses, prerequisites) { const graph = Array.from({ length: numCourses }, () => []); const indegree = new Array(numCourses).fill(0); for (const [course, prereq] of prerequisites) { graph[prereq].push(course); indegree[course]++; } const queue = []; for (let c = 0; c < numCourses; c++) if (indegree[c] === 0) queue.push(c); let head = 0; while (head < queue.length) { const c = queue[head++]; for (const next of graph[c]) { if (--indegree[next] === 0) queue.push(next); } } return head === numCourses;}console.log(canFinish(2, [[1, 0]])); // trueconsole.log(canFinish(2, [[1, 0], [0, 1]])); // falseconsole.log(canFinish(3, [])); // trueEach course goes into the queue once and each prerequisite arrow is used once. So the time is O(V + E) and the space is O(V + E). Here V is the number of courses and E is the number of arrows. If there is a cycle, every course on it keeps an in-degree of at least 1, because it waits for the one before it in the loop. So none of them is ever taken, and the count is too small.
Walk the graph with DFS (go as deep as you can, then back up). Keep track of which courses are on the path you are walking now. If you reach a course that is still on this path, you found a cycle. Mark a finished course so you never walk it twice.
function canFinish(numCourses, prerequisites) { const graph = Array.from({ length: numCourses }, () => []); for (const [course, prereq] of prerequisites) graph[prereq].push(course); const state = new Array(numCourses).fill(0); // 0 unseen, 1 on the current path, 2 finished function hasCycle(v) { if (state[v] === 1) return true; if (state[v] === 2) return false; state[v] = 1; for (const next of graph[v]) if (hasCycle(next)) return true; state[v] = 2; return false; } for (let c = 0; c < numCourses; c++) if (hasCycle(c)) return false; return true;}console.log(canFinish(2, [[1, 0]])); // trueconsole.log(canFinish(2, [[1, 0], [0, 1]])); // falseconsole.log(canFinish(3, [])); // trueThe time is the same, O(V + E). State 2 keeps it fast. Without it, a course reachable by many paths would be explored again each time, which can take a very long time. State 1 (instead of a plain visited flag) avoids false alarms on diamond shapes.
Kahn's version has no recursion and gives you the order for free, so it works directly for the next problem. The DFS version is the classic way to test for a cycle. (LeetCode 207.)
Same setup as the previous question. Return an order of the courses that follows every prerequisite. If several orders work, return any of them. If none works, return an empty array.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
Run Kahn's algorithm. Write down every course as it leaves the queue.
function findOrder(numCourses, prerequisites) { const graph = Array.from({ length: numCourses }, () => []); const indegree = new Array(numCourses).fill(0); for (const [course, prereq] of prerequisites) { graph[prereq].push(course); indegree[course]++; } const queue = []; for (let c = 0; c < numCourses; c++) if (indegree[c] === 0) queue.push(c); let head = 0; while (head < queue.length) { const c = queue[head++]; for (const next of graph[c]) { if (--indegree[next] === 0) queue.push(next); } } return queue.length === numCourses ? queue : []; // the queue ends up holding the order}console.log(findOrder(2, [[1, 0]])); // [ 0, 1 ]console.log(findOrder(4, [[1, 0], [2, 0], [3, 1], [3, 2]])); // [ 0, 1, 2, 3 ]console.log(findOrder(2, [[1, 0], [0, 1]])); // []We never remove anything from the queue array. We only move head. So after the loop the array holds every course in the order it was processed. The time is O(V + E).
Start a DFS from every course you have not seen yet, following arrows prerequisite → course. Add a course to a list when it finishes. The reversed list is the schedule. If you meet a course that is still on the current path, there is a cycle.
function findOrder(numCourses, prerequisites) { const graph = Array.from({ length: numCourses }, () => []); for (const [course, prereq] of prerequisites) graph[prereq].push(course); const state = new Array(numCourses).fill(0); // 0 unseen, 1 on the current path, 2 finished const finished = []; let cycle = false; function visit(v) { state[v] = 1; for (const next of graph[v]) { if (state[next] === 1) cycle = true; else if (state[next] === 0) visit(next); } state[v] = 2; finished.push(v); } for (let c = 0; c < numCourses; c++) if (state[c] === 0) visit(c); return cycle ? [] : finished.reverse();}console.log(findOrder(2, [[1, 0]])); // [ 0, 1 ]console.log(findOrder(4, [[1, 0], [2, 0], [3, 1], [3, 2]])); // [ 0, 2, 1, 3 ]console.log(findOrder(2, [[1, 0], [0, 1]])); // []A course finishes only after every course it unlocks has finished. So after reversing, each prerequisite comes before the courses it unlocks. The time is O(V + E). This order is different from Kahn's order, and both are accepted.
Kahn's is shorter and uses a loop instead of recursion. Know the DFS one too, for problems that are already DFS problems. (LeetCode 210.)
A directed graph has n nodes. graph[i] lists the nodes that i has an arrow to. A node is terminal if it has no outgoing arrows. A node is safe if every path that starts from it leads to a terminal node. Return all safe nodes in ascending order.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
Run the three-state DFS. A node is safe when all the nodes it points to are safe. If the DFS meets a node that is still on the current path, or one already known to be unsafe, then the node you started from is unsafe.
function eventualSafeNodes(graph) { const n = graph.length; const state = new Array(n).fill(0); // 0 unseen, 1 on the path or unsafe, 2 safe function isSafe(v) { if (state[v] !== 0) return state[v] === 2; state[v] = 1; // stays 1 if we return false: that marks "unsafe" for (const next of graph[v]) if (!isSafe(next)) return false; state[v] = 2; return true; } const result = []; for (let v = 0; v < n; v++) if (isSafe(v)) result.push(v); return result;}console.log(eventualSafeNodes([[1, 2], [2, 3], [5], [0], [5], [], []])); // [ 2, 4, 5, 6 ]console.log(eventualSafeNodes([[1, 2, 3, 4], [1, 2], [3, 4], [0, 4], []])); // [ 4 ]Every node is explored at most once because we remember its state. So the time is O(V + E). We leave state 1 on purpose when a node fails. A node on a cycle, or one that leads into a cycle, must never be called safe later.
- Reverse every arrow. Also record each node's out-degree (the number of arrows going out of it in the original graph).
- Start the queue with the terminal nodes (out-degree 0).
- When a node is safe, every node that pointed to it has one less unsettled arrow. When a node reaches out-degree 0, it is safe too.
function eventualSafeNodes(graph) { const n = graph.length; const reverse = Array.from({ length: n }, () => []); const outdeg = new Array(n).fill(0); for (let v = 0; v < n; v++) { outdeg[v] = graph[v].length; for (const next of graph[v]) reverse[next].push(v); } const queue = []; for (let v = 0; v < n; v++) if (outdeg[v] === 0) queue.push(v); const safe = new Array(n).fill(false); let head = 0; while (head < queue.length) { const v = queue[head++]; safe[v] = true; for (const prev of reverse[v]) { if (--outdeg[prev] === 0) queue.push(prev); } } const result = []; for (let v = 0; v < n; v++) if (safe[v]) result.push(v); // already in ascending order return result;}console.log(eventualSafeNodes([[1, 2], [2, 3], [5], [0], [5], [], []])); // [ 2, 4, 5, 6 ]console.log(eventualSafeNodes([[1, 2, 3, 4], [1, 2], [3, 4], [0, 4], []])); // [ 4 ]This is topological sort on the reversed graph. The nodes that can never be taken away are exactly the ones that reach a cycle. The time is O(V + E), it uses a loop instead of recursion, and the answer is easy to put in order by scanning the numbers.
Both are O(V + E). The DFS is short. The reversed Kahn avoids recursion. (LeetCode 802. A related problem is 1462, Course Schedule IV, which asks whether one course can reach another.)
A new language uses the lowercase English letters, but in an unknown order. You are given words, a list sorted in dictionary order of that alphabet. Return a string of the letters that appear, in a valid order for the alien alphabet. If the input contradicts itself, return an empty string. If several orders are valid, return any of them.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
- Collect every letter that appears. Each letter is one vertex (one dot in the graph).
- For each pair of next-to-each-other words, find the first place where they differ. Add an arrow
earlier → later(only once). If the second word is the first word's own start (a prefix) and is shorter, the input is invalid. Return an empty string. - Run Kahn's algorithm. If not every letter comes out, there is a cycle. Return an empty string.
function alienOrder(words) { const edges = new Map(); // letter -> set of letters that must come after it const indegree = new Map(); for (const word of words) { for (const ch of word) { if (!edges.has(ch)) { edges.set(ch, new Set()); indegree.set(ch, 0); } } } for (let i = 0; i + 1 < words.length; i++) { const a = words[i], b = words[i + 1]; if (a.length > b.length && a.startsWith(b)) return ""; // "abc" before "ab" is impossible for (let j = 0; j < Math.min(a.length, b.length); j++) { if (a[j] !== b[j]) { if (!edges.get(a[j]).has(b[j])) { edges.get(a[j]).add(b[j]); indegree.set(b[j], indegree.get(b[j]) + 1); } break; // later letters tell us nothing } } } const queue = []; for (const [ch, deg] of indegree) if (deg === 0) queue.push(ch); let head = 0; while (head < queue.length) { const ch = queue[head++]; for (const next of edges.get(ch)) { indegree.set(next, indegree.get(next) - 1); if (indegree.get(next) === 0) queue.push(next); } } return queue.length === edges.size ? queue.join("") : "";}console.log(alienOrder(["wrt", "wrf", "er", "ett", "rftt"])); // wertfconsole.log(alienOrder(["z", "x"])); // zxconsole.log(alienOrder(["z", "x", "z"])); // (empty string)console.log(alienOrder(["abc", "ab"])); // (empty string)The time is O(L + C + E). L is the total number of characters in all words. C is the number of different letters. E is the number of different arrows. Building the arrows reads each pair of next-to-each-other words once. Kahn takes time in line with the letters and arrows. Only the first difference between two words counts. The letters after it say nothing about the order. Letters with no rules can go anywhere, so several answers can be valid.
Build the same graph. Then run the three-state DFS and reverse the finishing order. If you meet a letter that is on the current path, there is a cycle.
function alienOrder(words) { const edges = new Map(); for (const word of words) for (const ch of word) if (!edges.has(ch)) edges.set(ch, new Set()); for (let i = 0; i + 1 < words.length; i++) { const a = words[i], b = words[i + 1]; if (a.length > b.length && a.startsWith(b)) return ""; for (let j = 0; j < Math.min(a.length, b.length); j++) { if (a[j] !== b[j]) { edges.get(a[j]).add(b[j]); break; } } } const state = new Map(); // missing = unseen, 1 = on the path, 2 = finished const finished = []; function visit(ch) { // returns false when a cycle is found state.set(ch, 1); for (const next of edges.get(ch)) { if (state.get(next) === 1) return false; if (!state.has(next) && !visit(next)) return false; } state.set(ch, 2); finished.push(ch); return true; } for (const ch of edges.keys()) if (!state.has(ch) && !visit(ch)) return ""; return finished.reverse().join("");}console.log(alienOrder(["wrt", "wrf", "er", "ett", "rftt"])); // wertfconsole.log(alienOrder(["z", "x"])); // zxconsole.log(alienOrder(["z", "x", "z"])); // (empty string)console.log(alienOrder(["abc", "ab"])); // (empty string)The graph and the time are the same. A Set for each letter's outgoing arrows removes repeats, for example when two pairs both say a is before b.
Kahn's is the usual interview answer, because the cycle check is just a count. Two traps make most attempts fail: the prefix case and counting the same arrow twice. This is a premium (paid) problem on LeetCode (269, "Alien Dictionary"). The statement is copied here.
A tree is a connected graph with no cycles. You get n nodes labelled 0..n-1 and a list of edges (links with no direction). You may choose any node as the root. The height of a rooted tree is the number of edges on the longest path going down from the root. Return the labels of every root that gives the smallest height, in any order.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
Use each node as the root, one by one. Run BFS (visit level by level) and count the levels. That count is the height. Keep the roots whose height equals the smallest one.
function findMinHeightTrees(n, edges) { const graph = Array.from({ length: n }, () => []); for (const [a, b] of edges) { graph[a].push(b); graph[b].push(a); } function height(root) { const seen = new Array(n).fill(false); seen[root] = true; let level = [root], h = -1; while (level.length) { h++; const next = []; for (const v of level) for (const w of graph[v]) if (!seen[w]) { seen[w] = true; next.push(w); } level = next; } return h; } let best = Infinity, result = []; for (let r = 0; r < n; r++) { const h = height(r); if (h < best) { best = h; result = [r]; } else if (h === best) result.push(r); } return result;}console.log(findMinHeightTrees(4, [[1, 0], [1, 2], [1, 3]])); // [ 1 ]console.log(findMinHeightTrees(6, [[3, 0], [3, 1], [3, 2], [3, 4], [5, 4]])); // [ 3, 4 ]console.log(findMinHeightTrees(1, [])); // [ 0 ]One BFS takes O(n) time and we run n of them, so the total is O(n²). It is correct, but too slow when n is up to 20,000.
- A node's degree is its number of neighbours. A leaf has degree 1.
- Remove all current leaves at once (one "layer"). This lowers the degree of their neighbours. Neighbours that become leaves form the next layer.
- Stop when 2 or fewer nodes remain. These are the centre of the tree, and they are the answer.
function findMinHeightTrees(n, edges) { if (n <= 2) return Array.from({ length: n }, (_, i) => i); const graph = Array.from({ length: n }, () => []); const degree = new Array(n).fill(0); for (const [a, b] of edges) { graph[a].push(b); graph[b].push(a); degree[a]++; degree[b]++; } let leaves = []; for (let v = 0; v < n; v++) if (degree[v] === 1) leaves.push(v); let remaining = n; while (remaining > 2) { remaining -= leaves.length; // delete this whole layer const nextLeaves = []; for (const leaf of leaves) { for (const nb of graph[leaf]) { if (--degree[nb] === 1) nextLeaves.push(nb); // nb has become a leaf } } leaves = nextLeaves; } return leaves;}console.log(findMinHeightTrees(4, [[1, 0], [1, 2], [1, 3]])); // [ 1 ]console.log(findMinHeightTrees(6, [[3, 0], [3, 1], [3, 2], [3, 4], [5, 4]])); // [ 3, 4 ]console.log(findMinHeightTrees(1, [])); // [ 0 ]Each node is removed once and each edge is looked at twice. So the time and space are O(n). The last one or two nodes are the middle of the longest path. That is why they give the smallest height. A tree has at most two such centres, so the loop can stop when 2 or fewer nodes remain.
Use the layer-removing method. It works like Kahn's algorithm on a tree with no arrow directions. The brute force is useful to check small inputs. (LeetCode 310.)
You are given n courses labelled 1..n and relations. A pair [prev, next] means course prev must be finished before next starts. In one semester you can take any number of courses, as long as you finished all their prerequisites in earlier semesters. Return the smallest number of semesters needed to take every course. Return -1 if it is impossible.
Hint 1
[prev, next] is an arrow prev → next. In one semester you may take every course whose prerequisites are all done.Hint 2
Hint 3
Show all 2 solutions with explanations
Process the queue one whole level at a time. Each level is one semester. Count the semesters and the courses taken. If you did not take all courses, there is a cycle.
function minimumSemesters(n, relations) { const graph = Array.from({ length: n + 1 }, () => []); // courses are numbered from 1 const indegree = new Array(n + 1).fill(0); for (const [prev, next] of relations) { graph[prev].push(next); indegree[next]++; } let current = []; for (let c = 1; c <= n; c++) if (indegree[c] === 0) current.push(c); let semesters = 0, taken = 0; while (current.length) { semesters++; const nextRound = []; for (const c of current) { taken++; for (const next of graph[c]) if (--indegree[next] === 0) nextRound.push(next); } current = nextRound; } return taken === n ? semesters : -1;}console.log(minimumSemesters(3, [[1, 3], [2, 3]])); // 2console.log(minimumSemesters(3, [[1, 2], [2, 3], [3, 1]])); // -1console.log(minimumSemesters(4, [[1, 2], [2, 3], [1, 4]])); // 3Each course is taken in the earliest semester in which all its prerequisites are finished. So the number of rounds equals the length of the longest prerequisite chain (counted in courses). The time is O(n + E).
The earliest semester for a course is 1 + the biggest earliest-semester among its prerequisites. Work this out with DFS and save each answer so you do not repeat work (a cache). Use three states to find a cycle.
function minimumSemesters(n, relations) { const prereqs = Array.from({ length: n + 1 }, () => []); // prereqs[c] = courses needed before c for (const [prev, next] of relations) prereqs[next].push(prev); const state = new Array(n + 1).fill(0); // 0 unseen, 1 on the path, 2 done const semester = new Array(n + 1).fill(0); // earliest semester for each course, once done function earliest(c) { // returns -1 if a cycle is found if (state[c] === 2) return semester[c]; if (state[c] === 1) return -1; state[c] = 1; let best = 0; for (const p of prereqs[c]) { const s = earliest(p); if (s === -1) return -1; best = Math.max(best, s); } state[c] = 2; semester[c] = best + 1; return semester[c]; } let answer = 0; for (let c = 1; c <= n; c++) { const s = earliest(c); if (s === -1) return -1; answer = Math.max(answer, s); } return answer;}console.log(minimumSemesters(3, [[1, 3], [2, 3]])); // 2console.log(minimumSemesters(3, [[1, 2], [2, 3], [3, 1]])); // -1console.log(minimumSemesters(4, [[1, 2], [2, 3], [1, 4]])); // 3Memoization is a technique that saves (remembers) the result of a call, so the same call is never worked out twice. Here it means each course is worked out only once. The time is O(n + E). This is the idea of the longest path in a DAG. It only works because the graph has no cycles.
Kahn level by level is the cleaner choice, because the number of rounds is the answer. This is a premium (paid) problem on LeetCode (1136, "Parallel Courses"). The statement is copied here.
You have recipes[i]. Each one needs the ingredients listed in ingredients[i]. You also start with a list of supplies (you have as many of each as you need). An ingredient can be another recipe. All recipe names and supplies are different from each other. Return every recipe you can make, in any order.
Hint 1
Hint 2
Hint 3
Show all 2 solutions with explanations
Keep a set of the items you have (the supplies). Go over all recipes again and again. Whenever all ingredients of a recipe are available, make it and add it to the set. Stop after a pass in which no new recipe was made.
function findAllRecipes(recipes, ingredients, supplies) { const have = new Set(supplies); const made = new Array(recipes.length).fill(false); const result = []; let changed = true; while (changed) { changed = false; for (let i = 0; i < recipes.length; i++) { if (made[i]) continue; if (ingredients[i].every((x) => have.has(x))) { made[i] = true; have.add(recipes[i]); result.push(recipes[i]); changed = true; } } } return result;}console.log(findAllRecipes(["bread"], [["yeast", "flour"]], ["yeast", "flour", "corn"])); // [ 'bread' ]console.log(findAllRecipes(["bread", "sandwich"], [["yeast", "flour"], ["bread", "meat"]], ["yeast", "flour", "meat"])); // [ 'bread', 'sandwich' ]console.log(findAllRecipes(["a", "b"], [["b"], ["a"]], [])); // []Each pass costs the total number of ingredients (I). In the worst case there are as many passes as recipes (R). So the time is O(R × I). This is fine for small inputs, but it keeps checking recipes that are far from ready.
- For each ingredient, remember which recipes need it. For each recipe, count its ingredients. This count is its in-degree.
- Put all supplies in the queue.
- Take an item from the queue. For each recipe that needs it, lower that recipe's count by 1. At 0, you can make the recipe. Save it in the answer and add it to the queue, because other recipes may need it.
function findAllRecipes(recipes, ingredients, supplies) { const neededBy = new Map(); // item -> recipes that use it const missing = new Map(); // recipe -> ingredients still unavailable for (let i = 0; i < recipes.length; i++) { missing.set(recipes[i], ingredients[i].length); for (const item of ingredients[i]) { if (!neededBy.has(item)) neededBy.set(item, []); neededBy.get(item).push(recipes[i]); } } const queue = [...supplies]; let head = 0; const result = []; while (head < queue.length) { const item = queue[head++]; for (const recipe of neededBy.get(item) || []) { missing.set(recipe, missing.get(recipe) - 1); if (missing.get(recipe) === 0) { result.push(recipe); queue.push(recipe); } } } return result;}console.log(findAllRecipes(["bread"], [["yeast", "flour"]], ["yeast", "flour", "corn"])); // [ 'bread' ]console.log(findAllRecipes(["bread", "sandwich"], [["yeast", "flour"], ["bread", "meat"]], ["yeast", "flour", "meat"])); // [ 'bread', 'sandwich' ]console.log(findAllRecipes(["a", "b"], [["b"], ["a"]], [])); // []Each ingredient entry is used once, so the time is O(S + R + I). A recipe on a cycle, or one that needs something nobody supplies, never reaches count 0. It is simply never reported, so you need no separate cycle check.
Use Kahn's algorithm. It runs in linear time instead of repeated passes, and the supplies act as the vertices with in-degree 0. (LeetCode 2115.)
Make it stick
- Say what a topological order and a DAG are, and which graphs have no valid order.
- Write Kahn's algorithm from memory, including the cycle check.
- Explain the three DFS states and why reversing the finish order works.
- Explain why a plain visited set gives a wrong cycle answer on a directed diamond.
- Say which way the arrow points for the pair [a, b] in Course Schedule.
- Name two problems where you must build the graph from other data first.
Revisit schedule: solve two of the questions again tomorrow, again in 3 days, and once more in a week — without looking at the answers. Each time it will feel easier. That is the memory forming.
What's next
So far every arrow counted the same, or had no length at all. In Lesson 52 arrows get costs. You will use BFS when all costs are equal. Then you will use Dijkstra with a priority queue (a line where the smallest item always comes out first) to find the cheapest route. Last comes Bellman–Ford, for when the number of stops is limited.
Practise on LeetCode
Real interview problems that use exactly what this lesson teaches. Solve them in JavaScript after the practice questions above.
Published by Jainik · Last updated · How these lessons are made
Keep learning
Part of the study guide
Related series