Planets and Kingdoms
You will see how two depth-first passes over a graph and its transpose group every mutually-reachable planet into one kingdom.
A game world has n planets and m one-way teleporters. Planets a and b belong to the same kingdom exactly when there is a route from a to b and a route back from b to a, each route following any number of teleporters in the direction they point.
The planets are numbered 1 to n. Each teleporter is a pair [a, b]: it lets you travel from planet a to planet b, never the other way, unless a separate teleporter says so. Work out how many kingdoms the planets split into, and which kingdom every planet belongs to. Kingdoms may be labeled any way you like, from 1 to the number of kingdoms, as long as two planets share a label exactly when they share a kingdom.
Worked Examples
n = 5, teleporters = [[1,2],[2,3],[3,1],[3,4],[4,5],[5,4]]2 kingdoms: planets 1, 2, 3 in kingdom 1; planets 4, 5 in kingdom 2n = 3, teleporters = [[1,2],[2,3]]3 kingdoms: planet 1 alone, planet 2 alone, planet 3 alone⚖️Formal Constraints & Bounds
1 <= n <= 1051 <= m <= 2 * 105(the number of teleporters)1 <= a, b <= n
Why It Works & Core Invariant
A planet finishes the first pass only once everything it can reach has already finished, so processing finish order from last to first always starts the second pass at a planet with no teleporter, in the original graph, into any kingdom not yet found; walking the transpose from there then reaches exactly the planets that can also reach it back.
Real-World Scenario & Production Applications
Finding mutually-dependent clusters in any directed relationship: modules whose imports form cycles in a build graph, accounts that can all wire funds to each other in a payments graph, or web pages that can each be reached from every other by following links.
Step-by-Step Execution Trace Table
Step-by-Step Kosaraju's Algorithm (n = 5, teleporters = [[1,2],[2,3],[3,1],[3,4],[4,5],[5,4]])
- Step 1 (First Pass, on
adj): DFS from planet 1: 1 -> 2 -> 3 -> 1 (already visited, skip) -> 4 -> 5 -> 4 (already visited): planet 5 has no unvisited neighbor left, so it finishes first,order = [5]; then 4 finishes:order = [5, 4]; then 3:order = [5, 4, 3]; then 2:order = [5, 4, 3, 2]; then 1:order = [5, 4, 3, 2, 1]. - Step 2 (Second Pass Starts at the Last Finisher):
reversed(order) = [1, 2, 3, 4, 5]. Start at planet 1, walkradj:radj[1] = [3](from teleporter 3 -> 1),radj[3] = [2],radj[2] = [1](already labeled). Kingdom 1 = {1, 2, 3}. - Step 3 (Next Unlabeled Finisher): 2 and 3 are already labeled; 4 is not. Start at planet 4, walk
radj:radj[4] = [3, 5](3 already labeled, 5 not), so kingdom grows to include 5;radj[5] = [4](already labeled). Kingdom 2 = {4, 5}. - Step 4 (Done): Every planet has a kingdom: 1, 2, 3 -> kingdom 1; 4, 5 -> kingdom 2 ✅.
n = 5, teleporters = [[1,2],[2,3],[3,1],[3,4],[4,5],[5,4]]Expected:2 kingdoms: planets 1, 2, 3 in kingdom 1; planets 4, 5 in kingdom 2| 1 | adj, radj = build the graph and its transpose |
| 2 | for start in 1..n: if unvisited, iterative DFS on adj, append(node) only when it has no unvisited neighbor left |
| 3 | for node in reversed(order): # decreasing finish time |
| 4 | if already labeled: continue |
| 5 | new label; DFS from node on radj, labeling everything reached |
| 6 | return the labels |
Target: Planets and Kingdoms. Both directions are kept: adj drives the first pass, radj drives the second
Maintains visited set or 3-color states (WHITE=unvisited, GREY=visiting/cycle, BLACK=processed) across adjacency lists.
Loop outer vertices 0..V-1 to handle disconnected subgraphs; explore edges via BFS/DFS/PriorityQueue.
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Kosaraju's algorithm finds every strongly connected component of a directed graph with two depth-first passes. The first pass walks the graph as given and records each node's finish order: the order in which a DFS runs out of unvisited neighbors to descend into. The second pass walks the transpose graph -- every edge reversed -- starting from unvisited nodes in decreasing finish order. Each tree the second pass grows is exactly one strongly connected component, because a node that finished late in the first pass has nothing left unexplored beneath it, so it is a "source" of whatever graph structure remains, and walking the transpose from a source reaches precisely the nodes that can also reach it back in the original graph.
🌌 The Analogy: Draining a Reservoir System From the Lowest Outlet
Picture a network of reservoirs connected by one-way spillways. The first pass is a survey: starting anywhere, follow spillways downstream and note, for each reservoir, the moment you have nothing left downstream of it to explore -- reservoirs that are true dead ends (or whose whole downstream network is already surveyed) get noted first. The second pass works backward: starting from the LAST reservoir noted (the one truly furthest downstream, still unassigned), follow the spillways upstream instead. Every reservoir reached this way can flow downstream back to the start (that is how you reached it walking upstream), and the start can flow to all of them too, since they were reachable walking downstream in the first pass restricted to this same neighborhood -- so they are one kingdom, drained together. Moving to the next unassigned reservoir (again picking the latest-noted one) always starts a fresh kingdom, because every reservoir it could drain into upstream is already claimed.
🪄 Breaking Down the Code's "Magic Trick"
for planet in reversed(order): # decreasing finish time: always a source of what's left if kingdom[planet] != -1: continue kingdom_id += 1 kingdom[planet] = kingdom_id stack = [planet] while stack: u = stack.pop() for v in radj[u]: # the TRANSPOSE graph, not adj again if kingdom[v] == -1: kingdom[v] = kingdom_id stack.append(v) Two things have to both be true for this to work. First, reversed(order) -- processing late finishers first -- guarantees each new component's starting planet has no edge, in the original graph, into any component that hasn't been found yet; if a lower-finishing planet were picked instead, its component could still have an edge into a not-yet-discovered one, and the walk would wrongly reach across that boundary. Second, radj: walking the transpose from that starting planet reaches every planet that can reach IT in the original graph. Combined with the fact those planets are also reachable FROM it (they finished no later, in the same downstream neighborhood surveyed in pass one), the set found is exactly the group that can reach each other both ways -- a kingdom.
💡 Summary
Two O(V + E) depth-first passes -- one to order nodes by how "finished" they are, one on the reversed graph in that order -- peel every strongly connected component off a directed graph in linear time, with no extra structure beyond the two adjacency lists and a visited/component array.
Checking only one direction: a node can reach everyone but not be reached by everyone (or the reverse); strong connectivity needs the SCC count to be exactly 1, not just "one DFS from node 1 reaches all n nodes".
Assuming any two components give a witness pair: an arbitrary pair of nodes from two different components is not always unreachable in one specific direction; the component with the topological order's last id cannot reach the component with the first id (never the other way round), so the witness must be built from that ordering, not any two mismatched nodes.
Off-by-one on kingdom labels: cities are 1-indexed and CSES-style outputs commonly start labels at 1, not 0;
comparrays built 0-indexed still need[1:]slicing or a+1shift before they are printed.Recomputing SCCs per query pair: for large n, do not re-run Kosaraju's algorithm once per candidate pair; compute the component labels once, in one
O(V + E)pass, then read off the answer.
4-Phase Thought Process Model
You will see how a senior engineer spots a two-way-reachability grouping question and defends Kosaraju's two passes.
Pattern Recognition Signals
The 10-second spot
Two planets belong together exactly when "there is a route both from a to b and from b to a": mutual reachability, on a directed graph, is a strongly connected component question, not a plain reachability or shortest-path one. "Determine for each planet its kingdom" asks for every group at once, up to 105 planets and 2 * 105 teleporters, so the answer has to come from one linear pass, not a search per pair.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
comp[u] == comp[v] should hold exactly when u and v can reach each other. Kosaraju's invariant is on the finish order: once the first DFS pass is done, a node's position in order reflects that everything reachable from it is already finished, so starting the second pass from the node finishing last, and walking the transpose graph from there, reaches exactly the nodes that can reach it back.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
The second pass walks
radj, the transpose, notadjagain:for v in radj[u]. Reusingadjfolds every node merely reachable FROM a component into it, even when the trip back doesn't exist.The second pass must start from unvisited nodes in
reversed(order)-- decreasing finish time. Starting from the front oforder(increasing finish time, or no particular order) can start a component at a node that still has an unfound component ahead of it in the condensation, and the transpose walk then crosses into it.DFS is run iteratively, with an explicit stack, never recursively: at
nup to105planets, one long teleporter chain overflows Python's default recursion limit.A node is appended to
orderonly once it has no unvisited neighbor left, not the moment it is first visited: that is finish order, and the whole argument for why the second pass can't cross an unfound boundary depends on it being finish order specifically.
The 60-Second Interview Pitch
Say this out loud before you type a single line
This is a mutual-reachability grouping question on a directed graph, so I'd use Kosaraju's algorithm: two depth-first passes. The first pass walks the graph as given and records each node's finish order -- the order in which its DFS runs out of unvisited neighbors below it. The second pass walks the transpose graph, every edge reversed, starting new trees at unvisited nodes taken in decreasing finish order. Each tree the second pass grows is exactly one kingdom. The trap is picking the wrong graph or the wrong order for the second pass: it has to be the transpose, and it has to start from the latest unfinished node, or the walk can cross into a kingdom that hasn't been found yet. Both passes are iterative DFS, since a recursive one can overflow the call stack on a long chain of planets. It's
O(V + E)time andO(V + E)space.
So: first pass on adj records finish order, second pass on radj from unvisited nodes in decreasing finish order labels one kingdom per tree.
Complexity & Mathematical Proof
O(V + E)
Look at the code: building adj and radj touches each of the E teleporters twice, O(V + E). The first pass's for start loop runs V times, but the inner while stack work, across the whole pass, visits each node once (visited guards re-entry) and scans each node's out-edges once total across all its stack frames, so the whole pass is O(V + E). The second pass is the same shape on radj: O(V + E). Building comp and slicing it is O(V). Total: O(V + E).
O(V + E)
adj and radj together hold 2E entries: O(E). visited, order, comp are each O(V). The explicit stack in either pass holds at most one frame per node on the current DFS path, O(V). Nothing is recursive, so there is no hidden call-stack cost beyond that.
T(V, E) = (V + E) [build adj, radj] + (V + E) [first pass] + (V + E) [second pass] = O(V + E)
Look at the code: building adj and radj touches each of the E teleporters twice, O(V + E). The first pass's for start loop runs V times, but the inner while stack work, across the whole pass, visits each node once (visited guards re-entry) and scans each node's out-edges once total across all its stack frames, so the whole pass is O(V + E). The second pass is the same shape on radj: O(V + E). Building comp and slicing it is O(V). Total: O(V + E).
Derivation Progression
2E appends: O(V + E)
Every teleporter is added once to adj and once to radj.
V starts, each node visited once, each out-edge scanned once: O(V + E)
The visited guard means every node's stack frame is pushed exactly once, and every edge is looked at exactly once across the whole pass.
V starts (in reversed(order)), each node visited once, each transpose edge scanned once: O(V + E)
Same shape as the first pass, run on radj instead of adj.
Variable Definitions
Number of planets, n
Number of teleporters, len(teleporters)
Memory Architecture & Bounds
O(1): both passes use an explicit stack, no recursion
O(V + E): adj and radj hold 2E entries; visited, order and comp hold O(V) each
O(V): the V kingdom labels of comp[1:]
Boundary Best / Worst Cases
: every planet and teleporter is touched once regardless of the graph's shape.
.
.
Graph & Tree Traversal Frontier
Senior SWE Deconstruction & Hardware Caveats
High-frequency interview keyword triggers that immediately dictate this algorithmic archetype.
Input boundaries and maximum time complexity budgets.
Subtle off-by-one errors and edge conditions.
Core Algorithmic State Invariants
Every unvisited cell initiates a new distinct connected component. Traversal (DFS/BFS) marks all reachable symmetric neighbors visited, preventing duplicate component counts.
Cells are mutated in-place ('0') or recorded in a visited set immediately upon discovery, preventing exponential redundant enqueuing and recursion stack overflow.
Each cell is visited a constant number of times (bounded by 4 * M * N operations), terminating cleanly when all grid components are exhausted.
Planets and Kingdoms
You will see how two depth-first passes over a graph and its transpose group every mutually-reachable planet into one kingdom.
A game world has n planets and m one-way teleporters. Planets a and b belong to the same kingdom exactly when there is a route from a to b and a route back from b to a, each route following any number of teleporters in the direction they point.
The planets are numbered 1 to n. Each teleporter is a pair [a, b]: it lets you travel from planet a to planet b, never the other way, unless a separate teleporter says so. Work out how many kingdoms the planets split into, and which kingdom every planet belongs to. Kingdoms may be labeled any way you like, from 1 to the number of kingdoms, as long as two planets share a label exactly when they share a kingdom.
Worked Examples
n = 5, teleporters = [[1,2],[2,3],[3,1],[3,4],[4,5],[5,4]]2 kingdoms: planets 1, 2, 3 in kingdom 1; planets 4, 5 in kingdom 2n = 3, teleporters = [[1,2],[2,3]]3 kingdoms: planet 1 alone, planet 2 alone, planet 3 alone⚖️Formal Constraints & Bounds
1 <= n <= 1051 <= m <= 2 * 105(the number of teleporters)1 <= a, b <= n
Why It Works & Core Invariant
A planet finishes the first pass only once everything it can reach has already finished, so processing finish order from last to first always starts the second pass at a planet with no teleporter, in the original graph, into any kingdom not yet found; walking the transpose from there then reaches exactly the planets that can also reach it back.
Real-World Scenario & Production Applications
Finding mutually-dependent clusters in any directed relationship: modules whose imports form cycles in a build graph, accounts that can all wire funds to each other in a payments graph, or web pages that can each be reached from every other by following links.
Step-by-Step Execution Trace Table
Step-by-Step Kosaraju's Algorithm (n = 5, teleporters = [[1,2],[2,3],[3,1],[3,4],[4,5],[5,4]])
- Step 1 (First Pass, on
adj): DFS from planet 1: 1 -> 2 -> 3 -> 1 (already visited, skip) -> 4 -> 5 -> 4 (already visited): planet 5 has no unvisited neighbor left, so it finishes first,order = [5]; then 4 finishes:order = [5, 4]; then 3:order = [5, 4, 3]; then 2:order = [5, 4, 3, 2]; then 1:order = [5, 4, 3, 2, 1]. - Step 2 (Second Pass Starts at the Last Finisher):
reversed(order) = [1, 2, 3, 4, 5]. Start at planet 1, walkradj:radj[1] = [3](from teleporter 3 -> 1),radj[3] = [2],radj[2] = [1](already labeled). Kingdom 1 = {1, 2, 3}. - Step 3 (Next Unlabeled Finisher): 2 and 3 are already labeled; 4 is not. Start at planet 4, walk
radj:radj[4] = [3, 5](3 already labeled, 5 not), so kingdom grows to include 5;radj[5] = [4](already labeled). Kingdom 2 = {4, 5}. - Step 4 (Done): Every planet has a kingdom: 1, 2, 3 -> kingdom 1; 4, 5 -> kingdom 2 ✅.
n = 5, teleporters = [[1,2],[2,3],[3,1],[3,4],[4,5],[5,4]]Expected:2 kingdoms: planets 1, 2, 3 in kingdom 1; planets 4, 5 in kingdom 2| 1 | adj, radj = build the graph and its transpose |
| 2 | for start in 1..n: if unvisited, iterative DFS on adj, append(node) only when it has no unvisited neighbor left |
| 3 | for node in reversed(order): # decreasing finish time |
| 4 | if already labeled: continue |
| 5 | new label; DFS from node on radj, labeling everything reached |
| 6 | return the labels |
Target: Planets and Kingdoms. Both directions are kept: adj drives the first pass, radj drives the second
Maintains visited set or 3-color states (WHITE=unvisited, GREY=visiting/cycle, BLACK=processed) across adjacency lists.
Loop outer vertices 0..V-1 to handle disconnected subgraphs; explore edges via BFS/DFS/PriorityQueue.
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Kosaraju's algorithm finds every strongly connected component of a directed graph with two depth-first passes. The first pass walks the graph as given and records each node's finish order: the order in which a DFS runs out of unvisited neighbors to descend into. The second pass walks the transpose graph -- every edge reversed -- starting from unvisited nodes in decreasing finish order. Each tree the second pass grows is exactly one strongly connected component, because a node that finished late in the first pass has nothing left unexplored beneath it, so it is a "source" of whatever graph structure remains, and walking the transpose from a source reaches precisely the nodes that can also reach it back in the original graph.
🌌 The Analogy: Draining a Reservoir System From the Lowest Outlet
Picture a network of reservoirs connected by one-way spillways. The first pass is a survey: starting anywhere, follow spillways downstream and note, for each reservoir, the moment you have nothing left downstream of it to explore -- reservoirs that are true dead ends (or whose whole downstream network is already surveyed) get noted first. The second pass works backward: starting from the LAST reservoir noted (the one truly furthest downstream, still unassigned), follow the spillways upstream instead. Every reservoir reached this way can flow downstream back to the start (that is how you reached it walking upstream), and the start can flow to all of them too, since they were reachable walking downstream in the first pass restricted to this same neighborhood -- so they are one kingdom, drained together. Moving to the next unassigned reservoir (again picking the latest-noted one) always starts a fresh kingdom, because every reservoir it could drain into upstream is already claimed.
🪄 Breaking Down the Code's "Magic Trick"
for planet in reversed(order): # decreasing finish time: always a source of what's left if kingdom[planet] != -1: continue kingdom_id += 1 kingdom[planet] = kingdom_id stack = [planet] while stack: u = stack.pop() for v in radj[u]: # the TRANSPOSE graph, not adj again if kingdom[v] == -1: kingdom[v] = kingdom_id stack.append(v) Two things have to both be true for this to work. First, reversed(order) -- processing late finishers first -- guarantees each new component's starting planet has no edge, in the original graph, into any component that hasn't been found yet; if a lower-finishing planet were picked instead, its component could still have an edge into a not-yet-discovered one, and the walk would wrongly reach across that boundary. Second, radj: walking the transpose from that starting planet reaches every planet that can reach IT in the original graph. Combined with the fact those planets are also reachable FROM it (they finished no later, in the same downstream neighborhood surveyed in pass one), the set found is exactly the group that can reach each other both ways -- a kingdom.
💡 Summary
Two O(V + E) depth-first passes -- one to order nodes by how "finished" they are, one on the reversed graph in that order -- peel every strongly connected component off a directed graph in linear time, with no extra structure beyond the two adjacency lists and a visited/component array.
Checking only one direction: a node can reach everyone but not be reached by everyone (or the reverse); strong connectivity needs the SCC count to be exactly 1, not just "one DFS from node 1 reaches all n nodes".
Assuming any two components give a witness pair: an arbitrary pair of nodes from two different components is not always unreachable in one specific direction; the component with the topological order's last id cannot reach the component with the first id (never the other way round), so the witness must be built from that ordering, not any two mismatched nodes.
Off-by-one on kingdom labels: cities are 1-indexed and CSES-style outputs commonly start labels at 1, not 0;
comparrays built 0-indexed still need[1:]slicing or a+1shift before they are printed.Recomputing SCCs per query pair: for large n, do not re-run Kosaraju's algorithm once per candidate pair; compute the component labels once, in one
O(V + E)pass, then read off the answer.
4-Phase Thought Process Model
You will see how a senior engineer spots a two-way-reachability grouping question and defends Kosaraju's two passes.
Pattern Recognition Signals
The 10-second spot
Two planets belong together exactly when "there is a route both from a to b and from b to a": mutual reachability, on a directed graph, is a strongly connected component question, not a plain reachability or shortest-path one. "Determine for each planet its kingdom" asks for every group at once, up to 105 planets and 2 * 105 teleporters, so the answer has to come from one linear pass, not a search per pair.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
comp[u] == comp[v] should hold exactly when u and v can reach each other. Kosaraju's invariant is on the finish order: once the first DFS pass is done, a node's position in order reflects that everything reachable from it is already finished, so starting the second pass from the node finishing last, and walking the transpose graph from there, reaches exactly the nodes that can reach it back.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
The second pass walks
radj, the transpose, notadjagain:for v in radj[u]. Reusingadjfolds every node merely reachable FROM a component into it, even when the trip back doesn't exist.The second pass must start from unvisited nodes in
reversed(order)-- decreasing finish time. Starting from the front oforder(increasing finish time, or no particular order) can start a component at a node that still has an unfound component ahead of it in the condensation, and the transpose walk then crosses into it.DFS is run iteratively, with an explicit stack, never recursively: at
nup to105planets, one long teleporter chain overflows Python's default recursion limit.A node is appended to
orderonly once it has no unvisited neighbor left, not the moment it is first visited: that is finish order, and the whole argument for why the second pass can't cross an unfound boundary depends on it being finish order specifically.
The 60-Second Interview Pitch
Say this out loud before you type a single line
This is a mutual-reachability grouping question on a directed graph, so I'd use Kosaraju's algorithm: two depth-first passes. The first pass walks the graph as given and records each node's finish order -- the order in which its DFS runs out of unvisited neighbors below it. The second pass walks the transpose graph, every edge reversed, starting new trees at unvisited nodes taken in decreasing finish order. Each tree the second pass grows is exactly one kingdom. The trap is picking the wrong graph or the wrong order for the second pass: it has to be the transpose, and it has to start from the latest unfinished node, or the walk can cross into a kingdom that hasn't been found yet. Both passes are iterative DFS, since a recursive one can overflow the call stack on a long chain of planets. It's
O(V + E)time andO(V + E)space.
So: first pass on adj records finish order, second pass on radj from unvisited nodes in decreasing finish order labels one kingdom per tree.
Complexity & Mathematical Proof
O(V + E)
Look at the code: building adj and radj touches each of the E teleporters twice, O(V + E). The first pass's for start loop runs V times, but the inner while stack work, across the whole pass, visits each node once (visited guards re-entry) and scans each node's out-edges once total across all its stack frames, so the whole pass is O(V + E). The second pass is the same shape on radj: O(V + E). Building comp and slicing it is O(V). Total: O(V + E).
O(V + E)
adj and radj together hold 2E entries: O(E). visited, order, comp are each O(V). The explicit stack in either pass holds at most one frame per node on the current DFS path, O(V). Nothing is recursive, so there is no hidden call-stack cost beyond that.
T(V, E) = (V + E) [build adj, radj] + (V + E) [first pass] + (V + E) [second pass] = O(V + E)
Look at the code: building adj and radj touches each of the E teleporters twice, O(V + E). The first pass's for start loop runs V times, but the inner while stack work, across the whole pass, visits each node once (visited guards re-entry) and scans each node's out-edges once total across all its stack frames, so the whole pass is O(V + E). The second pass is the same shape on radj: O(V + E). Building comp and slicing it is O(V). Total: O(V + E).
Derivation Progression
2E appends: O(V + E)
Every teleporter is added once to adj and once to radj.
V starts, each node visited once, each out-edge scanned once: O(V + E)
The visited guard means every node's stack frame is pushed exactly once, and every edge is looked at exactly once across the whole pass.
V starts (in reversed(order)), each node visited once, each transpose edge scanned once: O(V + E)
Same shape as the first pass, run on radj instead of adj.
Variable Definitions
Number of planets, n
Number of teleporters, len(teleporters)
Memory Architecture & Bounds
O(1): both passes use an explicit stack, no recursion
O(V + E): adj and radj hold 2E entries; visited, order and comp hold O(V) each
O(V): the V kingdom labels of comp[1:]
Boundary Best / Worst Cases
: every planet and teleporter is touched once regardless of the graph's shape.
.
.
Graph & Tree Traversal Frontier
Senior SWE Deconstruction & Hardware Caveats
High-frequency interview keyword triggers that immediately dictate this algorithmic archetype.
Input boundaries and maximum time complexity budgets.
Subtle off-by-one errors and edge conditions.
Core Algorithmic State Invariants
Every unvisited cell initiates a new distinct connected component. Traversal (DFS/BFS) marks all reachable symmetric neighbors visited, preventing duplicate component counts.
Cells are mutated in-place ('0') or recorded in a visited set immediately upon discovery, preventing exponential redundant enqueuing and recursion stack overflow.
Each cell is visited a constant number of times (bounded by 4 * M * N operations), terminating cleanly when all grid components are exhausted.
| Canonical Invariant | Concrete Code | Engineering Rationale |
|---|---|---|
| Build the graph and its transpose | adj = [[] for _ in range(n + 1)]
radj = [[] for _ in range(n + 1)]
for a, b in teleporters:
adj[a].append(b)
radj[b].append(a) | Both directions are kept: adj drives the first pass, radj drives the second |
| First pass: record finish order on the original graph | stack = [(start, iter(adj[start]))]
...
order.append(planet) | A planet is only appended once every teleporter it can reach has already finished |
| Second pass: unvisited starts, decreasing finish order | for planet in reversed(order): | The trap line: this always starts the next component at a source of what is left of the condensation |
| Second pass walks the transpose graph | for v in radj[u]:
if kingdom[v] == -1:
kingdom[v] = kingdom_id
stack.append(v) | The other trap: radj, not adj, or the walk leaks past this kingdom's own members |
| Return one label per node | return kingdom[1:] | Planets and Kingdoms accepts any numbering, so labels start at 1 and count up as each kingdom is found |