Builds on Tarjan's Bridges, which you haven’t finished yet.
Tarjan's Strongly Connected Components (One Pass with Low-Link)
A country has n cities, numbered 1 to n, and m one-way flights. A flight [a, b] takes you from city a to city b, and it does not take you back. A route is any sequence of flights, one after another.
Decide whether every city can reach every other city by some route. If it can, return ["YES"]. If it cannot, return ["NO", a, b], where a and b are two cities and no route leads from a to b. If several such pairs exist, any one of them is accepted.
Tarjan SCC
directed graph ➔ one DFS with disc, low and a stack of waiting nodes ➔ a group popped when low[node] == disc[node]| 1 | from typing import List |
| 2 | |
| 3 | def tarjan_scc(n: int, edges: List[List[int]]) -> List[int]: |
| 4 | """Tarjan's algorithm (the core of Flight Routes Check): each node's strongly |
| 5 | connected component id, 0-indexed, in the order the groups are completed. |
| 6 | Time: O(V + E) | Space: O(V + E) |
| 7 | """ |
| 8 | adj = [[] for _ in range(n + 1)] |
| 9 | for u, v in edges: |
| 10 | adj[u].append(v) |
| 11 | |
| 12 | # discovery time, counted from 1; 0 = not reached yet |
| 13 | disc = [0] * (n + 1) |
| 14 | # smallest disc this node's subtree reaches while the target still waits on |
| 15 | # group_stack |
| 16 | low = [0] * (n + 1) |
| 17 | # True while a node waits on group_stack for its group to be completed |
| 18 | on_stack = [False] * (n + 1) |
| 19 | # nodes reached but not yet assigned to a group |
| 20 | group_stack = [] |
| 21 | # group id of each node; -1 = none yet |
| 22 | comp = [-1] * (n + 1) |
| 23 | time = 0 |
| 24 | comp_id = 0 |
| 25 | |
| 26 | # 1. Start a Search From Every Unreached Node: |
| 27 | # One search reaches only the nodes its edges lead to, so each node that is still |
| 28 | # unreached starts its own search. |
| 29 | for start in range(1, n + 1): |
| 30 | if disc[start] != 0: |
| 31 | continue |
| 32 | time += 1 |
| 33 | disc[start] = low[start] = time |
| 34 | group_stack.append(start) |
| 35 | on_stack[start] = True |
| 36 | # iterative DFS: no recursion limit on a long chain |
| 37 | path = [(start, iter(adj[start]))] |
| 38 | while path: |
| 39 | node, it = path[-1] |
| 40 | advanced = False |
| 41 | for nxt in it: |
| 42 | # 2. Go Deeper Into an Unreached Neighbour: |
| 43 | # The new node gets the next disc, and it waits on group_stack until its |
| 44 | # group is complete. |
| 45 | if disc[nxt] == 0: |
| 46 | time += 1 |
| 47 | disc[nxt] = low[nxt] = time |
| 48 | group_stack.append(nxt) |
| 49 | on_stack[nxt] = True |
| 50 | path.append((nxt, iter(adj[nxt]))) |
| 51 | advanced = True |
| 52 | break |
| 53 | # 3. Count a Reached Neighbour Only While It Still Waits on group_stack: |
| 54 | # A waiting node belongs to a group that node can climb back into. A |
| 55 | # node that is not waiting |
| 56 | # already finished its own group, so its disc must not lower low[node]. |
| 57 | if on_stack[nxt]: |
| 58 | low[node] = min(low[node], disc[nxt]) |
| 59 | if advanced: |
| 60 | continue |
| 61 | path.pop() |
| 62 | # 4. Complete a Group When low Equals disc: |
| 63 | # Nothing below node climbs above it, so node is the first node of its group |
| 64 | # that the search reached. |
| 65 | # Every node above it on group_stack, down to node itself, belongs to that |
| 66 | # group. |
| 67 | if low[node] == disc[node]: |
| 68 | while True: |
| 69 | member = group_stack.pop() |
| 70 | on_stack[member] = False |
| 71 | comp[member] = comp_id |
| 72 | if member == node: |
| 73 | break |
| 74 | comp_id += 1 |
| 75 | # 5. Hand low Up to the Parent: |
| 76 | # The parent can climb as far as its child's subtree can, so it keeps the |
| 77 | # smaller value. |
| 78 | if path: |
| 79 | parent = path[-1][0] |
| 80 | low[parent] = min(low[parent], low[node]) |
| 81 | |
| 82 | return comp[1:] |
group_stack holds exactly the reached nodes whose group is not complete; low[node] is the smallest disc of a waiting node that node's subtree reaches, and node is the root of a group exactly when low[node] == disc[node].Give each node a number disc in the order the search reaches it, and push it on group_stack. Track low[node], the smallest disc of a waiting node that node's subtree reaches. When node has tried all of its neighbours and low[node] == disc[node], nothing below node can climb above it. So node is the root of one group: pop group_stack down to node. This is correct because node reaches every node above it on group_stack: the search reached them through node. None of them can reach a waiting node that is older than node. That would have lowered low[node]. So each of them climbs back to node, and the group reaches itself in both directions. The trap is a neighbour that was already reached. Lower low[node] with disc[nxt] only when on_stack[nxt] is True. A reached neighbour that is not on group_stack belongs to a group that is already complete. It has no path back to node. If you use it anyway, node is popped together with a group it does not belong to.
See it run
Tarjan's one search, step by step
group_stack [1]
Each circle is a city and each arrow is a flight. The rows under the graph show disc, low, group_stack and comp for every city, and the small number under a box is the city.
nowthe city the search is working on, or the flight just followedwaita city on group_stack: it was reached, but its group is not completegroupeda city whose group is complete and already has a comp_idbacka flight to a city that still waits, which lowers lowignoreda flight to a city whose group is complete, which is not used
Pass 1 starts a search at city 1. disc[1] and low[1] are 1, and city 1 goes on group_stack. A city on group_stack is waiting for its group to be complete.
Gotcha 1 of 4
- The trap
Gotcha 1: Lowering
low[node]with every reached neighbour, not only with a neighbour ongroup_stackMistakelow[node] = min(low[node], disc[nxt])Fixif on_stack[nxt]: low[node] = min(low[node], disc[nxt])What happens: A reached neighbour that is no longer on
group_stackbelongs to a group that is already complete. It cannot reachnode. If itsdiscstill lowerslow[node], thennodeis no longer the root of its own group. In this example, node 2 has no edge out, so it is popped as a group of its own first. Node 3 then sees node 2 withdisc[2] = 2, solow[3]becomes 2 and node 3 is not a root. Node 1 then pops nodes 3 and 1 together, although node 3 has no path to node 1.Example: n = 3, edges = [[1, 2], [1, 3], [3, 2]]Mistake: returns [1, 0, 1]Fix: returns [2, 0, 1]
- Both cards find the groups of a directed graph, for example the cycles of Longest Cycle in a Graph (LC 2360). Switch to Strongly Connected Components when the reversed graph is already stored, or when you want the groups from a source group to a sink group. Its second pass lists them in that order. This card lists them from a sink group to a source group. See Strongly Connected Components
- The graph is undirected and the question is which single link, when removed, splits it, as in Critical Connections in a Network (LC 1192). A link has no direction and there are no groups to pop, so no stack is needed: use the bridge test on the tree edge. See Tarjan's Bridges
- You only need to know whether the directed graph has a cycle, as in Course Schedule (LC 207), not which nodes sit on which cycle. The three colors answer that with no
lowvalues and no group stack. See Cycle Detection
Deep Dive
Why low[node] == disc[node] marks the first city of a group
A strongly connected 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.
Run one DFS (depth-first search) and give each node a number, disc, in the order the search reaches it. Push the node on group_stack. A node on group_stack is waiting: it was reached, but its group is not complete.
low[node] is the smallest disc of a waiting node that the subtree of node can reach. The search lowers it with an edge to a node that still waits, and with the low of a finished child.
When node has no edge left, compare low[node] with disc[node]. If they are equal, nothing in the subtree of node can climb to a waiting node older than node. So node is the first node of its group that the search reached.
Every node above node on group_stack was reached through node, so node reaches it. None of those nodes is a first node of its own group, so each one climbs to an older waiting node, and the climbs end at node. They reach node back. That makes them one group, and the search pops group_stack down to node.
Example: in 1 -> 2, 2 -> 1, 2 -> 3, the search goes 1, 2, 3. Node 3 has no edge left and low[3] = disc[3] = 3, so it forms the group {3}. Node 2 has the edge back to node 1, so low[2] = 1. Node 1 ends with low[1] = disc[1] = 1, and it pops {2, 1}.
One trace: seven cities, three groups
The board uses 7 cities. The groups are {1, 2, 3}, {4, 5, 6} and {7}. The flights are 1 -> 2, 2 -> 3, 3 -> 1, then 3 -> 4, then 4 -> 5, 5 -> 6, 6 -> 4, then 7 -> 4.
The search goes 1, 2, 3. The flight 3 -> 1 leads to city 1, which waits on group_stack, so low[3] becomes 1. Then the search goes 4, 5, 6, and the flight 6 -> 4 makes low[6] equal to 4.
City 6 finishes with low[6] = 4, which is not disc[6] = 6, so it stays on the stack and hands 4 to city 5. City 5 does the same for city 4. City 4 finishes with low[4] = disc[4] = 4, so group_stack is popped down to 4. The group {4, 5, 6} gets comp_id 0.
Cities 3 and 2 finish with low 1, and hand it up. City 1 finishes with low[1] = disc[1] = 1, so the stack is popped down to 1. The group {1, 2, 3} gets comp_id 1.
Then a new search starts at city 7. Its flight 7 -> 4 leads to city 4, which is already in group 0. Here on_stack[4] is False, so disc[4] = 4 is not used, and low[7] stays 7. City 7 forms the group {7} with comp_id 2.
There are 3 groups, so the answer is NO. Group 0 was completed first. No flight leaves it, so no route leads from city 6 to city 7. The answer is ["NO", 6, 7], and the final comp is [1, 1, 1, 0, 0, 0, 2].
The traps: the on_stack test, the flag, and passing low up
The first trap is the test if on_stack[nxt]:. A neighbour that was reached but is not on group_stack is in a group that is already complete. It cannot reach node, so its disc must not lower low[node].
On the board graph, leave the test out. City 7 sees city 4, and low[7] becomes 4. Now low[7] is not disc[7], so city 7 is never popped. The result is comp = [1, 1, 1, 0, 0, 0, -1], and city 7 has no group.
The smallest example is n = 2 and edges = [[2, 1]]. City 1 forms its own group first. City 2 then sees city 1 and takes low[2] = 1, so city 2 is never popped. Only one group is counted, and Flight Routes Check would answer ["YES"] where the right answer is ["NO", 1, 2].
The second trap is the flag. on_stack[node] must become False only when node is popped with its group, not when the search leaves node. A node can wait on group_stack long after the search has left it. For n = 3 and edges = [[1, 2], [2, 1], [1, 3], [3, 2]], an early clear gives [1, 1, 0], two groups. The right answer is [0, 0, 0].
The third trap is the last step of a finished node: low[parent] = min(low[parent], low[node]). On the board graph, without it the result is [3, 2, 2, 1, 0, 0, 4], five groups instead of three.
The search is iterative, with path holding (node, iterator). Flight Routes Check allows 10^5 cities in one chain. A recursive dfs would nest 10^5 calls. With Python's default limit, a chain of 2000 cities already raises RecursionError.
The cost, and how it differs from Kosaraju's algorithm
Each city is pushed on group_stack once and popped once. Each flight is read once by the iterator of its city. The time is O(V + E). adj, disc, low, on_stack, comp, group_stack and path hold one entry per city or per flight, so the space is O(V + E).
On the board graph, 7 cities and 8 flights give 7 pushes, 7 pops and 8 flight reads.
Strongly Connected Components (Kosaraju) finds the same groups with two searches and a reversed copy of the graph. The first search records the finish order. The second search reads that order from the end and walks the reversed graph.
Tarjan's algorithm needs one search over the original graph, and no reversed copy. It needs two extra arrays, low and on_stack, and a stack of waiting nodes. Both algorithms cost O(V + E).
The group ids come out in opposite orders. Kosaraju numbers a group that no edge enters first. Tarjan numbers a group that no edge leaves first. Every edge between two groups goes from a higher comp_id to a lower one in Tarjan's output.
The undirected cousin of this card is Tarjan's Bridges. It uses the same disc and low, but it needs no stack, because an undirected search has no edge between two different subtrees. If the graph is undirected and you only count pieces, a plain search or union-find is enough.
60-Second Pitch
"Every city must reach every other city, so I check whether the graph is one strongly connected group. I use Tarjan's algorithm, which is one depth-first search. I number each city with disc when I reach it, and I keep a stack of cities whose group is not complete. low is the smallest disc of a waiting city that a city's subtree reaches. A flight to a reached city lowers low only if that city is still on the stack. When a city finishes with low equal to disc, it is the first city of a group, so I pop the stack down to it. One group means YES. Otherwise the first group completed has no flight out, so I name a city from it and a city outside it. The search is iterative, and the time and space are both O(V + E)."
Common Questions
- Why must the test be
on_stack[nxt]and not onlydisc[nxt] != 0? A reached node that is not on
group_stackis in a group that is already complete. It cannot reach the current node, so it must not lowerlow. If it does, the current node is never popped. Forn = 2andedges = [[2, 1]], the group of city 2 is lost and Flight Routes Check answers YES instead of naming a pair.- Should
lowtakedisc[nxt]orlow[nxt]from a node that is still on the stack? Both give the same groups, and
disc[nxt]is the standard form. A test of both forms on 20000 random graphs of up to 8 nodes gave the samecompevery time.- How is Tarjan's algorithm different from Kosaraju's, and which would you pick?
Both find the same groups in
O(V + E). Kosaraju uses two searches and a reversed graph, and its reason is short to explain. Tarjan uses one search over the original graph, withdisc,lowand a stack. Pick Tarjan when the reversed graph is not stored, or when you want the groups from a sink group to a source group.- Why does the first group completed give a pair with no route?
A group is completed only when nothing in it climbs to an older waiting node, so every flight that leaves its nodes leads to a group that is already complete. The first group has none to lead to. So no flight leaves it, and no route leads from a city of that group to a city of any other group.
- What are the time and space complexity?
O(V + E)time andO(V + E)space. Each node is pushed and popped once, and each flight is read once. The search uses an explicitpath, so a long chain does not hit the recursion limit.
SWE Reasoning
Spot it
- The graph is directed, and the question asks which nodes can reach each other.
- The words are every city reaches every other city, mutual reachability, groups, kingdoms, or cycles of dependencies.
- The answer for a whole graph is YES when only one group exists, so the number of groups matters.
- The input is large, up to 10^5 nodes, so the search must be iterative and
O(V + E). - You want the groups from one search, without building a reversed copy of the graph.
The invariant, step by step
disc[u]is 0 until the search reachesu. The first time, it gets the next number fromtime,low[u]starts equal to it, andugoes ongroup_stackwithon_stack[u]True.- The search reads one flight at a time from the top of
path. A flight to a node withdisc[nxt] == 0goes deeper. - A flight to a node that is already reached lowers
low[node]only ifon_stack[nxt]is True. Otherwise the flight is ignored. - When a node has no flight left, it is popped from
path. Iflow[node] == disc[node],group_stackis popped down tonode, and each popped node getscomp_idandon_stackFalse. - If
low[node]is smaller thandisc[node], the node stays ongroup_stack. Itslowgoes to the parent withlow[parent] = min(low[parent], low[node]). - When every search is done, each node has one
comp_id. Two nodes share it exactly when each can reach the other, and every edge between two groups goes from a higher id to a lower one.
Traps
- Using
disc[nxt]for every reached node. A node outsidegroup_stackis in a complete group. Teston_stack[nxt]first. - Clearing
on_stack[node]when the search leavesnode. Clear it when the node is popped with its group. - Skipping
low[parent] = min(low[parent], low[node]). A back edge found deep in the search then never reaches the nodes between. - Recursive
dfs. A chain of 10^5 cities nests 10^5 calls. Python stops at 1000 by default. Keep the path inpath. - Naming the pair in the wrong direction. The first group completed has no flight leaving it, so it is the city of that group that cannot reach another group, not the other way round.
Practice
Builds on Tarjan's Bridges, which you haven’t finished yet.
Tarjan's Strongly Connected Components (One Pass with Low-Link)
A country has n cities, numbered 1 to n, and m one-way flights. A flight [a, b] takes you from city a to city b, and it does not take you back. A route is any sequence of flights, one after another.
Decide whether every city can reach every other city by some route. If it can, return ["YES"]. If it cannot, return ["NO", a, b], where a and b are two cities and no route leads from a to b. If several such pairs exist, any one of them is accepted.
Tarjan SCC
directed graph ➔ one DFS with disc, low and a stack of waiting nodes ➔ a group popped when low[node] == disc[node]| 1 | from typing import List |
| 2 | |
| 3 | def tarjan_scc(n: int, edges: List[List[int]]) -> List[int]: |
| 4 | """Tarjan's algorithm (the core of Flight Routes Check): each node's strongly |
| 5 | connected component id, 0-indexed, in the order the groups are completed. |
| 6 | Time: O(V + E) | Space: O(V + E) |
| 7 | """ |
| 8 | adj = [[] for _ in range(n + 1)] |
| 9 | for u, v in edges: |
| 10 | adj[u].append(v) |
| 11 | |
| 12 | # discovery time, counted from 1; 0 = not reached yet |
| 13 | disc = [0] * (n + 1) |
| 14 | # smallest disc this node's subtree reaches while the target still waits on |
| 15 | # group_stack |
| 16 | low = [0] * (n + 1) |
| 17 | # True while a node waits on group_stack for its group to be completed |
| 18 | on_stack = [False] * (n + 1) |
| 19 | # nodes reached but not yet assigned to a group |
| 20 | group_stack = [] |
| 21 | # group id of each node; -1 = none yet |
| 22 | comp = [-1] * (n + 1) |
| 23 | time = 0 |
| 24 | comp_id = 0 |
| 25 | |
| 26 | # 1. Start a Search From Every Unreached Node: |
| 27 | # One search reaches only the nodes its edges lead to, so each node that is still |
| 28 | # unreached starts its own search. |
| 29 | for start in range(1, n + 1): |
| 30 | if disc[start] != 0: |
| 31 | continue |
| 32 | time += 1 |
| 33 | disc[start] = low[start] = time |
| 34 | group_stack.append(start) |
| 35 | on_stack[start] = True |
| 36 | # iterative DFS: no recursion limit on a long chain |
| 37 | path = [(start, iter(adj[start]))] |
| 38 | while path: |
| 39 | node, it = path[-1] |
| 40 | advanced = False |
| 41 | for nxt in it: |
| 42 | # 2. Go Deeper Into an Unreached Neighbour: |
| 43 | # The new node gets the next disc, and it waits on group_stack until its |
| 44 | # group is complete. |
| 45 | if disc[nxt] == 0: |
| 46 | time += 1 |
| 47 | disc[nxt] = low[nxt] = time |
| 48 | group_stack.append(nxt) |
| 49 | on_stack[nxt] = True |
| 50 | path.append((nxt, iter(adj[nxt]))) |
| 51 | advanced = True |
| 52 | break |
| 53 | # 3. Count a Reached Neighbour Only While It Still Waits on group_stack: |
| 54 | # A waiting node belongs to a group that node can climb back into. A |
| 55 | # node that is not waiting |
| 56 | # already finished its own group, so its disc must not lower low[node]. |
| 57 | if on_stack[nxt]: |
| 58 | low[node] = min(low[node], disc[nxt]) |
| 59 | if advanced: |
| 60 | continue |
| 61 | path.pop() |
| 62 | # 4. Complete a Group When low Equals disc: |
| 63 | # Nothing below node climbs above it, so node is the first node of its group |
| 64 | # that the search reached. |
| 65 | # Every node above it on group_stack, down to node itself, belongs to that |
| 66 | # group. |
| 67 | if low[node] == disc[node]: |
| 68 | while True: |
| 69 | member = group_stack.pop() |
| 70 | on_stack[member] = False |
| 71 | comp[member] = comp_id |
| 72 | if member == node: |
| 73 | break |
| 74 | comp_id += 1 |
| 75 | # 5. Hand low Up to the Parent: |
| 76 | # The parent can climb as far as its child's subtree can, so it keeps the |
| 77 | # smaller value. |
| 78 | if path: |
| 79 | parent = path[-1][0] |
| 80 | low[parent] = min(low[parent], low[node]) |
| 81 | |
| 82 | return comp[1:] |
group_stack holds exactly the reached nodes whose group is not complete; low[node] is the smallest disc of a waiting node that node's subtree reaches, and node is the root of a group exactly when low[node] == disc[node].Give each node a number disc in the order the search reaches it, and push it on group_stack. Track low[node], the smallest disc of a waiting node that node's subtree reaches. When node has tried all of its neighbours and low[node] == disc[node], nothing below node can climb above it. So node is the root of one group: pop group_stack down to node. This is correct because node reaches every node above it on group_stack: the search reached them through node. None of them can reach a waiting node that is older than node. That would have lowered low[node]. So each of them climbs back to node, and the group reaches itself in both directions. The trap is a neighbour that was already reached. Lower low[node] with disc[nxt] only when on_stack[nxt] is True. A reached neighbour that is not on group_stack belongs to a group that is already complete. It has no path back to node. If you use it anyway, node is popped together with a group it does not belong to.
See it run
Tarjan's one search, step by step
group_stack [1]
Each circle is a city and each arrow is a flight. The rows under the graph show disc, low, group_stack and comp for every city, and the small number under a box is the city.
nowthe city the search is working on, or the flight just followedwaita city on group_stack: it was reached, but its group is not completegroupeda city whose group is complete and already has a comp_idbacka flight to a city that still waits, which lowers lowignoreda flight to a city whose group is complete, which is not used
Pass 1 starts a search at city 1. disc[1] and low[1] are 1, and city 1 goes on group_stack. A city on group_stack is waiting for its group to be complete.
Gotcha 1 of 4
- The trap
Gotcha 1: Lowering
low[node]with every reached neighbour, not only with a neighbour ongroup_stackMistakelow[node] = min(low[node], disc[nxt])Fixif on_stack[nxt]: low[node] = min(low[node], disc[nxt])What happens: A reached neighbour that is no longer on
group_stackbelongs to a group that is already complete. It cannot reachnode. If itsdiscstill lowerslow[node], thennodeis no longer the root of its own group. In this example, node 2 has no edge out, so it is popped as a group of its own first. Node 3 then sees node 2 withdisc[2] = 2, solow[3]becomes 2 and node 3 is not a root. Node 1 then pops nodes 3 and 1 together, although node 3 has no path to node 1.Example: n = 3, edges = [[1, 2], [1, 3], [3, 2]]Mistake: returns [1, 0, 1]Fix: returns [2, 0, 1]
- Both cards find the groups of a directed graph, for example the cycles of Longest Cycle in a Graph (LC 2360). Switch to Strongly Connected Components when the reversed graph is already stored, or when you want the groups from a source group to a sink group. Its second pass lists them in that order. This card lists them from a sink group to a source group. See Strongly Connected Components
- The graph is undirected and the question is which single link, when removed, splits it, as in Critical Connections in a Network (LC 1192). A link has no direction and there are no groups to pop, so no stack is needed: use the bridge test on the tree edge. See Tarjan's Bridges
- You only need to know whether the directed graph has a cycle, as in Course Schedule (LC 207), not which nodes sit on which cycle. The three colors answer that with no
lowvalues and no group stack. See Cycle Detection
Deep Dive
Why low[node] == disc[node] marks the first city of a group
A strongly connected 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.
Run one DFS (depth-first search) and give each node a number, disc, in the order the search reaches it. Push the node on group_stack. A node on group_stack is waiting: it was reached, but its group is not complete.
low[node] is the smallest disc of a waiting node that the subtree of node can reach. The search lowers it with an edge to a node that still waits, and with the low of a finished child.
When node has no edge left, compare low[node] with disc[node]. If they are equal, nothing in the subtree of node can climb to a waiting node older than node. So node is the first node of its group that the search reached.
Every node above node on group_stack was reached through node, so node reaches it. None of those nodes is a first node of its own group, so each one climbs to an older waiting node, and the climbs end at node. They reach node back. That makes them one group, and the search pops group_stack down to node.
Example: in 1 -> 2, 2 -> 1, 2 -> 3, the search goes 1, 2, 3. Node 3 has no edge left and low[3] = disc[3] = 3, so it forms the group {3}. Node 2 has the edge back to node 1, so low[2] = 1. Node 1 ends with low[1] = disc[1] = 1, and it pops {2, 1}.
One trace: seven cities, three groups
The board uses 7 cities. The groups are {1, 2, 3}, {4, 5, 6} and {7}. The flights are 1 -> 2, 2 -> 3, 3 -> 1, then 3 -> 4, then 4 -> 5, 5 -> 6, 6 -> 4, then 7 -> 4.
The search goes 1, 2, 3. The flight 3 -> 1 leads to city 1, which waits on group_stack, so low[3] becomes 1. Then the search goes 4, 5, 6, and the flight 6 -> 4 makes low[6] equal to 4.
City 6 finishes with low[6] = 4, which is not disc[6] = 6, so it stays on the stack and hands 4 to city 5. City 5 does the same for city 4. City 4 finishes with low[4] = disc[4] = 4, so group_stack is popped down to 4. The group {4, 5, 6} gets comp_id 0.
Cities 3 and 2 finish with low 1, and hand it up. City 1 finishes with low[1] = disc[1] = 1, so the stack is popped down to 1. The group {1, 2, 3} gets comp_id 1.
Then a new search starts at city 7. Its flight 7 -> 4 leads to city 4, which is already in group 0. Here on_stack[4] is False, so disc[4] = 4 is not used, and low[7] stays 7. City 7 forms the group {7} with comp_id 2.
There are 3 groups, so the answer is NO. Group 0 was completed first. No flight leaves it, so no route leads from city 6 to city 7. The answer is ["NO", 6, 7], and the final comp is [1, 1, 1, 0, 0, 0, 2].
The traps: the on_stack test, the flag, and passing low up
The first trap is the test if on_stack[nxt]:. A neighbour that was reached but is not on group_stack is in a group that is already complete. It cannot reach node, so its disc must not lower low[node].
On the board graph, leave the test out. City 7 sees city 4, and low[7] becomes 4. Now low[7] is not disc[7], so city 7 is never popped. The result is comp = [1, 1, 1, 0, 0, 0, -1], and city 7 has no group.
The smallest example is n = 2 and edges = [[2, 1]]. City 1 forms its own group first. City 2 then sees city 1 and takes low[2] = 1, so city 2 is never popped. Only one group is counted, and Flight Routes Check would answer ["YES"] where the right answer is ["NO", 1, 2].
The second trap is the flag. on_stack[node] must become False only when node is popped with its group, not when the search leaves node. A node can wait on group_stack long after the search has left it. For n = 3 and edges = [[1, 2], [2, 1], [1, 3], [3, 2]], an early clear gives [1, 1, 0], two groups. The right answer is [0, 0, 0].
The third trap is the last step of a finished node: low[parent] = min(low[parent], low[node]). On the board graph, without it the result is [3, 2, 2, 1, 0, 0, 4], five groups instead of three.
The search is iterative, with path holding (node, iterator). Flight Routes Check allows 10^5 cities in one chain. A recursive dfs would nest 10^5 calls. With Python's default limit, a chain of 2000 cities already raises RecursionError.
The cost, and how it differs from Kosaraju's algorithm
Each city is pushed on group_stack once and popped once. Each flight is read once by the iterator of its city. The time is O(V + E). adj, disc, low, on_stack, comp, group_stack and path hold one entry per city or per flight, so the space is O(V + E).
On the board graph, 7 cities and 8 flights give 7 pushes, 7 pops and 8 flight reads.
Strongly Connected Components (Kosaraju) finds the same groups with two searches and a reversed copy of the graph. The first search records the finish order. The second search reads that order from the end and walks the reversed graph.
Tarjan's algorithm needs one search over the original graph, and no reversed copy. It needs two extra arrays, low and on_stack, and a stack of waiting nodes. Both algorithms cost O(V + E).
The group ids come out in opposite orders. Kosaraju numbers a group that no edge enters first. Tarjan numbers a group that no edge leaves first. Every edge between two groups goes from a higher comp_id to a lower one in Tarjan's output.
The undirected cousin of this card is Tarjan's Bridges. It uses the same disc and low, but it needs no stack, because an undirected search has no edge between two different subtrees. If the graph is undirected and you only count pieces, a plain search or union-find is enough.
60-Second Pitch
"Every city must reach every other city, so I check whether the graph is one strongly connected group. I use Tarjan's algorithm, which is one depth-first search. I number each city with disc when I reach it, and I keep a stack of cities whose group is not complete. low is the smallest disc of a waiting city that a city's subtree reaches. A flight to a reached city lowers low only if that city is still on the stack. When a city finishes with low equal to disc, it is the first city of a group, so I pop the stack down to it. One group means YES. Otherwise the first group completed has no flight out, so I name a city from it and a city outside it. The search is iterative, and the time and space are both O(V + E)."
Common Questions
- Why must the test be
on_stack[nxt]and not onlydisc[nxt] != 0? A reached node that is not on
group_stackis in a group that is already complete. It cannot reach the current node, so it must not lowerlow. If it does, the current node is never popped. Forn = 2andedges = [[2, 1]], the group of city 2 is lost and Flight Routes Check answers YES instead of naming a pair.- Should
lowtakedisc[nxt]orlow[nxt]from a node that is still on the stack? Both give the same groups, and
disc[nxt]is the standard form. A test of both forms on 20000 random graphs of up to 8 nodes gave the samecompevery time.- How is Tarjan's algorithm different from Kosaraju's, and which would you pick?
Both find the same groups in
O(V + E). Kosaraju uses two searches and a reversed graph, and its reason is short to explain. Tarjan uses one search over the original graph, withdisc,lowand a stack. Pick Tarjan when the reversed graph is not stored, or when you want the groups from a sink group to a source group.- Why does the first group completed give a pair with no route?
A group is completed only when nothing in it climbs to an older waiting node, so every flight that leaves its nodes leads to a group that is already complete. The first group has none to lead to. So no flight leaves it, and no route leads from a city of that group to a city of any other group.
- What are the time and space complexity?
O(V + E)time andO(V + E)space. Each node is pushed and popped once, and each flight is read once. The search uses an explicitpath, so a long chain does not hit the recursion limit.
SWE Reasoning
Spot it
- The graph is directed, and the question asks which nodes can reach each other.
- The words are every city reaches every other city, mutual reachability, groups, kingdoms, or cycles of dependencies.
- The answer for a whole graph is YES when only one group exists, so the number of groups matters.
- The input is large, up to 10^5 nodes, so the search must be iterative and
O(V + E). - You want the groups from one search, without building a reversed copy of the graph.
The invariant, step by step
disc[u]is 0 until the search reachesu. The first time, it gets the next number fromtime,low[u]starts equal to it, andugoes ongroup_stackwithon_stack[u]True.- The search reads one flight at a time from the top of
path. A flight to a node withdisc[nxt] == 0goes deeper. - A flight to a node that is already reached lowers
low[node]only ifon_stack[nxt]is True. Otherwise the flight is ignored. - When a node has no flight left, it is popped from
path. Iflow[node] == disc[node],group_stackis popped down tonode, and each popped node getscomp_idandon_stackFalse. - If
low[node]is smaller thandisc[node], the node stays ongroup_stack. Itslowgoes to the parent withlow[parent] = min(low[parent], low[node]). - When every search is done, each node has one
comp_id. Two nodes share it exactly when each can reach the other, and every edge between two groups goes from a higher id to a lower one.
Traps
- Using
disc[nxt]for every reached node. A node outsidegroup_stackis in a complete group. Teston_stack[nxt]first. - Clearing
on_stack[node]when the search leavesnode. Clear it when the node is popped with its group. - Skipping
low[parent] = min(low[parent], low[node]). A back edge found deep in the search then never reaches the nodes between. - Recursive
dfs. A chain of 10^5 cities nests 10^5 calls. Python stops at 1000 by default. Keep the path inpath. - Naming the pair in the wrong direction. The first group completed has no flight leaving it, so it is the city of that group that cannot reach another group, not the other way round.