Articulation Points
Find every vertex of an undirected graph whose removal disconnects it, via DFS low-link values with a special rule for the root.
Overview
An articulation point (cut vertex) is a vertex whose removal — together with its incident edges — increases the number of Connected Components. A graph with no articulation points (and at least 3 vertices) is biconnected: any two vertices lie on a common cycle.
The algorithm is the vertex analogue of Bridges: DFS with discovery times disc[u] and low-link values low[u]. A non-root vertex u is an articulation point if it has some DFS child v with low[v] >= disc[u] — the subtree of v cannot reach *above* u without passing through u. The root of the DFS tree is special: it is an articulation point iff it has two or more DFS children.
Unlike bridges, one vertex can be reported by several children; collect into a boolean array, not a list, to avoid duplicates. The blocks between articulation points are the biconnected components, which can be extracted with an edge stack in the same DFS.
Intuition
A mental model before the formal terms.
Think of the DFS tree as a hanging mobile. Pinch a vertex u and lift it out. Each child subtree stays attached to the rest only if it has a rope (back edge) tied *strictly above* u. If some child subtree's highest rope reaches only u itself or lower — low[v] >= disc[u] — that subtree drops off. Note the >=: a rope tied to u does not help, because u is the thing being removed. For bridges it was > because there the edge, not u, was being removed and reaching u was enough.
The root has no "above", so the rule degenerates: it is a cut vertex only if it holds two separate child subtrees together, i.e. has at least two DFS children.
Example: edges 0—1, 1—2, 2—0, 2—3. DFS from 0: disc = [0, 1, 2, 3]. low[3] = 3 >= disc[2] = 2 → 2 is an articulation point (removing it strands 3). low[2] = 0 (back edge to 0), so low[2] < disc[1] → 1 is not. Root 0 has one DFS child (1) → not an articulation point.
How it works
- Initialise
disc[v] = -1, timert = 0,isCut[v] = false. dfs(u, parent):disc[u] = low[u] = t++;children = 0. For each neighbourv: ifv == parent, skip (for multigraphs skip only the one edge you came along). Ifvundiscovered:children++,dfs(v, u),low[u] = min(low[u], low[v]); ifparent != -1andlow[v] >= disc[u], markisCut[u]. Else:low[u] = min(low[u], disc[v]).- After the loop, if
parent == -1andchildren > 1, markisCut[u](root rule). - Run
dfs(s, -1)from every undiscovered vertexs.
Why it works
Removing a non-root u separates a child subtree T(v) from the rest iff T(v) has no back edge to a proper ancestor of u. Since all non-tree edges in an undirected DFS are back edges to ancestors, "escaping" means reaching a discovery time < disc[u]. low[v] is the minimum reachable, so low[v] >= disc[u] ⇔ no escape ⇔ u is a cut vertex. Ancestors of u and the other subtrees remain connected through the tree, so this is the only way the count of components can rise.
For the root, every child subtree is separated from every other child subtree when the root is removed (a back edge from one child subtree can only go to an ancestor — which is the root itself). So with ≥ 2 children the root is a cut vertex, and with ≤ 1 child its removal leaves a connected tree.
Same DFS as bridges: O(V + E).
Recognition
How to tell a problem wants this.
- "Which server / person / junction, if removed, splits the network?" — vertex removal, undirected graph.
- "Is the network resilient to any single node failure?" — check for zero articulation points.
- Biconnected components, block-cut tree, "cactus" graph structure questions.
Interactive visualization
Play, step, change the input. ← → and space work too.
1time = 02def dfs(u, parent):3 disc[u] = low[u] = time; time += 14 for v in neighbors(u), skipping parent:5 if v unvisited: dfs(v, u); low[u] = min(low[u], low[v])6 if parent != None and low[v] >= disc[u]: u is an articulation point7 else: low[u] = min(low[u], disc[v]) # back edge8 if parent == None and tree children >= 2: root u is an articulation point9for u in nodes: if u unvisited: dfs(u, None)Pseudocode
1disc[*] = -1; t = 0; isCut[*] = false2dfs(u, parent):3 disc[u] = low[u] = t++; children = 04 for v in adj[u]:5 if v == parent: continue6 if disc[v] == -1:7 children++; dfs(v, u); low[u] = min(low[u], low[v])8 if parent != -1 and low[v] >= disc[u]: isCut[u] = true9 else: low[u] = min(low[u], disc[v])10 if parent == -1 and children > 1: isCut[u] = trueImplementations
1def articulation_points(adj: list[list[int]]) -> list[int]:2 """An articulation point (cut vertex) is a vertex whose removal increases3 the number of connected components. Tarjan's rule, same disc/low machinery4 as bridges but with two cases:5 - non-root u is a cut vertex iff some child v has low[v] >= disc[u]6 - the root is a cut vertex iff it has two or more DFS children"""7 n = len(adj)8 91 · disc, low, and a flag list for the answer10 disc = [-1] * n11 low = [0] * n12 is_cut = [False] * n13 timer = 014 15 for s in range(n):16 if disc[s] != -1:17 continue18 root_children = 019 202 · Frames carry the vertex, its DFS parent, and a neighbour cursor21 stack = [[s, -1, 0]] # [vertex, parent, neighbour cursor]22 disc[s] = low[s] = timer23 timer += 124 25 while stack:26 f = stack[-1]27 u, parent, i = f[0], f[1], f[2]28 if i < len(adj[u]):29 f[2] += 130 v = adj[u][i]31 if v == parent:32 continue # the edge we arrived on33 if disc[v] == -1:34 if u == s:35 root_children += 136 disc[v] = low[v] = timer37 timer += 138 stack.append([v, u, 0])39 else:40 low[u] = min(low[u], disc[v]) # back edge41 else:423 · Unwinding: propagate low, then test the non-root rule43 stack.pop()44 if stack:45 p = stack[-1][0]46 low[p] = min(low[p], low[u])47 if p != s and low[u] >= disc[p]:48 is_cut[p] = True49 504 · The root is special: it cuts only if the DFS branched twice51 if root_children >= 2:52 is_cut[s] = True53 545 · Collect the flagged vertices55 return [v for v in range(n) if is_cut[v]]- Frames are mutable three-element lists so
f[2] += 1advances the cursor in place. u, parent, i = f[0], f[1], f[2]unpacks the frame at the top of each iteration for readability.- The
>=inlow[u] >= disc[p]is the cut-vertex rule, distinct from the bridge rule's>. root_childrenis reset per component because it is declared inside the outerfor sloop.- The final list comprehension collects the flagged vertices in one expression.
networkx.articulation_points(G)yields them directly, andnetworkx.biconnected_components(G)gives the related decomposition.- A tuple frame would raise
TypeErroron the cursor increment, hence the list. - The list comprehension
[v for v in range(n) if is_cut[v]]is idiomatic;list(compress(range(n), is_cut))fromitertoolsis the faster C-level equivalent. - The iterative form avoids
RecursionError, which a recursive Tarjan reaches on a path of about 1000 vertices.
- Declaring
root_childrenoutside the component loop, so it accumulates and misreports later roots. - Using
>instead of>=, computing bridge endpoints rather than cut vertices. - Using a tuple frame and hitting
TypeErroron the increment.
- The algorithm is identical in all four; the only structural difference is how the DFS frame is mutated — C++ indexes the stack to avoid reference invalidation, JS/TS mutate a heap object, and Python mutates a list.
- C++ is again the only language where the frame reference can be invalidated by a push, which is why this version indexes
stack[depth]rather than bindingback(). - Library support:
networkx.articulation_pointsin Python and Boost.Grapharticulation_pointsin C++; nothing in JS/TS. - Collecting the flagged vertices: a C++ loop, a JS/TS loop or
flatMap, and a Python list comprehension (oritertools.compress) — the same operation with increasingly compact spellings.
Complexity
Same DFS as bridge-finding; both can be computed in one pass.
Compare growth rates in the Complexity Explorer →When to use — and when not to
- Single-node-failure analysis of undirected networks.
- Building the block-cut tree / biconnected components for structural questions.
- Checking biconnectivity (no cut vertices and connected).
- Edge removal questions — use Bridges (
>instead of>=, no root rule). - Directed graphs — vertex-cut reachability there needs dominator trees or SCC reasoning.
- "Remove
kvertices" fork ≥ 2— vertex connectivity in general needs max-flow.
Alternatives
Common mistakes
- Using
>instead of>=— a child whose subtree reaches exactlyustill gets cut off whenuis removed. - Applying the
low[v] >= disc[u]rule to the root — a root with one child would be misreported. The root needs the child-count rule. - Counting
childrenfrom the neighbour list instead of from *DFS tree* children (only count whendisc[v] == -1before recursing). - Pushing
uinto a result list every time the condition fires, producing duplicates; use a boolean array. - Updating
low[u]fromlow[v]for back edges (should bedisc[v]).
Interview patterns
- Find all cut vertices, then answer "is the network resilient to one node failure".
- Biconnected components via an edge stack popped whenever
low[v] >= disc[u]. - Explain the difference between the
>(bridge) and>=(articulation) conditions and the root special case — a classic interview probe.
- Choosing between BFS, DFS, Dijkstra and DPAdvanced
- Stack versus queueBeginner
- Recursion versus iterationIntermediate
- When space complexity mattersIntermediate
- Course ScheduleIntermediate
- Number of IslandsIntermediate
- Word SearchAdvanced