Bridges and articulation points
Find the edges and nodes whose removal would split an undirected network into pieces, in one DFS using discovery times and low-links.
O(n + e)Space O(n + e)2 practice problemsStep-through pictureThe idea
In an undirected network, a bridge is an edge that is the only connection between two parts: remove it and the network splits. An articulation point (or cut vertex) is a node with the same property. These are the single points of failure in a network.
Checking each edge by removing it and running a search would take O(e * (n + e)) time. Tarjan's method finds them all in one DFS. Each node gets disc, its discovery time (the order DFS reached it), and low, the earliest discovery time it can reach by going down the DFS tree and then taking at most one edge back up.
The rule: the tree edge from node to its child next is a bridge exactly when low[next] > disc[node]. That means nothing below next has any other way back up to node or above it.
An everyday example
Escape routes from a building
Imagine exploring a building and numbering rooms in the order you enter them. For every room you ask: "from here or any room further in, what's the lowest-numbered room I can reach through a side door?" If all the rooms behind one doorway can't reach anything at or before the room you came from except through that doorway, then that doorway is the only way out. Blocking it traps everyone behind it.
| In the example | In the code |
|---|---|
| The number you give a room when you enter | disc[node] |
| The lowest room reachable through side doors from here or further in | low[node] |
| The doorway you came through | parentEdge, the edge id you skip |
| A doorway that is the only way out | A bridge: low[next] > disc[node] |
From the slow way to bridges and articulation points
- Start with the obvious way: remove each edge in turn, run a BFS, and see whether the graph is still in one piece. That's
esearches ofO(n + e)each. For 10,000 cables, it's hundreds of millions of steps. - Look at what a bridge really is: an edge that lies on no loop. If an edge is on a loop, removing it still leaves the rest of the loop as a way around. So the question becomes: which edges have no loop through them?
- DFS on an undirected graph makes loops easy to see. Every edge that isn't a tree edge joins a node to one of its ancestors, called a back edge, because DFS would have explored any unvisited neighbor right away. A back edge from
uup to an ancestor closes a loop with the tree path between them. So the tree edge fromnodeto its childnextis on a loop exactly when some back edge goes fromnext's subtree tonodeor higher. - Number nodes in the order DFS reaches them,
disc, and computelow[node]: the smallestdiscthatnode's subtree can reach with one back edge, filled in as each call returns. Then the tree edge is a bridge whenlow[next] > disc[node]. Skip the edge you came in on by its id, so it isn't mistaken for a back edge. That single DFS is Tarjan's bridge-finding algorithm.
How it works
- Build the undirected
graph, storing[neighbor, edgeId]so each edge can be recognized from both ends. - Run
visit(node, parentEdge)from every node that has nodiscyet. - In
visit, setdisc[node]andlow[node]to the currenttime, then add one totime. - For each
[next, id], skip the edge you arrived by (id === parentEdge). - If
nextis new, visit it, then takelow[node] = min(low[node], low[next]). Iflow[next] > disc[node], the edge is a bridge. - If
nextwas already visited, it's a back edge: takelow[node] = min(low[node], disc[next]).
Watch it run
This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.
See every step at onceThe same run, laid out side by side
The code
Point at a note to light up the lines it explains.
function findBridges(n, edges) {const graph = Array.from({ length: n }, () => []);edges.forEach(([a, b], id) => {graph[a].push([b, id]);graph[b].push([a, id]);});const disc = new Array(n).fill(-1);const low = new Array(n).fill(0);const bridges = [];let time = 0;function visit(node, parentEdge) {disc[node] = low[node] = time++;for (const [next, id] of graph[node]) {if (id === parentEdge) continue;if (disc[next] === -1) {visit(next, id);low[node] = Math.min(low[node], low[next]);if (low[next] > disc[node]) bridges.push(edges[id]);} else {low[node] = Math.min(low[node], disc[next]);}}}for (let node = 0; node < n; node++) {if (disc[node] === -1) visit(node, -1);}return bridges;}
How fast it is
| Time | O(n + e) |
|---|---|
| Extra memory | O(n + e) |
One DFS visits each node once and checks each edge from both ends. The graph, disc, low, and the call stack fit in O(n + e).
- Best case
O(n + e)Every input. Even with no edges, the outer loop starts
none-node searches. - Average case
O(n + e)Every input: each node is visited once and each edge is looked at from both ends.
- Worst case
O(n + e)Every input. A long path is the same speed but makes the recursion
ncalls deep.
Common mistakes
- Using
low[next]instead ofdisc[next]for back edges in the articulation point version, which can mark points wrongly. - Skipping the parent node instead of the parent edge, which breaks graphs with duplicate edges.
- Using
>=for bridges or>for articulation points. Bridges uselow[next] > disc[node]; articulation points use>=. - Forgetting the special rule for the DFS root when looking for articulation points.
- Running it only from node 0 when the network has several pieces.
When to use it
Look for these clues in a problem:
- The question asks which links or nodes are critical: removing one would disconnect something.
- The network is undirected, such as cables, roads, or friendships.
- You need single points of failure, or edges that are not part of any loop.
- A brute force of removing each edge and testing connectivity would be too slow.
Check yourself
Answer each one in your head first, then open it.
Why is the bridge test > while the articulation point test is >=?
If the child's subtree can reach node itself, there's a loop through the edge, so it's not a bridge. But removing node still cuts that subtree off, so node is a cut point.
Why skip the arrival edge by id instead of skipping the parent node?
With two cables to the parent, the second one is a real way back up. Skipping the parent node would ignore it and report a bridge that isn't one.
How many bridges does a tree with 8 nodes have?
7: a tree has n - 1 edges and no loops, so every edge is a bridge.
In the trace, why is 4-5 not a bridge even though low[5] is not 0?
low[5] = 3, which is above node 4, so 5's subtree can reach node 3 through edge 5-3, around 4-5.
Practice problems
Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.
0 of 2 solved. Solve one to earn the bronze medal, half for silver, and all of them for gold.
Start practicingGo further
You don't need these to start practicing. Open one when you want more.
Why it works
The reasoning that makes it always right
In a DFS of an undirected graph, every edge is either a tree edge or joins a node to one of its ancestors or descendants. There are no edges across branches: when DFS stands on u and sees an unvisited neighbor v, it visits v right away, so v ends up below u.
When visit(node) returns, low[node] is the smallest disc among node itself and every node reachable from node's subtree by one non-tree edge, not counting the edge node arrived by. The code builds this from node's own visited neighbors, using disc[next], and from each child's low as the child returns. Edges that point down to an already-finished descendant, like 3-5 in the trace, give a larger disc and change nothing.
Removing the tree edge node-next cuts off next's subtree exactly when no other edge leaves that subtree. Because there are no edges across branches, any other edge leaving the subtree must go up to node or higher, which would make low[next] <= disc[node]. So the edge is a bridge exactly when low[next] > disc[node]. Each node is visited once, so the DFS always ends.
Going deeper
3 more ideas
Why the low-link test finds bridges
Everything DFS discovers below next forms a subtree. Its only possible connections to the rest of the graph are the tree edge node-next and back edges from inside the subtree to nodes above it. low[next] is the earliest node those back edges reach. If it's still later than node (low[next] > disc[node]), no back edge escapes, so the tree edge is the only link: a bridge.
Articulation points
A node that isn't the DFS root is an articulation point when some child next has low[next] >= disc[node] (note >=, not >): the child's subtree can reach at most node itself. The root is an articulation point when it has two or more DFS children. In the example, nodes 1 and 3 are articulation points.
Why skip by edge id instead of by parent node
If two cables connect the same pair of nodes, neither is a bridge. Skipping the parent node would ignore the second cable too and wrongly report a bridge. Skipping only the exact edge you came in on handles duplicate edges correctly.
Work it by hand
Every step in a table, with why
Seven nodes: a triangle, a bridge, a second triangle, and a dead-end node. Both bridges are found as the calls return.
n = 7, edges = [[1,0], [0,2], [2,1], [2,3], [3,4], [4,5], [5,3], [5,6]]
| Step | Event | disc[node] | low[node] | What happens | Why |
|---|---|---|---|---|---|
| 1 | visit(0, -1) | 0 | 0 | Start the DFS | Node 0 is discovered first. Its first neighbor is 1, through edge 0. |
| 2 | visit(1, 0) | 1 | 1 | Skip edge 0 back to 0 | Edge 0 is how node 1 was reached, so it doesn't count as a way back up. Next is node 2, through edge 2. |
| 3 | visit(2, 2) | 2 | 0 | Back edge to 0 | Node 2's edge to 0, edge 1, reaches an already-visited ancestor, so low[2] = disc[0] = 0. |
| 4 | visit(3, 3) | 3 | 3 | Go deeper | Node 3 is reached by edge 3, from node 2. Its first other neighbor is 4. |
| 5 | visit(4, 4) | 4 | 4 | Go deeper | Node 4's only other neighbor is 5. |
| 6 | visit(5, 5) | 5 | 3 | Back edge to 3 | Edge 6 joins node 5 to its ancestor 3, so low[5] = 3. Then node 5 visits 6. |
| 7 | visit(6, 7) | 6 | 6 | Only the arrival edge | Node 6's one edge is the one it came in on, so low[6] stays at its own disc. |
| 8 | back at 5 from 6 | 5 | 3 | 6 > 5: bridge [5, 6] | Nothing below 6 can reach 5 or above without edge 7, so it's a bridge. |
| 9 | back at 4 from 5 | 4 | 3 | 3 > 4 is false | Node 5's subtree reaches node 3, above 4, so edge 4-5 is on a loop. low[4] becomes 3. |
| 10 | back at 3 from 4 | 3 | 3 | 3 > 3 is false | The subtree reaches node 3 itself, so 3-4 is on the loop 3-4-5. Then node 3's edge to 5 is already visited, and disc[5] = 5 changes nothing. |
| 11 | back at 2 from 3 | 2 | 0 | 3 > 2: bridge [2, 3] | Nothing below 3 reaches node 2 or higher, so the edge 2-3 is the only link. |
| 12 | back at 1, then 0 | 1, then 0 | 0, then 0 | No more bridges | low[2] = 0 flows up to 1 and 0, so the triangle's edges are on a loop. Node 0's edge to 2 is already visited. |
It returns [[5, 6], [2, 3]], deepest first. The final low is [0, 0, 0, 3, 3, 3, 6]. With the >= rule for articulation points, nodes 2, 3, and 5 are cut points, and the root 0, with one child, is not.
Compared with other ways
When to pick something else
| Approach | Time | Extra memory | Choose it when |
|---|---|---|---|
| Bridges and articulation points | O(n + e) | O(n + e) | You need every critical link or node in one pass, on any size of network. |
| Remove each edge and search | O(n · e + e^2) | O(n + e) | The graph is tiny, or you want an easy check to test the fast version against. |
Extra memory: O(n + e): the adjacency lists store each edge twice, disc and low hold n numbers, and the call stack can be n deep.
How the steps grow
- Bridges and articulation points
O(n + e) - Remove each edge and search
O(n · e + e^2)
| Approach | n = 10 | n = 1,000 | n = 1,000,000 |
|---|---|---|---|
| Bridges and articulation points | 20 | 2,000 | 2 million |
| Remove each edge and search | 200 | 2 million | 2 trillion |
Inputs that need care
6 tricky inputs
- Two cables between the same pair:
[[0,1], [0,1], [1,2]] - Only the arrival edge is skipped by id, so the second
0-1counts as a back edge. It returns[[1, 2]], correctly leaving0-1out. - A network in several pieces
- The outer loop starts a new DFS from every node without a
disc, so bridges in every piece are found. - A tree, with no loops
- Every edge is a bridge, so it returns all
n - 1edges. - One big loop through every node
- Every subtree reaches back to the root, so it returns
[]. - A self-loop like
[3, 3] - It's stored twice in
graph[3]and points at an already-visited node,3itself, so it changes nothing and is never a bridge. - A path of 100,000 nodes
- The recursion goes 100,000 calls deep and can overflow the call stack. Use an explicit stack for graphs that deep.
Variations and follow-ups
Twists interviewers add
Articulation points
Use the same disc and low, but a non-root node is a cut point when some child has low[next] >= disc[node]: the child's subtree can't get above node. The DFS root is a cut point only when it has two or more DFS children.
Two-edge-connected pieces
Remove every bridge, and the pieces left are groups where every pair of nodes has two routes with no shared edge. Shrinking each piece to a node turns the graph into a tree of bridges, which answers "how many bridges lie between a and b?".
Biconnected components
Push each edge onto a stack as DFS explores it. When a child satisfies low[next] >= disc[node], pop edges down to node-next: those edges form one piece that has no single cut point inside.
In an interview
Spot it, explain it, answer follow-ups
- Spot it from "critical connections", "single point of failure", "which link, if cut, disconnects the network", or "edges not on any cycle" in an undirected graph.
- Explain
lowin plain words: "the earliest node this subtree can climb back to without using the edge it came down". Then the bridge test reads naturally: if the child can't climb back tonode, the edge is the only way. - Test by hand a triangle with a tail, where only the tail edges are bridges, and a pair of nodes joined by two cables, where neither cable is a bridge.
- Expect "how do you find articulation points instead?". Answer: change
>to>=for non-root nodes, and treat the root separately: it's a cut point only with two or more DFS children.
Where frontend code uses it
The same idea in real UI code
- Infrastructure and network dashboards that highlight single points of failure, such as the one link between two data centers.
- Graph visualization tools that highlight edges which, if removed, would split a diagram into separate pieces.
- Game level editors that check a map for chokepoints: corridors or rooms whose blocking would cut the level in two.