Builds on Cycle Detection, which you haven’t finished yet.
Strongly Connected Components (Kosaraju / Tarjan)
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.
Strongly Connected Components
graph ➔ finish order, then search the reversed graph ➔ one group per search| 1 | from typing import List |
| 2 | |
| 3 | def 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:] |
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 []
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.
nowthe node the search is working on, or the edge just followedstacka node on the current search pathdonea finished node in pass 1, or a node that has a comp_id in pass 2groupa 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.
Gotcha 1 of 4
- The trap
Gotcha 1: Walking
adjagain in the second pass instead ofradjMistakefor v in adj[u]:Fixfor v in radj[u]:What happens: The second pass must reach only the unlabeled nodes that can reach
node.radjfollows every edge backwards, so it finds exactly those.adjfollows the edges forwards. It reaches every node downstream ofnode, even one that cannot come back. Those nodes get the samecomp_idby mistake.Example: n = 3, edges = [[1, 2], [2, 1], [2, 3]]Mistake: returns [0, 0, 0]Fix: returns [0, 0, 1]
- 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
orderis in a group that no other unlabeled group leads into. A search onradjfrom 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
radjtherefore 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 andO(V + E)space. Pass 1 reads each edge ofadjonce, and pass 2 reads each edge ofradjonce. Both passes use an explicitstack.- 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 keepsdiscandlowfor 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
adjholds the edges forwards, andradjholds the same edges reversed.- Pass 1 starts a search at every unvisited node. A node goes into
orderwhen it has no unvisited neighbour left. - Because of the finish-time fact, the last node in
orderis in a group that no other unlabeled group leads into. - Pass 2 reads
orderfrom the end. A node withcomp[node] == -1starts a new search with a newcomp_id. - 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. - 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
adjin pass 2. The search then reaches every node downstream, not only the group. Walkradj. - Reading
orderfrom the start. The first node inordercan be in a group that other groups lead into. Usereversed(order). - Adding a node to
orderwhen it is first visited. That is discovery order. Add it when it has no unvisited neighbour left. - Recursive
dfsin pass 1. A chain of 10^5 planets nests 10^5 calls. Python stops at 1000 by default. Keep the path instack. - Using it on an undirected graph. Reachability is symmetric there, so a plain search or union-find is enough.
Practice
Builds on Cycle Detection, which you haven’t finished yet.
Strongly Connected Components (Kosaraju / Tarjan)
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.
Strongly Connected Components
graph ➔ finish order, then search the reversed graph ➔ one group per search| 1 | from typing import List |
| 2 | |
| 3 | def 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:] |
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 []
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.
nowthe node the search is working on, or the edge just followedstacka node on the current search pathdonea finished node in pass 1, or a node that has a comp_id in pass 2groupa 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.
Gotcha 1 of 4
- The trap
Gotcha 1: Walking
adjagain in the second pass instead ofradjMistakefor v in adj[u]:Fixfor v in radj[u]:What happens: The second pass must reach only the unlabeled nodes that can reach
node.radjfollows every edge backwards, so it finds exactly those.adjfollows the edges forwards. It reaches every node downstream ofnode, even one that cannot come back. Those nodes get the samecomp_idby mistake.Example: n = 3, edges = [[1, 2], [2, 1], [2, 3]]Mistake: returns [0, 0, 0]Fix: returns [0, 0, 1]
- 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
orderis in a group that no other unlabeled group leads into. A search onradjfrom 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
radjtherefore 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 andO(V + E)space. Pass 1 reads each edge ofadjonce, and pass 2 reads each edge ofradjonce. Both passes use an explicitstack.- 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 keepsdiscandlowfor 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
adjholds the edges forwards, andradjholds the same edges reversed.- Pass 1 starts a search at every unvisited node. A node goes into
orderwhen it has no unvisited neighbour left. - Because of the finish-time fact, the last node in
orderis in a group that no other unlabeled group leads into. - Pass 2 reads
orderfrom the end. A node withcomp[node] == -1starts a new search with a newcomp_id. - 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. - 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
adjin pass 2. The search then reaches every node downstream, not only the group. Walkradj. - Reading
orderfrom the start. The first node inordercan be in a group that other groups lead into. Usereversed(order). - Adding a node to
orderwhen it is first visited. That is discovery order. Add it when it has no unvisited neighbour left. - Recursive
dfsin pass 1. A chain of 10^5 planets nests 10^5 calls. Python stops at 1000 by default. Keep the path instack. - Using it on an undirected graph. Reachability is symmetric there, so a plain search or union-find is enough.