Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Graph Algorithms

Builds on Cycle Detection, which you haven’t finished yet.

P3HardBuilt on Flood Fill~6 min speedrun

Strongly Connected Components (Kosaraju / Tarjan)

Built on
Cycle DetectionRecord the finish order, then search the reversed graph in reverse finish order, and each search finds one group.
The problem

Planets and Kingdoms (CSES 1683)(opens the source problem)

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 which kingdom every planet belongs to: return a list whose i-th value is the kingdom label of planet i + 1. Labels run from 1 to the number of kingdoms k, so k is the largest label. Kingdoms may be labeled any way you like, as long as two planets share a label exactly when they share a kingdom.

Example 1
Input:n = 5, teleporters = [[1,2],[2,3],[3,1],[3,4],[4,5],[5,4]]
Output:[1, 1, 1, 2, 2]
Planets 1, 2 and 3 form a cycle, so each reaches the other two and back: one kingdom. Planets 4 and 5 form their own cycle, a second kingdom. A teleporter runs from 3 into 4, but nothing runs back from kingdom 2 into kingdom 1, so the two groups stay separate: k = 2. Any labeling of the same two groups, such as [2, 2, 2, 1, 1], is also accepted.
123450

Strongly Connected Components

graph ➔ finish order, then search the reversed graph ➔ one group per search
Spot it when: You must group the nodes of a directed graph so that two nodes share a group exactly when each can reach the other (mutual reachability, cycles of dependencies, one-way links).
Strongly Connected Components
Python
1from typing import List
2 
3def kosaraju_scc(n: int, edges: List[List[int]]) -> List[int]:
4 """Kosaraju's algorithm (the core of Planets and Kingdoms): each node's strongly
5 connected component id, 0-indexed, in topological order of the condensation.
6 Time: O(V + E) | Space: O(V + E)
7 """
8 adj = [[] for _ in range(n + 1)]
9 radj = [[] for _ in range(n + 1)]
10 for u, v in edges:
11 adj[u].append(v)
12 radj[v].append(u)
13 
14 visited = [False] * (n + 1)
15 # finish order, first pass, on the ORIGINAL graph
16 order = []
17 
18 for start in range(1, n + 1):
19 if visited[start]:
20 continue
21 visited[start] = True
22 # iterative DFS: no recursion limit on a long chain
23 stack = [(start, iter(adj[start]))]
24 while stack:
25 node, it = stack[-1]
26 advanced = False
27 for nxt in it:
28 if not visited[nxt]:
29 visited[nxt] = True
30 stack.append((nxt, iter(adj[nxt])))
31 advanced = True
32 break
33 if not advanced:
34 # no unvisited neighbor left: node is finished
35 order.append(node)
36 stack.pop()
37 
38 comp = [-1] * (n + 1)
39 comp_id = 0
40 # decreasing finish time: the other trap
41 for node in reversed(order):
42 if comp[node] != -1:
43 continue
44 comp[node] = comp_id
45 stack = [node]
46 while stack:
47 u = stack.pop()
48 for v in radj[u]: # walk the TRANSPOSE graph: the trap
49 if comp[v] == -1:
50 comp[v] = comp_id
51 stack.append(v)
52 comp_id += 1
53 
54 return comp[1:]
Invariant: After the second pass, comp[u] == comp[v] exactly when u and v can reach each other; every edge between two different components in edges runs from a lower comp id to a higher one, never the other way.

Two passes fix it: run DFS on adj to record each node's finish order in order, then re-run DFS on the transpose radj, starting from unvisited nodes in decreasing finish order, and every node reached before the next unvisited start belongs to the same comp_id. This works because of one fact about finish times: if any edge leads from one group to a different group, some node of the first group finishes after every node of the second. So the unlabeled node that finishes last lies in a group that no other unlabeled group leads into, and the walk on radj, which follows edges backwards, reaches exactly the nodes that can reach it in the original graph: its own group, and nothing from a group still waiting for its turn. The trap is walking adj again in the second pass instead of radj: without the transpose, the second DFS just retraces the first pass's reachability outward, and everything downstream of a group -- not just the group's own members -- gets folded into the same fake comp_id.

See it run

Kosaraju's two passes, step by step

order []

order []

order [1]

order [1]

order [1, 2]

order [1, 2]

order [1, 2]

order [1, 2]

order [1, 2, 5]

order [1, 2, 5, 4]

order [1, 2, 5, 4, 3]

order [1, 2, 5, 4, 3]

order [1, 2, 5, 4, 3]

order [1, 2, 5, 4, 3]

order [1, 2, 5, 4, 3, 8]

order [1, 2, 5, 4, 3, 8, 7]

order [1, 2, 5, 4, 3, 8, 7, 6]

comp [-1, -1, -1, -1, -1, -1, -1, -1]

comp [-1, -1, -1, -1, -1, -1, -1, -1]

comp [-1, -1, -1, -1, -1, 0, 0, 0]

comp [-1, -1, -1, -1, -1, 0, 0, 0]

comp [-1, -1, 1, 1, 1, 0, 0, 0]

comp [-1, -1, 1, 1, 1, 0, 0, 0]

comp [-1, 2, 1, 1, 1, 0, 0, 0]

comp [-1, 2, 1, 1, 1, 0, 0, 0]

comp [3, 2, 1, 1, 1, 0, 0, 0]

comp [3, 2, 1, 1, 1, 0, 0, 0] · answer [3, 2, 1, 1, 1, 0, 0, 0]

Each circle is a node and each arrow is an edge. Pass 2 draws every arrow reversed. The rows under the graph show stack and order in pass 1, and order (last node first) and comp in pass 2.

  • now the node the search is working on, or the edge just followed
  • stack a node on the current search path
  • done a finished node in pass 1, or a node that has a comp_id in pass 2
  • group a node reached by the pass 2 search that is running

Pass 1 builds order, the nodes in the order they finish. The search follows edges forwards, and a node finishes when it has no unvisited neighbour left.

Node 1 is not visited, so a new search starts there. visited[1] is set to True and node 1 goes on stack.

Node 1 has no unvisited neighbour left, so it finishes. Add node 1 to order.

Node 2 is not visited, so a new search starts there. visited[2] is set to True and node 2 goes on stack.

Node 2 has no unvisited neighbour left, so it finishes. Add node 2 to order.

Node 3 is not visited, so a new search starts there. visited[3] is set to True and node 3 goes on stack.

Node 3 has an unvisited neighbour, node 4. Follow the edge 3 to 4, and put node 4 on stack.

Node 4 has an unvisited neighbour, node 5. Follow the edge 4 to 5, and put node 5 on stack.

Node 5 has no unvisited neighbour left, so it finishes. Add node 5 to order.

Node 4 has no unvisited neighbour left, so it finishes. Add node 4 to order.

Node 3 has no unvisited neighbour left, so it finishes. Add node 3 to order.

Node 6 is not visited, so a new search starts there. visited[6] is set to True and node 6 goes on stack.

Node 6 has an unvisited neighbour, node 7. Follow the edge 6 to 7, and put node 7 on stack.

Node 7 has an unvisited neighbour, node 8. Follow the edge 7 to 8, and put node 8 on stack.

Node 8 has no unvisited neighbour left, so it finishes. Add node 8 to order.

Node 7 has no unvisited neighbour left, so it finishes. Add node 7 to order.

Node 6 has no unvisited neighbour left, so it finishes. Add node 6 to order.

Pass 1 is done, and order is [1, 2, 5, 4, 3, 8, 7, 6]. Pass 2 reverses every edge, then reads order from the last node to the first.

Node 6 is the last node of order that has no comp_id yet. Start a search on the reversed graph from node 6, with comp_id 0.

The search reached 6, 7 and 8, so they all get comp_id 0.

Node 3 is the last node of order that has no comp_id yet. Start a search on the reversed graph from node 3, with comp_id 1.

The search reached 3, 4 and 5, so they all get comp_id 1. It also met 8, which already has a comp_id, so it did not go there.

Node 2 is the last node of order that has no comp_id yet. Start a search on the reversed graph from node 2, with comp_id 2.

The search reached only node 2, so it gets comp_id 2. It also met 5, which already has a comp_id, so it did not go there.

Node 1 is the last node of order that has no comp_id yet. Start a search on the reversed graph from node 1, with comp_id 3.

The search reached only node 1, so it gets comp_id 3. It also met 2, which already has a comp_id, so it did not go there.

Every node has a comp_id. comp is [3, 2, 1, 1, 1, 0, 0, 0], so there are 4 strongly connected groups.

12345678
stack
empty
order
empty
comp
empty
1 / 27
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Walking adj again in the second pass instead of radj

    Mistake
    for v in adj[u]:
    Fix
    for v in radj[u]:

    What happens: The second pass must reach only the unlabeled nodes that can reach node. radj follows every edge backwards, so it finds exactly those. adj follows the edges forwards. It reaches every node downstream of node, even one that cannot come back. Those nodes get the same comp_id by mistake.

    Example: n = 3, edges = [[1, 2], [2, 1], [2, 3]]
    Mistake: returns [0, 0, 0]Fix: returns [0, 0, 1]
  2. 2

    Gotcha 2: Taking the finish order forwards instead of reversed(order)

    Mistake
    for node in order:
    Fix
    for node in reversed(order):

    What happens: The node that finishes last lies in a group that no other unlabeled group leads into. The second pass must start there. The node that finishes first can lie in a group that other groups lead into. The walk on radj from it then pulls those groups in too.

    Example: n = 3, edges = [[1, 2], [2, 1], [2, 3]]
    Mistake: returns [0, 0, 0]Fix: returns [0, 0, 1]
  3. 3

    Gotcha 3: Writing the first pass as a recursive dfs

    Mistake
    def dfs(u):
        visited[u] = True
        for v in adj[u]:
            if not visited[v]:
                dfs(v)
        order.append(u)
    Fix
    stack = [(start, iter(adj[start]))]

    What happens: A recursive dfs makes one nested call per node on the current path. Planets and Kingdoms allows 105 planets in one teleporter chain. Python's default recursion limit is 1000. A chain of 2000 planets already raises a RecursionError. The blueprint keeps its path in stack, which has no such limit.

    Example: n = 2000 planets in one chain: 1 -> 2 -> ... -> 2000
    Mistake: raises RecursionErrorFix: returns 2000
  4. 4

    Gotcha 4: Appending a node to order when it is first visited

    Mistake
    visited[nxt] = True
    order.append(nxt)
    Fix
    if not advanced:
        order.append(node)

    What happens: The ordering argument holds only for finish order. A node finishes when it has no unvisited neighbour left. Appending on the first visit records discovery order instead. The second pass can then start in the wrong group and pull other groups into it.

    Example: n = 3, edges = [[1, 2], [2, 1], [1, 3]]
    Mistake: returns [0, 0, 0]Fix: returns [0, 0, 1]
Not this one if…
  • The graph is undirected and you must find single links whose loss splits it, as in Critical Connections in a Network (LC 1192). See Tarjan's Bridges
  • The graph is undirected, as in Redundant Connection (LC 684), so reachability is already symmetric and a union-find forest tracks components in near O(1) per edge; Kosaraju's two directed passes are only needed when edges are one-way. See Union-Find

Deep Dive

Why the last node to finish is a safe place to start

A strongly connected component, or group, is a set of nodes in a directed graph where each node can reach every other node of the set. Two nodes are in one group exactly when each can reach the other.

If you collapse each group into one point, the groups form a graph with no cycle. Every edge between two groups goes one way. A group that no edge enters is a source group. A group that no edge leaves is a sink group.

Pass 1 runs a DFS (depth-first search) on adj and adds a node to order when it finishes. One fact makes this useful. If an edge leads from group A to a different group B, then the node of A that finishes last finishes after every node of B.

The reason has two cases. If the search enters A first, it reaches all of B before the first node of A finishes. If the search enters B first, it cannot reach A, because that would make A and B one group. So B is completely finished before A is entered. In both cases A finishes later.

So the node at the end of order is in a source group, and no other group leads into it. Pass 2 walks radj, the graph with every edge reversed, from that node. Reversed edges lead from a node to the nodes that can reach it, so the walk finds this group and nothing else.

Example: in the graph 1 -> 2, 2 -> 1, 2 -> 3, the groups are {1, 2} and {3}. The node 3 finishes first. The last node in order is in {1, 2}, and the reversed walk from it reaches only node 1 and node 2.

One trace: eight planets, four groups

The board uses 8 planets, as in Planets and Kingdoms (CSES 1683). The groups are {6, 7, 8}, {3, 4, 5}, {2} and {1}. The edges are 6 -> 7, 7 -> 8, 8 -> 6, then 8 -> 3, then 3 -> 4, 4 -> 5, 5 -> 3, then 5 -> 2, then 2 -> 1.

Pass 1 starts a search at 1, then at 2, then at 3, then at 6. The start loop runs from 1 to n, so it starts a new search at each node that is not visited yet. The finish order is order = [1, 2, 5, 4, 3, 8, 7, 6].

Pass 2 reads order from the end. The first node is 6. The search on radj reaches 8 and 7, because they can reach 6. The group {6, 7, 8} gets comp_id 0.

The next node with no group is 3. In radj[3], node 8 already has a comp_id, so the search does not go there. It reaches 5 and 4. The group {3, 4, 5} gets comp_id 1. Then node 2 gets 2, and node 1 gets 3.

The answer is comp = [3, 2, 1, 1, 1, 0, 0, 0] for planets 1 to 8. Every edge between two groups goes from a lower comp_id to a higher one. For example, 8 -> 3 goes from 0 to 1.

The traps: forward edges, the wrong order, and the wrong finish time

The second pass must walk radj. If it walks adj, the search from node 6 follows every edge forwards and reaches every planet downstream. On the board graph the result is comp = [0, 0, 0, 0, 0, 0, 0, 0]: one group, which is wrong.

The second pass must read order from the end, with reversed(order). If it reads order from the start, the first node is 1, which is in a group that other groups lead into. On the board graph, this also gives eight zeros.

A node must be added to order when it finishes, which is when it has no unvisited neighbour left. If you add it when it is first visited, order records discovery order, and the finish-time fact no longer applies.

Example: for n = 3 and edges = [[1, 2], [2, 1], [2, 3]], the correct answer is [0, 0, 1]. Walking adj in pass 2 gives [0, 0, 0], and so does reading order forwards.

The first pass is iterative, with stack holding (node, iterator). Planets and Kingdoms allows 10^5 planets in one chain. A recursive dfs would nest 10^5 calls and pass Python's recursion limit.

The cost, and how it differs from the neighbours

Pass 1 reads every edge of adj once, and pass 2 reads every edge of radj once. The time is O(V + E). adj, radj, visited, order, comp and stack hold one entry per node or per edge, so the space is O(V + E).

On the board graph, 8 nodes and 9 edges give 9 forward entries and 9 reversed entries, 18 in total.

The ids come out in topological order of the groups, because pass 2 starts in a source group and later passes start in groups it leads to. This is why comp can also order the groups.

If the graph is undirected, reachability is already symmetric, so union-find or a plain search finds the groups, as in Redundant Connection (LC 684) or Number of Provinces (LC 547). If you need the edges whose removal splits an undirected graph, use Critical Connections in a Network (LC 1192). Kosaraju is needed only when the edges have a direction.

Tarjan's algorithm finds the same groups in one pass with disc and low. Kosaraju uses two passes, and its reason is easier to state, so many interviews prefer it.

60-Second Pitch

"Two nodes are in one group exactly when each can reach the other, so I need strongly connected components. I use Kosaraju's algorithm with two passes. Pass 1 is a DFS on the original graph. I add a node to order when it finishes. If an edge leads from one group to another, the first group finishes later, so the last node in order is in a group that nothing else leads into. Pass 2 reads order from the end and searches the reversed graph from each node that has no group yet. That search reaches exactly the nodes that can reach the start, so each search is one group, and I give it the next comp_id. Each edge is read twice in total, so the time is O(V + E) and the space is O(V + E). Both passes use an explicit stack."

Common Questions

Why does the order of the second pass matter?

The last node in order is in a group that no other unlabeled group leads into. A search on radj from it stays inside its own group. If you start from another node, the search can add a group that should be separate, because it follows an edge from a group that leads into the start group.

Why do you reverse the edges in the second pass?

Reversed edges lead from a node to the nodes that can reach it. A search from the start node on radj therefore finds the nodes that can reach it. The nodes that are still unlabeled are exactly its group. A node of another group that can reach the start belongs to a group that leads into the start group, and the finish-time fact says that group was labeled earlier.

Why does a finish time decide the order and not a discovery time?

The fact that makes the algorithm correct is about the last finish time in each group. If an edge leads from group A to group B, the last finish in A is later than every finish in B. Discovery order does not have this property, so using it can start the second pass in the wrong group.

What are the time and space complexity?

O(V + E) time and O(V + E) space. Pass 1 reads each edge of adj once, and pass 2 reads each edge of radj once. Both passes use an explicit stack.

How is Kosaraju different from Tarjan's algorithm?

Both find the same groups in O(V + E). Kosaraju uses two passes and a reversed graph. Tarjan uses one pass and keeps disc and low for each node, the same two arrays as in the bridge search. Tarjan needs no reversed graph. Kosaraju's argument is shorter to explain.

SWE Reasoning

Spot it

  • The graph is directed, and the question asks which nodes can reach each other.
  • The words are mutual reachability, cycles of dependencies, groups, kingdoms, or "can get from any to any".
  • You need to shrink each cycle into one node and then work on the graph of groups, which has no cycle.
  • The input is large, up to 10^5 nodes, so the search must be iterative and O(V + E).
  • A single search from one node cannot answer it, because reaching a node does not mean the node can come back.

The invariant, step by step

  1. adj holds the edges forwards, and radj holds the same edges reversed.
  2. Pass 1 starts a search at every unvisited node. A node goes into order when it has no unvisited neighbour left.
  3. Because of the finish-time fact, the last node in order is in a group that no other unlabeled group leads into.
  4. Pass 2 reads order from the end. A node with comp[node] == -1 starts a new search with a new comp_id.
  5. The search walks radj. It reaches only nodes that can reach the start node, and a node that is not labeled yet and can reach the start node is in the same group.
  6. Every node gets one comp_id. Two nodes share it exactly when each can reach the other, and edges between groups run from a lower id to a higher one.

Traps

  • Walking adj in pass 2. The search then reaches every node downstream, not only the group. Walk radj.
  • Reading order from the start. The first node in order can be in a group that other groups lead into. Use reversed(order).
  • Adding a node to order when it is first visited. That is discovery order. Add it when it has no unvisited neighbour left.
  • Recursive dfs in pass 1. A chain of 10^5 planets nests 10^5 calls. Python stops at 1000 by default. Keep the path in stack.
  • Using it on an undirected graph. Reachability is symmetric there, so a plain search or union-find is enough.

Practice

Canonical LeetCode:Planets and Kingdoms (CSES 1683)Flight Routes Check (CSES 1682)Longest Cycle in a Graph (LC 2360)
Theory Context•Graph Algorithms

Builds on Cycle Detection, which you haven’t finished yet.

P3HardBuilt on Flood Fill~6 min speedrun

Strongly Connected Components (Kosaraju / Tarjan)

Built on
Cycle DetectionRecord the finish order, then search the reversed graph in reverse finish order, and each search finds one group.
The problem

Planets and Kingdoms (CSES 1683)(opens the source problem)

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 which kingdom every planet belongs to: return a list whose i-th value is the kingdom label of planet i + 1. Labels run from 1 to the number of kingdoms k, so k is the largest label. Kingdoms may be labeled any way you like, as long as two planets share a label exactly when they share a kingdom.

Example 1
Input:n = 5, teleporters = [[1,2],[2,3],[3,1],[3,4],[4,5],[5,4]]
Output:[1, 1, 1, 2, 2]
Planets 1, 2 and 3 form a cycle, so each reaches the other two and back: one kingdom. Planets 4 and 5 form their own cycle, a second kingdom. A teleporter runs from 3 into 4, but nothing runs back from kingdom 2 into kingdom 1, so the two groups stay separate: k = 2. Any labeling of the same two groups, such as [2, 2, 2, 1, 1], is also accepted.
123450

Strongly Connected Components

graph ➔ finish order, then search the reversed graph ➔ one group per search
Spot it when: You must group the nodes of a directed graph so that two nodes share a group exactly when each can reach the other (mutual reachability, cycles of dependencies, one-way links).
Strongly Connected Components
Python
1from typing import List
2 
3def kosaraju_scc(n: int, edges: List[List[int]]) -> List[int]:
4 """Kosaraju's algorithm (the core of Planets and Kingdoms): each node's strongly
5 connected component id, 0-indexed, in topological order of the condensation.
6 Time: O(V + E) | Space: O(V + E)
7 """
8 adj = [[] for _ in range(n + 1)]
9 radj = [[] for _ in range(n + 1)]
10 for u, v in edges:
11 adj[u].append(v)
12 radj[v].append(u)
13 
14 visited = [False] * (n + 1)
15 # finish order, first pass, on the ORIGINAL graph
16 order = []
17 
18 for start in range(1, n + 1):
19 if visited[start]:
20 continue
21 visited[start] = True
22 # iterative DFS: no recursion limit on a long chain
23 stack = [(start, iter(adj[start]))]
24 while stack:
25 node, it = stack[-1]
26 advanced = False
27 for nxt in it:
28 if not visited[nxt]:
29 visited[nxt] = True
30 stack.append((nxt, iter(adj[nxt])))
31 advanced = True
32 break
33 if not advanced:
34 # no unvisited neighbor left: node is finished
35 order.append(node)
36 stack.pop()
37 
38 comp = [-1] * (n + 1)
39 comp_id = 0
40 # decreasing finish time: the other trap
41 for node in reversed(order):
42 if comp[node] != -1:
43 continue
44 comp[node] = comp_id
45 stack = [node]
46 while stack:
47 u = stack.pop()
48 for v in radj[u]: # walk the TRANSPOSE graph: the trap
49 if comp[v] == -1:
50 comp[v] = comp_id
51 stack.append(v)
52 comp_id += 1
53 
54 return comp[1:]
Invariant: After the second pass, comp[u] == comp[v] exactly when u and v can reach each other; every edge between two different components in edges runs from a lower comp id to a higher one, never the other way.

Two passes fix it: run DFS on adj to record each node's finish order in order, then re-run DFS on the transpose radj, starting from unvisited nodes in decreasing finish order, and every node reached before the next unvisited start belongs to the same comp_id. This works because of one fact about finish times: if any edge leads from one group to a different group, some node of the first group finishes after every node of the second. So the unlabeled node that finishes last lies in a group that no other unlabeled group leads into, and the walk on radj, which follows edges backwards, reaches exactly the nodes that can reach it in the original graph: its own group, and nothing from a group still waiting for its turn. The trap is walking adj again in the second pass instead of radj: without the transpose, the second DFS just retraces the first pass's reachability outward, and everything downstream of a group -- not just the group's own members -- gets folded into the same fake comp_id.

See it run

Kosaraju's two passes, step by step

order []

order []

order [1]

order [1]

order [1, 2]

order [1, 2]

order [1, 2]

order [1, 2]

order [1, 2, 5]

order [1, 2, 5, 4]

order [1, 2, 5, 4, 3]

order [1, 2, 5, 4, 3]

order [1, 2, 5, 4, 3]

order [1, 2, 5, 4, 3]

order [1, 2, 5, 4, 3, 8]

order [1, 2, 5, 4, 3, 8, 7]

order [1, 2, 5, 4, 3, 8, 7, 6]

comp [-1, -1, -1, -1, -1, -1, -1, -1]

comp [-1, -1, -1, -1, -1, -1, -1, -1]

comp [-1, -1, -1, -1, -1, 0, 0, 0]

comp [-1, -1, -1, -1, -1, 0, 0, 0]

comp [-1, -1, 1, 1, 1, 0, 0, 0]

comp [-1, -1, 1, 1, 1, 0, 0, 0]

comp [-1, 2, 1, 1, 1, 0, 0, 0]

comp [-1, 2, 1, 1, 1, 0, 0, 0]

comp [3, 2, 1, 1, 1, 0, 0, 0]

comp [3, 2, 1, 1, 1, 0, 0, 0] · answer [3, 2, 1, 1, 1, 0, 0, 0]

Each circle is a node and each arrow is an edge. Pass 2 draws every arrow reversed. The rows under the graph show stack and order in pass 1, and order (last node first) and comp in pass 2.

  • now the node the search is working on, or the edge just followed
  • stack a node on the current search path
  • done a finished node in pass 1, or a node that has a comp_id in pass 2
  • group a node reached by the pass 2 search that is running

Pass 1 builds order, the nodes in the order they finish. The search follows edges forwards, and a node finishes when it has no unvisited neighbour left.

Node 1 is not visited, so a new search starts there. visited[1] is set to True and node 1 goes on stack.

Node 1 has no unvisited neighbour left, so it finishes. Add node 1 to order.

Node 2 is not visited, so a new search starts there. visited[2] is set to True and node 2 goes on stack.

Node 2 has no unvisited neighbour left, so it finishes. Add node 2 to order.

Node 3 is not visited, so a new search starts there. visited[3] is set to True and node 3 goes on stack.

Node 3 has an unvisited neighbour, node 4. Follow the edge 3 to 4, and put node 4 on stack.

Node 4 has an unvisited neighbour, node 5. Follow the edge 4 to 5, and put node 5 on stack.

Node 5 has no unvisited neighbour left, so it finishes. Add node 5 to order.

Node 4 has no unvisited neighbour left, so it finishes. Add node 4 to order.

Node 3 has no unvisited neighbour left, so it finishes. Add node 3 to order.

Node 6 is not visited, so a new search starts there. visited[6] is set to True and node 6 goes on stack.

Node 6 has an unvisited neighbour, node 7. Follow the edge 6 to 7, and put node 7 on stack.

Node 7 has an unvisited neighbour, node 8. Follow the edge 7 to 8, and put node 8 on stack.

Node 8 has no unvisited neighbour left, so it finishes. Add node 8 to order.

Node 7 has no unvisited neighbour left, so it finishes. Add node 7 to order.

Node 6 has no unvisited neighbour left, so it finishes. Add node 6 to order.

Pass 1 is done, and order is [1, 2, 5, 4, 3, 8, 7, 6]. Pass 2 reverses every edge, then reads order from the last node to the first.

Node 6 is the last node of order that has no comp_id yet. Start a search on the reversed graph from node 6, with comp_id 0.

The search reached 6, 7 and 8, so they all get comp_id 0.

Node 3 is the last node of order that has no comp_id yet. Start a search on the reversed graph from node 3, with comp_id 1.

The search reached 3, 4 and 5, so they all get comp_id 1. It also met 8, which already has a comp_id, so it did not go there.

Node 2 is the last node of order that has no comp_id yet. Start a search on the reversed graph from node 2, with comp_id 2.

The search reached only node 2, so it gets comp_id 2. It also met 5, which already has a comp_id, so it did not go there.

Node 1 is the last node of order that has no comp_id yet. Start a search on the reversed graph from node 1, with comp_id 3.

The search reached only node 1, so it gets comp_id 3. It also met 2, which already has a comp_id, so it did not go there.

Every node has a comp_id. comp is [3, 2, 1, 1, 1, 0, 0, 0], so there are 4 strongly connected groups.

12345678
stack
empty
order
empty
comp
empty
1 / 27
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Walking adj again in the second pass instead of radj

    Mistake
    for v in adj[u]:
    Fix
    for v in radj[u]:

    What happens: The second pass must reach only the unlabeled nodes that can reach node. radj follows every edge backwards, so it finds exactly those. adj follows the edges forwards. It reaches every node downstream of node, even one that cannot come back. Those nodes get the same comp_id by mistake.

    Example: n = 3, edges = [[1, 2], [2, 1], [2, 3]]
    Mistake: returns [0, 0, 0]Fix: returns [0, 0, 1]
  2. 2

    Gotcha 2: Taking the finish order forwards instead of reversed(order)

    Mistake
    for node in order:
    Fix
    for node in reversed(order):

    What happens: The node that finishes last lies in a group that no other unlabeled group leads into. The second pass must start there. The node that finishes first can lie in a group that other groups lead into. The walk on radj from it then pulls those groups in too.

    Example: n = 3, edges = [[1, 2], [2, 1], [2, 3]]
    Mistake: returns [0, 0, 0]Fix: returns [0, 0, 1]
  3. 3

    Gotcha 3: Writing the first pass as a recursive dfs

    Mistake
    def dfs(u):
        visited[u] = True
        for v in adj[u]:
            if not visited[v]:
                dfs(v)
        order.append(u)
    Fix
    stack = [(start, iter(adj[start]))]

    What happens: A recursive dfs makes one nested call per node on the current path. Planets and Kingdoms allows 105 planets in one teleporter chain. Python's default recursion limit is 1000. A chain of 2000 planets already raises a RecursionError. The blueprint keeps its path in stack, which has no such limit.

    Example: n = 2000 planets in one chain: 1 -> 2 -> ... -> 2000
    Mistake: raises RecursionErrorFix: returns 2000
  4. 4

    Gotcha 4: Appending a node to order when it is first visited

    Mistake
    visited[nxt] = True
    order.append(nxt)
    Fix
    if not advanced:
        order.append(node)

    What happens: The ordering argument holds only for finish order. A node finishes when it has no unvisited neighbour left. Appending on the first visit records discovery order instead. The second pass can then start in the wrong group and pull other groups into it.

    Example: n = 3, edges = [[1, 2], [2, 1], [1, 3]]
    Mistake: returns [0, 0, 0]Fix: returns [0, 0, 1]
Not this one if…
  • The graph is undirected and you must find single links whose loss splits it, as in Critical Connections in a Network (LC 1192). See Tarjan's Bridges
  • The graph is undirected, as in Redundant Connection (LC 684), so reachability is already symmetric and a union-find forest tracks components in near O(1) per edge; Kosaraju's two directed passes are only needed when edges are one-way. See Union-Find

Deep Dive

Why the last node to finish is a safe place to start

A strongly connected component, or group, is a set of nodes in a directed graph where each node can reach every other node of the set. Two nodes are in one group exactly when each can reach the other.

If you collapse each group into one point, the groups form a graph with no cycle. Every edge between two groups goes one way. A group that no edge enters is a source group. A group that no edge leaves is a sink group.

Pass 1 runs a DFS (depth-first search) on adj and adds a node to order when it finishes. One fact makes this useful. If an edge leads from group A to a different group B, then the node of A that finishes last finishes after every node of B.

The reason has two cases. If the search enters A first, it reaches all of B before the first node of A finishes. If the search enters B first, it cannot reach A, because that would make A and B one group. So B is completely finished before A is entered. In both cases A finishes later.

So the node at the end of order is in a source group, and no other group leads into it. Pass 2 walks radj, the graph with every edge reversed, from that node. Reversed edges lead from a node to the nodes that can reach it, so the walk finds this group and nothing else.

Example: in the graph 1 -> 2, 2 -> 1, 2 -> 3, the groups are {1, 2} and {3}. The node 3 finishes first. The last node in order is in {1, 2}, and the reversed walk from it reaches only node 1 and node 2.

One trace: eight planets, four groups

The board uses 8 planets, as in Planets and Kingdoms (CSES 1683). The groups are {6, 7, 8}, {3, 4, 5}, {2} and {1}. The edges are 6 -> 7, 7 -> 8, 8 -> 6, then 8 -> 3, then 3 -> 4, 4 -> 5, 5 -> 3, then 5 -> 2, then 2 -> 1.

Pass 1 starts a search at 1, then at 2, then at 3, then at 6. The start loop runs from 1 to n, so it starts a new search at each node that is not visited yet. The finish order is order = [1, 2, 5, 4, 3, 8, 7, 6].

Pass 2 reads order from the end. The first node is 6. The search on radj reaches 8 and 7, because they can reach 6. The group {6, 7, 8} gets comp_id 0.

The next node with no group is 3. In radj[3], node 8 already has a comp_id, so the search does not go there. It reaches 5 and 4. The group {3, 4, 5} gets comp_id 1. Then node 2 gets 2, and node 1 gets 3.

The answer is comp = [3, 2, 1, 1, 1, 0, 0, 0] for planets 1 to 8. Every edge between two groups goes from a lower comp_id to a higher one. For example, 8 -> 3 goes from 0 to 1.

The traps: forward edges, the wrong order, and the wrong finish time

The second pass must walk radj. If it walks adj, the search from node 6 follows every edge forwards and reaches every planet downstream. On the board graph the result is comp = [0, 0, 0, 0, 0, 0, 0, 0]: one group, which is wrong.

The second pass must read order from the end, with reversed(order). If it reads order from the start, the first node is 1, which is in a group that other groups lead into. On the board graph, this also gives eight zeros.

A node must be added to order when it finishes, which is when it has no unvisited neighbour left. If you add it when it is first visited, order records discovery order, and the finish-time fact no longer applies.

Example: for n = 3 and edges = [[1, 2], [2, 1], [2, 3]], the correct answer is [0, 0, 1]. Walking adj in pass 2 gives [0, 0, 0], and so does reading order forwards.

The first pass is iterative, with stack holding (node, iterator). Planets and Kingdoms allows 10^5 planets in one chain. A recursive dfs would nest 10^5 calls and pass Python's recursion limit.

The cost, and how it differs from the neighbours

Pass 1 reads every edge of adj once, and pass 2 reads every edge of radj once. The time is O(V + E). adj, radj, visited, order, comp and stack hold one entry per node or per edge, so the space is O(V + E).

On the board graph, 8 nodes and 9 edges give 9 forward entries and 9 reversed entries, 18 in total.

The ids come out in topological order of the groups, because pass 2 starts in a source group and later passes start in groups it leads to. This is why comp can also order the groups.

If the graph is undirected, reachability is already symmetric, so union-find or a plain search finds the groups, as in Redundant Connection (LC 684) or Number of Provinces (LC 547). If you need the edges whose removal splits an undirected graph, use Critical Connections in a Network (LC 1192). Kosaraju is needed only when the edges have a direction.

Tarjan's algorithm finds the same groups in one pass with disc and low. Kosaraju uses two passes, and its reason is easier to state, so many interviews prefer it.

60-Second Pitch

"Two nodes are in one group exactly when each can reach the other, so I need strongly connected components. I use Kosaraju's algorithm with two passes. Pass 1 is a DFS on the original graph. I add a node to order when it finishes. If an edge leads from one group to another, the first group finishes later, so the last node in order is in a group that nothing else leads into. Pass 2 reads order from the end and searches the reversed graph from each node that has no group yet. That search reaches exactly the nodes that can reach the start, so each search is one group, and I give it the next comp_id. Each edge is read twice in total, so the time is O(V + E) and the space is O(V + E). Both passes use an explicit stack."

Common Questions

Why does the order of the second pass matter?

The last node in order is in a group that no other unlabeled group leads into. A search on radj from it stays inside its own group. If you start from another node, the search can add a group that should be separate, because it follows an edge from a group that leads into the start group.

Why do you reverse the edges in the second pass?

Reversed edges lead from a node to the nodes that can reach it. A search from the start node on radj therefore finds the nodes that can reach it. The nodes that are still unlabeled are exactly its group. A node of another group that can reach the start belongs to a group that leads into the start group, and the finish-time fact says that group was labeled earlier.

Why does a finish time decide the order and not a discovery time?

The fact that makes the algorithm correct is about the last finish time in each group. If an edge leads from group A to group B, the last finish in A is later than every finish in B. Discovery order does not have this property, so using it can start the second pass in the wrong group.

What are the time and space complexity?

O(V + E) time and O(V + E) space. Pass 1 reads each edge of adj once, and pass 2 reads each edge of radj once. Both passes use an explicit stack.

How is Kosaraju different from Tarjan's algorithm?

Both find the same groups in O(V + E). Kosaraju uses two passes and a reversed graph. Tarjan uses one pass and keeps disc and low for each node, the same two arrays as in the bridge search. Tarjan needs no reversed graph. Kosaraju's argument is shorter to explain.

SWE Reasoning

Spot it

  • The graph is directed, and the question asks which nodes can reach each other.
  • The words are mutual reachability, cycles of dependencies, groups, kingdoms, or "can get from any to any".
  • You need to shrink each cycle into one node and then work on the graph of groups, which has no cycle.
  • The input is large, up to 10^5 nodes, so the search must be iterative and O(V + E).
  • A single search from one node cannot answer it, because reaching a node does not mean the node can come back.

The invariant, step by step

  1. adj holds the edges forwards, and radj holds the same edges reversed.
  2. Pass 1 starts a search at every unvisited node. A node goes into order when it has no unvisited neighbour left.
  3. Because of the finish-time fact, the last node in order is in a group that no other unlabeled group leads into.
  4. Pass 2 reads order from the end. A node with comp[node] == -1 starts a new search with a new comp_id.
  5. The search walks radj. It reaches only nodes that can reach the start node, and a node that is not labeled yet and can reach the start node is in the same group.
  6. Every node gets one comp_id. Two nodes share it exactly when each can reach the other, and edges between groups run from a lower id to a higher one.

Traps

  • Walking adj in pass 2. The search then reaches every node downstream, not only the group. Walk radj.
  • Reading order from the start. The first node in order can be in a group that other groups lead into. Use reversed(order).
  • Adding a node to order when it is first visited. That is discovery order. Add it when it has no unvisited neighbour left.
  • Recursive dfs in pass 1. A chain of 10^5 planets nests 10^5 calls. Python stops at 1000 by default. Keep the path in stack.
  • Using it on an undirected graph. Reachability is symmetric there, so a plain search or union-find is enough.

Practice

Canonical LeetCode:Planets and Kingdoms (CSES 1683)Flight Routes Check (CSES 1682)Longest Cycle in a Graph (LC 2360)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•