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.

P3HardMental model~6 min speedrun

Tarjan's Bridges & Articulation Points

Grows into
Tarjan SCC
The problem

Critical Connections in a Network (LC 1192)(opens leetcode.com)

Return every critical link of a network of n servers, numbered 0 to n - 1, in any order. The links are the pairs in connections: [ai, bi] is a two-way link between servers ai and bi, and together the links let every server reach every other one, directly or through other servers.

A link is critical when taking it away leaves some server unable to reach some other server. A critical link may be returned as [ai, bi] or as [bi, ai].

Example 1
Input:n = 4, connections = [[0,1],[1,2],[2,0],[1,3]]
Output:[[1,3]]
Servers 0, 1 and 2 form a triangle, so any one of its links can go; the link 1-3 is server 3's only way in. [[3,1]] is accepted too.
0123

Tarjan's Bridges

DFS tree ➔ discovery time and low-link per node ➔ bridge when low[child] > disc[parent]
Spot it when: You must find the edges (or nodes) whose removal splits an undirected graph into more pieces: critical links, single points of failure, cells whose loss disconnects a region.
Tarjan's Bridges
Python
1from typing import List
2 
3def find_bridges(n: int, edges: List[List[int]]) -> List[List[int]]:
4 """Tarjan's Bridges (the core of Critical Connections in a Network): every edge
5 whose removal disconnects the graph.
6 Time: O(V + E) | Space: O(V + E)
7 """
8 graph = [[] for _ in range(n)]
9 for i, (u, v) in enumerate(edges):
10 graph[u].append((v, i))
11 graph[v].append((u, i))
12 # discovery time; -1 = not reached yet
13 disc = [-1] * n
14 # earliest disc the subtree reaches with one back edge
15 low = [0] * n
16 bridges = []
17 time = 0
18 for root in range(n):
19 if disc[root] != -1:
20 continue
21 disc[root] = low[root] = time
22 time += 1
23 # (node, edge id we came in by, next neighbour index)
24 stack = [(root, -1, 0)]
25 while stack:
26 node, in_edge, idx = stack[-1]
27 if idx < len(graph[node]):
28 stack[-1] = (node, in_edge, idx + 1)
29 nxt, edge = graph[node][idx]
30 if edge == in_edge:
31 continue # not the tree edge back to the parent
32 if disc[nxt] == -1:
33 disc[nxt] = low[nxt] = time # tree edge: go deeper
34 time += 1
35 stack.append((nxt, edge, 0))
36 else:
37 # back edge: reaches an earlier node
38 low[node] = min(low[node], disc[nxt])
39 else:
40 stack.pop() # node is finished
41 if stack:
42 parent = stack[-1][0]
43 low[parent] = min(low[parent], low[node])
44 # nothing below node climbs to parent or above
45 if low[node] > disc[parent]:
46 bridges.append([parent, node])
47 return bridges
Invariant: disc[u] is the order in which the DFS first reached u, and once u is finished, low[u] is the smallest disc its subtree reaches using tree edges down and one other edge up.

Number the nodes in DFS order with disc, and let low[node] be the earliest disc that node's subtree can reach through one edge other than the tree edge it came in by; when node finishes, the tree edge parent -> node is a bridge exactly when low[node] > disc[parent]. This is correct because any other route between the two sides of that edge has to leave node's subtree by some edge, and an undirected DFS has no edges across subtrees, so that edge climbs to parent or above and would make low[node] <= disc[parent]. The trap is the comparison: bridges need the strict low[node] > disc[parent]. With >=, which is the test for articulation points, an edge whose subtree climbs back exactly to parent (so it sits on a cycle) is wrongly reported.

See it run

Finding bridges with disc and low, step by step

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges [[6, 7]]

bridges [[6, 7], [5, 6]]

bridges [[6, 7], [5, 6]]

bridges [[6, 7], [5, 6]]

bridges [[6, 7], [5, 6], [1, 3]]

bridges [[6, 7], [5, 6], [1, 3]]

bridges [[6, 7], [5, 6], [1, 3]] · answer [[6, 7], [5, 6], [1, 3]]

Each circle is a node. The two rows under the graph give disc and low for every node, and the small number under each box is the node.

  • now the node the search is working on
  • stack a node, or a search edge, on the current search path
  • done a finished node, or an edge that is not a bridge
  • back an edge to a node that was reached earlier
  • bridge an edge that disconnects the graph when removed

Start the search at node 0. disc[0] = low[0] = 0, and node 0 goes on stack. Every other disc is -1, which means not reached yet.

Follow the edge 0-1 to node 1, which is new. disc[1] = low[1] = 1.

Follow the edge 1-2 to node 2, which is new. disc[2] = low[2] = 2.

Node 2 sees the edge to node 0, which was reached earlier with disc 0. So low[2] becomes 0.

Node 2 is finished, and low[1] = min(1, 0) = 0. Here low[2] = 0 is not greater than disc[1] = 1. So the edge 1-2 is not a bridge.

Follow the edge 1-3 to node 3, which is new. disc[3] = low[3] = 3.

Follow the edge 3-4 to node 4, which is new. disc[4] = low[4] = 4.

Follow the edge 4-5 to node 5, which is new. disc[5] = low[5] = 5.

Node 5 sees the edge to node 3, which was reached earlier with disc 3. So low[5] becomes 3.

Follow the edge 5-6 to node 6, which is new. disc[6] = low[6] = 6.

Follow the edge 6-7 to node 7, which is new. disc[7] = low[7] = 7.

Node 7 is finished, and low[6] stays 6. Here low[7] = 7 is greater than disc[6] = 6. Nothing below node 7 climbs to node 6 or above, so the edge 6-7 is a bridge.

Node 6 is finished, and low[5] stays 3. Here low[6] = 6 is greater than disc[5] = 5. Nothing below node 6 climbs to node 5 or above, so the edge 5-6 is a bridge.

Node 5 is finished, and low[4] = min(4, 3) = 3. Here low[5] = 3 is not greater than disc[4] = 4. So the edge 4-5 is not a bridge.

Node 4 is finished, and low[3] = min(3, 3) = 3. Here low[4] = 3 is not greater than disc[3] = 3. So the edge 3-4 is not a bridge.

Node 3 is finished, and low[1] stays 0. Here low[3] = 3 is greater than disc[1] = 1. Nothing below node 3 climbs to node 1 or above, so the edge 1-3 is a bridge.

Node 1 is finished, and low[0] = min(0, 0) = 0. Here low[1] = 0 is not greater than disc[0] = 0. So the edge 0-1 is not a bridge.

Every node is finished. The bridges, in the order they were found, are [[6, 7], [5, 6], [1, 3]].

0now1234567
disc
00
-1
-2
-3
-4
-5
-6
-7
low
00
-1
-2
-3
-4
-5
-6
-7
1 / 18
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Testing low[node] >= disc[parent] instead of >

    Mistake
    if low[node] >= disc[parent]:
    Fix
    if low[node] > disc[parent]:

    What happens: low[node] == disc[parent] means the subtree of node climbs back exactly to parent. So the edge parent -> node lies on a cycle, and removing it disconnects nothing. >= reports it anyway. >= is the test for articulation points, not for bridges.

    Example: Critical Connections in a Network (LC 1192): n = 4, connections = [[0, 1], [1, 2], [2, 0], [1, 3]]
    Mistake: returns [[1, 3], [0, 1]]Fix: returns [[1, 3]]
  2. 2

    Gotcha 2: Writing the search as a recursive dfs

    Mistake
    def dfs(node, in_edge):
        ...
        dfs(nxt, edge)
    Fix
    stack = [(root, -1, 0)]

    What happens: A recursive dfs makes one nested call per node on the current path. Critical Connections in a Network (LC 1192) allows 10^5 servers in one chain. Python's default recursion limit is 1000. A chain of 2000 servers already raises a RecursionError. The blueprint keeps its path in stack, which has no such limit.

    Example: n = 2000 servers in one chain: [[0, 1], [1, 2], ..., [1998, 1999]]
    Mistake: raises RecursionErrorFix: returns 1999
  3. 3

    Gotcha 3: Following the edge back to the parent

    Mistake
    nxt, edge = graph[node][idx]
    Fix
    nxt, edge = graph[node][idx]
    if edge == in_edge:
        continue

    What happens: The tree edge parent -> node is also in graph[node]. If it is followed back, node reaches parent through it. So low[node] <= disc[parent] always holds, and no bridge is ever found. The check skips that one edge by its id, in_edge. Two parallel edges have different ids, so the second parallel edge is still followed as a back edge.

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

    Gotcha 4: Not passing low[node] up to the parent

    Mistake
    parent = stack[-1][0]
    Fix
    parent = stack[-1][0]
    low[parent] = min(low[parent], low[node])

    What happens: low[parent] must learn how high the subtree of node can climb. Without this line, low[parent] sees only the back edges of parent itself. An edge above a cycle then looks like a bridge.

    Example: n = 4, connections = [[0, 1], [1, 2], [2, 0], [1, 3]]
    Mistake: returns [[1, 3], [0, 1]]Fix: returns [[1, 3]]
Not this one if…
  • You need the edge that closes a cycle as edges arrive one by one, as in Redundant Connection (LC 684), not the edges that no cycle protects. See Union-Find
  • You only need to count the connected pieces as they are, as in Number of Provinces (LC 547), not which single removal would split one. See Flood Fill

Deep Dive

Why low[node] > disc[parent] marks a bridge

A bridge is an edge whose removal splits the graph into more pieces. Run a DFS (depth-first search) and give each node a number, disc, in the order the search first reaches it. The edges the search follows to new nodes are tree edges. Every other edge of an undirected graph joins a node to one of its ancestors or descendants, and it is called a back edge.

low[node] is the smallest disc that the subtree of node can reach with tree edges down and one back edge up. When node finishes, look at the tree edge parent -> node. If low[node] > disc[parent], nothing in the subtree of node has any edge to parent or above. The only link between the subtree and the rest of the graph is the edge parent -> node, so it is a bridge.

If low[node] <= disc[parent], some edge from the subtree climbs to parent or above. That edge is a second route, so removing parent -> node disconnects nothing.

An undirected DFS has no edge between two different subtrees. So a second route from the subtree of node can only go up. This is why low needs only the disc of nodes that were already reached.

Example: in the loop 0, 1, 2 with the extra node 3 on node 1, the edge [1, 3] is a bridge and the edges of the loop are not. This is the example of Critical Connections in a Network (LC 1192).

One trace: eight servers, three bridges

The board uses 8 nodes. Nodes 0 to 3 and their edges [0, 1], [1, 2], [2, 0], [1, 3] are the example of Critical Connections in a Network (LC 1192). The extra edges [3, 4], [4, 5], [5, 3], [5, 6], [6, 7] add a second loop and a chain.

The search goes 0, 1, 2. At node 2, the edge to node 0 is a back edge, so low[2] becomes 0. Node 2 finishes, and low[1] becomes 0. Since low[2] = 0 is not greater than disc[1] = 1, the edge 1-2 is not a bridge.

Then the search goes 1, 3, 4, 5, 6, 7. At node 5, the edge to node 3 is a back edge, so low[5] becomes 3. Node 7 finishes first of these nodes. Its low[7] = 7 is greater than disc[6] = 6, so 6-7 is a bridge. Node 6 finishes with low[6] = 6, which is greater than disc[5] = 5, so 5-6 is a bridge.

Node 5 finishes with low[5] = 3, which is not greater than disc[4] = 4. Node 4 finishes with low[4] = 3, and disc[3] = 3. This is not greater, so 3-4 is not a bridge. Node 3 finishes with low[3] = 3, which is greater than disc[1] = 1, so 1-3 is a bridge.

The final answer is [[6, 7], [5, 6], [1, 3]], in the order the bridges were found. The final low values are [0, 0, 0, 3, 3, 3, 6, 7].

The traps: >=, the parent edge, and passing low up

Bridges need the strict test low[node] > disc[parent]. With >=, an edge whose subtree climbs back exactly to parent is reported. That edge is on a cycle. On the board graph, >= reports 5 edges: the 3 real bridges and also 3-4 and 0-1.

The edge back to the parent must be skipped by its id, edge == in_edge. Without the skip, node sees parent through the tree edge, so low[node] <= disc[parent] always holds and no bridge is ever found. The id and not the node is compared, so a second edge between the same two nodes is still treated as a back edge.

low[parent] = min(low[parent], low[node]) must run when node finishes. Without it, low[parent] sees only the back edges of parent itself. On the board graph, leaving that line out reports the same 5 edges as >=: 3-4 and 0-1 look like bridges, because low[4] and low[1] never learn about the back edges in their subtrees.

The search is iterative, with stack holding (node, in_edge, idx). Critical Connections in a Network (LC 1192) allows 10^5 servers in one chain. A recursive dfs would nest 10^5 calls and pass Python's recursion limit.

The cost, and the neighbours

Each node is pushed on stack once. Each entry in graph[node] is read once, so each edge is read twice. The time is O(V + E). graph, disc, low and stack hold at most one entry per node or per edge, so the space is O(V + E).

On the board graph, 8 nodes and 9 edges give 18 neighbour entries. The search reads each entry once.

The same two arrays also find articulation points, which are nodes whose removal splits the graph. That test is low[child] >= disc[node], and the root needs a separate rule. This card finds edges, so the strict test applies.

If edges arrive one at a time and you need the first edge that closes a cycle, use union-find, as in Redundant Connection (LC 684). If you only count the pieces, a plain search is enough, as in Number of Provinces (LC 547). Bridges answer a different question: which single edge is the only link.

60-Second Pitch

"I need the edges whose removal splits the network. I run one DFS and give each node a number, disc, in the order it is reached. For each node I also keep low, the smallest disc its subtree can reach with one back edge. When a node finishes, I pass its low up to its parent. The tree edge from the parent is a bridge exactly when low[node] > disc[parent]. That means nothing below the node has an edge to the parent or above, so this edge is the only link. I skip the edge I came in by, using its id. I use > and not >=, because >= is the articulation point test and reports edges on cycles. Each node and each edge is handled once, so the time is O(V + E) and the space is O(V + E). I use an explicit stack so a long chain cannot pass the recursion limit."

Common Questions

Why is low[node] > disc[parent] the right test?

low[node] is the smallest disc the subtree of node can reach. If it is greater than disc[parent], no edge from the subtree reaches parent or any node above. So the tree edge parent -> node is the only link between the subtree and the rest of the graph.

Why can a subtree escape only through an edge to an ancestor?

In an undirected DFS, every non-tree edge joins a node to one of its ancestors or descendants. If an edge joined two different subtrees, the search would have followed it and made it a tree edge. So any second route from the subtree of node must go up to parent or above.

What is the difference between a bridge and an articulation point?

A bridge is an edge whose removal increases the number of pieces. An articulation point is a node whose removal does. For a bridge, the test is low[child] > disc[node]. For an articulation point, it is low[child] >= disc[node], and the root needs its own rule: it is one when it has two or more children in the DFS tree.

What are the time and space complexity?

O(V + E) time and O(V + E) space. Each node enters stack once, and each edge is read twice, once from each end. graph holds both directions of every edge.

Why use an iterative search?

The input can be one long chain of 10^5 nodes. A recursive dfs would make one nested call per node, and Python's default limit is 1000 calls. The blueprint keeps (node, in_edge, idx) on stack, so the depth is limited by memory only.

SWE Reasoning

Spot it

  • The question asks for the links, roads or cables whose failure splits the network.
  • The wording says critical, single point of failure, or the only path between two parts.
  • The graph is undirected, and removing one edge or one node can increase the number of pieces.
  • The input can be large, up to 10^5 nodes, so the search must be O(V + E) and iterative.
  • Checking each edge by removing it and counting pieces would cost O(E * (V + E)). That is too slow.

The invariant, step by step

  1. disc[u] is -1 until the search reaches u. The first time, it gets the next number from time, and low[u] starts equal to it.
  2. The search reads one neighbour at a time from the top of stack. It skips the edge it came in by, edge == in_edge.
  3. A neighbour with disc[nxt] == -1 is a tree edge. The search sets its numbers and pushes it.
  4. A neighbour that already has a number is a back edge. It lowers low[node] to min(low[node], disc[nxt]).
  5. When a node has no neighbour left, it is popped. Its low goes up to the parent with low[parent] = min(low[parent], low[node]).
  6. The tree edge parent -> node is added to bridges exactly when low[node] > disc[parent]. No edge from the subtree reaches parent or above, so this edge is the only link.

Traps

  • >= instead of >. low[node] == disc[parent] means the subtree climbs back to parent, so the edge is on a cycle. >= is the test for articulation points, not for bridges.
  • Following the edge back to the parent. The tree edge is in graph[node] too. Skip it by its id, edge == in_edge.
  • Not passing low[node] to the parent. Then low[parent] knows only its own back edges, and edges above a cycle look like bridges.
  • Recursive dfs. A chain of 10^5 servers nests 10^5 calls. Python stops at 1000 by default. Keep the path in stack.
  • Skipping the parent by node and not by edge id. Two parallel edges between the same two nodes are not bridges. Compare edge ids so the second edge counts as a back edge.

Practice

Canonical LeetCode:Critical Connections in a Network (LC 1192)Minimum Number of Days to Disconnect Island (LC 1568)
Theory Context•Graph Algorithms

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

P3HardMental model~6 min speedrun

Tarjan's Bridges & Articulation Points

Grows into
Tarjan SCC
The problem

Critical Connections in a Network (LC 1192)(opens leetcode.com)

Return every critical link of a network of n servers, numbered 0 to n - 1, in any order. The links are the pairs in connections: [ai, bi] is a two-way link between servers ai and bi, and together the links let every server reach every other one, directly or through other servers.

A link is critical when taking it away leaves some server unable to reach some other server. A critical link may be returned as [ai, bi] or as [bi, ai].

Example 1
Input:n = 4, connections = [[0,1],[1,2],[2,0],[1,3]]
Output:[[1,3]]
Servers 0, 1 and 2 form a triangle, so any one of its links can go; the link 1-3 is server 3's only way in. [[3,1]] is accepted too.
0123

Tarjan's Bridges

DFS tree ➔ discovery time and low-link per node ➔ bridge when low[child] > disc[parent]
Spot it when: You must find the edges (or nodes) whose removal splits an undirected graph into more pieces: critical links, single points of failure, cells whose loss disconnects a region.
Tarjan's Bridges
Python
1from typing import List
2 
3def find_bridges(n: int, edges: List[List[int]]) -> List[List[int]]:
4 """Tarjan's Bridges (the core of Critical Connections in a Network): every edge
5 whose removal disconnects the graph.
6 Time: O(V + E) | Space: O(V + E)
7 """
8 graph = [[] for _ in range(n)]
9 for i, (u, v) in enumerate(edges):
10 graph[u].append((v, i))
11 graph[v].append((u, i))
12 # discovery time; -1 = not reached yet
13 disc = [-1] * n
14 # earliest disc the subtree reaches with one back edge
15 low = [0] * n
16 bridges = []
17 time = 0
18 for root in range(n):
19 if disc[root] != -1:
20 continue
21 disc[root] = low[root] = time
22 time += 1
23 # (node, edge id we came in by, next neighbour index)
24 stack = [(root, -1, 0)]
25 while stack:
26 node, in_edge, idx = stack[-1]
27 if idx < len(graph[node]):
28 stack[-1] = (node, in_edge, idx + 1)
29 nxt, edge = graph[node][idx]
30 if edge == in_edge:
31 continue # not the tree edge back to the parent
32 if disc[nxt] == -1:
33 disc[nxt] = low[nxt] = time # tree edge: go deeper
34 time += 1
35 stack.append((nxt, edge, 0))
36 else:
37 # back edge: reaches an earlier node
38 low[node] = min(low[node], disc[nxt])
39 else:
40 stack.pop() # node is finished
41 if stack:
42 parent = stack[-1][0]
43 low[parent] = min(low[parent], low[node])
44 # nothing below node climbs to parent or above
45 if low[node] > disc[parent]:
46 bridges.append([parent, node])
47 return bridges
Invariant: disc[u] is the order in which the DFS first reached u, and once u is finished, low[u] is the smallest disc its subtree reaches using tree edges down and one other edge up.

Number the nodes in DFS order with disc, and let low[node] be the earliest disc that node's subtree can reach through one edge other than the tree edge it came in by; when node finishes, the tree edge parent -> node is a bridge exactly when low[node] > disc[parent]. This is correct because any other route between the two sides of that edge has to leave node's subtree by some edge, and an undirected DFS has no edges across subtrees, so that edge climbs to parent or above and would make low[node] <= disc[parent]. The trap is the comparison: bridges need the strict low[node] > disc[parent]. With >=, which is the test for articulation points, an edge whose subtree climbs back exactly to parent (so it sits on a cycle) is wrongly reported.

See it run

Finding bridges with disc and low, step by step

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges []

bridges [[6, 7]]

bridges [[6, 7], [5, 6]]

bridges [[6, 7], [5, 6]]

bridges [[6, 7], [5, 6]]

bridges [[6, 7], [5, 6], [1, 3]]

bridges [[6, 7], [5, 6], [1, 3]]

bridges [[6, 7], [5, 6], [1, 3]] · answer [[6, 7], [5, 6], [1, 3]]

Each circle is a node. The two rows under the graph give disc and low for every node, and the small number under each box is the node.

  • now the node the search is working on
  • stack a node, or a search edge, on the current search path
  • done a finished node, or an edge that is not a bridge
  • back an edge to a node that was reached earlier
  • bridge an edge that disconnects the graph when removed

Start the search at node 0. disc[0] = low[0] = 0, and node 0 goes on stack. Every other disc is -1, which means not reached yet.

Follow the edge 0-1 to node 1, which is new. disc[1] = low[1] = 1.

Follow the edge 1-2 to node 2, which is new. disc[2] = low[2] = 2.

Node 2 sees the edge to node 0, which was reached earlier with disc 0. So low[2] becomes 0.

Node 2 is finished, and low[1] = min(1, 0) = 0. Here low[2] = 0 is not greater than disc[1] = 1. So the edge 1-2 is not a bridge.

Follow the edge 1-3 to node 3, which is new. disc[3] = low[3] = 3.

Follow the edge 3-4 to node 4, which is new. disc[4] = low[4] = 4.

Follow the edge 4-5 to node 5, which is new. disc[5] = low[5] = 5.

Node 5 sees the edge to node 3, which was reached earlier with disc 3. So low[5] becomes 3.

Follow the edge 5-6 to node 6, which is new. disc[6] = low[6] = 6.

Follow the edge 6-7 to node 7, which is new. disc[7] = low[7] = 7.

Node 7 is finished, and low[6] stays 6. Here low[7] = 7 is greater than disc[6] = 6. Nothing below node 7 climbs to node 6 or above, so the edge 6-7 is a bridge.

Node 6 is finished, and low[5] stays 3. Here low[6] = 6 is greater than disc[5] = 5. Nothing below node 6 climbs to node 5 or above, so the edge 5-6 is a bridge.

Node 5 is finished, and low[4] = min(4, 3) = 3. Here low[5] = 3 is not greater than disc[4] = 4. So the edge 4-5 is not a bridge.

Node 4 is finished, and low[3] = min(3, 3) = 3. Here low[4] = 3 is not greater than disc[3] = 3. So the edge 3-4 is not a bridge.

Node 3 is finished, and low[1] stays 0. Here low[3] = 3 is greater than disc[1] = 1. Nothing below node 3 climbs to node 1 or above, so the edge 1-3 is a bridge.

Node 1 is finished, and low[0] = min(0, 0) = 0. Here low[1] = 0 is not greater than disc[0] = 0. So the edge 0-1 is not a bridge.

Every node is finished. The bridges, in the order they were found, are [[6, 7], [5, 6], [1, 3]].

0now1234567
disc
00
-1
-2
-3
-4
-5
-6
-7
low
00
-1
-2
-3
-4
-5
-6
-7
1 / 18
Gotchas1 of 4

Gotcha 1 of 4

  1. 1
    The trap

    Gotcha 1: Testing low[node] >= disc[parent] instead of >

    Mistake
    if low[node] >= disc[parent]:
    Fix
    if low[node] > disc[parent]:

    What happens: low[node] == disc[parent] means the subtree of node climbs back exactly to parent. So the edge parent -> node lies on a cycle, and removing it disconnects nothing. >= reports it anyway. >= is the test for articulation points, not for bridges.

    Example: Critical Connections in a Network (LC 1192): n = 4, connections = [[0, 1], [1, 2], [2, 0], [1, 3]]
    Mistake: returns [[1, 3], [0, 1]]Fix: returns [[1, 3]]
  2. 2

    Gotcha 2: Writing the search as a recursive dfs

    Mistake
    def dfs(node, in_edge):
        ...
        dfs(nxt, edge)
    Fix
    stack = [(root, -1, 0)]

    What happens: A recursive dfs makes one nested call per node on the current path. Critical Connections in a Network (LC 1192) allows 10^5 servers in one chain. Python's default recursion limit is 1000. A chain of 2000 servers already raises a RecursionError. The blueprint keeps its path in stack, which has no such limit.

    Example: n = 2000 servers in one chain: [[0, 1], [1, 2], ..., [1998, 1999]]
    Mistake: raises RecursionErrorFix: returns 1999
  3. 3

    Gotcha 3: Following the edge back to the parent

    Mistake
    nxt, edge = graph[node][idx]
    Fix
    nxt, edge = graph[node][idx]
    if edge == in_edge:
        continue

    What happens: The tree edge parent -> node is also in graph[node]. If it is followed back, node reaches parent through it. So low[node] <= disc[parent] always holds, and no bridge is ever found. The check skips that one edge by its id, in_edge. Two parallel edges have different ids, so the second parallel edge is still followed as a back edge.

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

    Gotcha 4: Not passing low[node] up to the parent

    Mistake
    parent = stack[-1][0]
    Fix
    parent = stack[-1][0]
    low[parent] = min(low[parent], low[node])

    What happens: low[parent] must learn how high the subtree of node can climb. Without this line, low[parent] sees only the back edges of parent itself. An edge above a cycle then looks like a bridge.

    Example: n = 4, connections = [[0, 1], [1, 2], [2, 0], [1, 3]]
    Mistake: returns [[1, 3], [0, 1]]Fix: returns [[1, 3]]
Not this one if…
  • You need the edge that closes a cycle as edges arrive one by one, as in Redundant Connection (LC 684), not the edges that no cycle protects. See Union-Find
  • You only need to count the connected pieces as they are, as in Number of Provinces (LC 547), not which single removal would split one. See Flood Fill

Deep Dive

Why low[node] > disc[parent] marks a bridge

A bridge is an edge whose removal splits the graph into more pieces. Run a DFS (depth-first search) and give each node a number, disc, in the order the search first reaches it. The edges the search follows to new nodes are tree edges. Every other edge of an undirected graph joins a node to one of its ancestors or descendants, and it is called a back edge.

low[node] is the smallest disc that the subtree of node can reach with tree edges down and one back edge up. When node finishes, look at the tree edge parent -> node. If low[node] > disc[parent], nothing in the subtree of node has any edge to parent or above. The only link between the subtree and the rest of the graph is the edge parent -> node, so it is a bridge.

If low[node] <= disc[parent], some edge from the subtree climbs to parent or above. That edge is a second route, so removing parent -> node disconnects nothing.

An undirected DFS has no edge between two different subtrees. So a second route from the subtree of node can only go up. This is why low needs only the disc of nodes that were already reached.

Example: in the loop 0, 1, 2 with the extra node 3 on node 1, the edge [1, 3] is a bridge and the edges of the loop are not. This is the example of Critical Connections in a Network (LC 1192).

One trace: eight servers, three bridges

The board uses 8 nodes. Nodes 0 to 3 and their edges [0, 1], [1, 2], [2, 0], [1, 3] are the example of Critical Connections in a Network (LC 1192). The extra edges [3, 4], [4, 5], [5, 3], [5, 6], [6, 7] add a second loop and a chain.

The search goes 0, 1, 2. At node 2, the edge to node 0 is a back edge, so low[2] becomes 0. Node 2 finishes, and low[1] becomes 0. Since low[2] = 0 is not greater than disc[1] = 1, the edge 1-2 is not a bridge.

Then the search goes 1, 3, 4, 5, 6, 7. At node 5, the edge to node 3 is a back edge, so low[5] becomes 3. Node 7 finishes first of these nodes. Its low[7] = 7 is greater than disc[6] = 6, so 6-7 is a bridge. Node 6 finishes with low[6] = 6, which is greater than disc[5] = 5, so 5-6 is a bridge.

Node 5 finishes with low[5] = 3, which is not greater than disc[4] = 4. Node 4 finishes with low[4] = 3, and disc[3] = 3. This is not greater, so 3-4 is not a bridge. Node 3 finishes with low[3] = 3, which is greater than disc[1] = 1, so 1-3 is a bridge.

The final answer is [[6, 7], [5, 6], [1, 3]], in the order the bridges were found. The final low values are [0, 0, 0, 3, 3, 3, 6, 7].

The traps: >=, the parent edge, and passing low up

Bridges need the strict test low[node] > disc[parent]. With >=, an edge whose subtree climbs back exactly to parent is reported. That edge is on a cycle. On the board graph, >= reports 5 edges: the 3 real bridges and also 3-4 and 0-1.

The edge back to the parent must be skipped by its id, edge == in_edge. Without the skip, node sees parent through the tree edge, so low[node] <= disc[parent] always holds and no bridge is ever found. The id and not the node is compared, so a second edge between the same two nodes is still treated as a back edge.

low[parent] = min(low[parent], low[node]) must run when node finishes. Without it, low[parent] sees only the back edges of parent itself. On the board graph, leaving that line out reports the same 5 edges as >=: 3-4 and 0-1 look like bridges, because low[4] and low[1] never learn about the back edges in their subtrees.

The search is iterative, with stack holding (node, in_edge, idx). Critical Connections in a Network (LC 1192) allows 10^5 servers in one chain. A recursive dfs would nest 10^5 calls and pass Python's recursion limit.

The cost, and the neighbours

Each node is pushed on stack once. Each entry in graph[node] is read once, so each edge is read twice. The time is O(V + E). graph, disc, low and stack hold at most one entry per node or per edge, so the space is O(V + E).

On the board graph, 8 nodes and 9 edges give 18 neighbour entries. The search reads each entry once.

The same two arrays also find articulation points, which are nodes whose removal splits the graph. That test is low[child] >= disc[node], and the root needs a separate rule. This card finds edges, so the strict test applies.

If edges arrive one at a time and you need the first edge that closes a cycle, use union-find, as in Redundant Connection (LC 684). If you only count the pieces, a plain search is enough, as in Number of Provinces (LC 547). Bridges answer a different question: which single edge is the only link.

60-Second Pitch

"I need the edges whose removal splits the network. I run one DFS and give each node a number, disc, in the order it is reached. For each node I also keep low, the smallest disc its subtree can reach with one back edge. When a node finishes, I pass its low up to its parent. The tree edge from the parent is a bridge exactly when low[node] > disc[parent]. That means nothing below the node has an edge to the parent or above, so this edge is the only link. I skip the edge I came in by, using its id. I use > and not >=, because >= is the articulation point test and reports edges on cycles. Each node and each edge is handled once, so the time is O(V + E) and the space is O(V + E). I use an explicit stack so a long chain cannot pass the recursion limit."

Common Questions

Why is low[node] > disc[parent] the right test?

low[node] is the smallest disc the subtree of node can reach. If it is greater than disc[parent], no edge from the subtree reaches parent or any node above. So the tree edge parent -> node is the only link between the subtree and the rest of the graph.

Why can a subtree escape only through an edge to an ancestor?

In an undirected DFS, every non-tree edge joins a node to one of its ancestors or descendants. If an edge joined two different subtrees, the search would have followed it and made it a tree edge. So any second route from the subtree of node must go up to parent or above.

What is the difference between a bridge and an articulation point?

A bridge is an edge whose removal increases the number of pieces. An articulation point is a node whose removal does. For a bridge, the test is low[child] > disc[node]. For an articulation point, it is low[child] >= disc[node], and the root needs its own rule: it is one when it has two or more children in the DFS tree.

What are the time and space complexity?

O(V + E) time and O(V + E) space. Each node enters stack once, and each edge is read twice, once from each end. graph holds both directions of every edge.

Why use an iterative search?

The input can be one long chain of 10^5 nodes. A recursive dfs would make one nested call per node, and Python's default limit is 1000 calls. The blueprint keeps (node, in_edge, idx) on stack, so the depth is limited by memory only.

SWE Reasoning

Spot it

  • The question asks for the links, roads or cables whose failure splits the network.
  • The wording says critical, single point of failure, or the only path between two parts.
  • The graph is undirected, and removing one edge or one node can increase the number of pieces.
  • The input can be large, up to 10^5 nodes, so the search must be O(V + E) and iterative.
  • Checking each edge by removing it and counting pieces would cost O(E * (V + E)). That is too slow.

The invariant, step by step

  1. disc[u] is -1 until the search reaches u. The first time, it gets the next number from time, and low[u] starts equal to it.
  2. The search reads one neighbour at a time from the top of stack. It skips the edge it came in by, edge == in_edge.
  3. A neighbour with disc[nxt] == -1 is a tree edge. The search sets its numbers and pushes it.
  4. A neighbour that already has a number is a back edge. It lowers low[node] to min(low[node], disc[nxt]).
  5. When a node has no neighbour left, it is popped. Its low goes up to the parent with low[parent] = min(low[parent], low[node]).
  6. The tree edge parent -> node is added to bridges exactly when low[node] > disc[parent]. No edge from the subtree reaches parent or above, so this edge is the only link.

Traps

  • >= instead of >. low[node] == disc[parent] means the subtree climbs back to parent, so the edge is on a cycle. >= is the test for articulation points, not for bridges.
  • Following the edge back to the parent. The tree edge is in graph[node] too. Skip it by its id, edge == in_edge.
  • Not passing low[node] to the parent. Then low[parent] knows only its own back edges, and edges above a cycle look like bridges.
  • Recursive dfs. A chain of 10^5 servers nests 10^5 calls. Python stops at 1000 by default. Keep the path in stack.
  • Skipping the parent by node and not by edge id. Two parallel edges between the same two nodes are not bridges. Compare edge ids so the second edge counts as a back edge.

Practice

Canonical LeetCode:Critical Connections in a Network (LC 1192)Minimum Number of Days to Disconnect Island (LC 1568)
© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•