⚙️AlgorithmIntermediate

Bridges and Articulation Points

Key Points

  • A bridge is an edge whose removal increases the number of connected components; an articulation point is a vertex with the same property.
  • We can find both using one DFS by tracking discovery times and low-link values that capture the earliest reachable ancestor.
  • In an undirected graph, edge (u, v) is a bridge if and only if low[v] > disc[u] where v is a DFS child of u.
  • A vertex u is an articulation point if it is a DFS root with at least two DFS children, or it has a child v with low[v] >= disc[u].
  • The algorithm runs in O(V + E) time and O(V + E) space and is known as Tarjan’s algorithm for bridges and cut vertices.
  • Bridges define 2-edge-connected components and contracting them yields a tree called the bridge tree.
  • Articulation points and biconnected components can be organized into a block-cut tree that is always bipartite.
  • These structures are practical for network reliability, query decomposition, and many graph problems in competitive programming.

Prerequisites

  • Undirected graphs and adjacency listsYou need to represent graphs efficiently and understand edges in both directions.
  • Depth-first search (DFS)The algorithm relies on DFS traversal order, parent/child edges, and backtracking.
  • Trees and tree terminologyDFS induces a tree structure; ancestor/descendant relationships are central to low-link logic.
  • Big-O notationTo analyze why the algorithm is linear time and linear space.
  • Stacks and recursionRecursive DFS uses the call stack, and Tarjan’s biconnected components use an explicit edge stack.

Detailed Explanation

Tap terms for definitions

01Overview

Hook: Imagine a network of roads connecting cities. If a single road breaks and the country splits into two isolated regions, that road was critical. Likewise, if a single city’s shutdown disconnects entire areas, that city was critical too. These are exactly the ideas behind bridges and articulation points in graphs. Concept: Bridges are edges that, when removed, disconnect the graph. Articulation points (cut vertices) are vertices that, when removed, disconnect the graph. We can detect both efficiently with a single depth-first search (DFS) using two timestamps per vertex: discovery time (when we first visit it) and low-link value (the earliest discovery time reachable from its subtree using at most one back edge). The low-link concept elegantly summarizes all paths that can climb back to ancestors, allowing us to test if an edge or vertex holds the graph together. Example: Consider a chain of nodes 0–1–2–3. Every edge is a bridge because removing any edge breaks the chain. No internal cycles means no alternate path. If we add an extra edge 1–3, then edge 2–3 stops being a bridge because a cycle provides an alternate route. Similarly, vertex 2 might or might not be an articulation point depending on whether its incident edges lie on cycles that keep the graph connected after removing 2. With one DFS pass, we can label all bridges and articulation points in O(V + E) time.

02Intuition & Analogies

Hook: Think of a mountain trail system. Some trails are the only passage between two large areas; closing such a trail splits the park. Some cabins (junctions) also work like chokepoints: if a key cabin shuts down, hikers cannot pass between regions. Concept: DFS builds a spanning tree of the graph—the route you’d take if you always walk to the next unvisited junction. Edges not in this tree that go from a node to one of its ancestors are like safety ladders (back edges): they create shortcuts that keep the system connected even if part of the DFS tree is removed. The low-link value of a node answers: “What is the earliest (smallest discovery time) ancestor I can reach from this node or its descendants using at most one back edge?” If a child subtree cannot climb above its parent, then the edge to that child is critical—a bridge. If a node has a child subtree that cannot climb to the node’s ancestors, then removing the node separates that subtree—making the node an articulation point. Example: Suppose we have a hub-and-spoke setup: a central node 0 connects to nodes 1, 2, 3, 4, but there are no edges among the leaves. Every edge (0, i) is a bridge because the leaf has no alternate path, and 0 is an articulation point because removing it isolates all leaves. Add extra edges among leaves to form a cycle (say 1–2–3–1): suddenly, edges (0,1), (0,2), (0,3) may no longer be bridges if cycles provide alternate routes. However, if some leaf like 4 remains isolated (no extra edges), edge (0,4) stays a bridge. This mirrors real networks: redundancy (cycles) removes single points of failure.

03Formal Definition

Let G = (V, E) be a simple, undirected graph. Perform a DFS that assigns each vertex u a discovery time disc[u] (the time step when u is first visited, starting from 0 and increasing by 1) and a low-link value low[u]. Define low[u] as the minimum discovery time reachable from u by using zero or more tree edges (edges in the DFS tree) followed by at most one back edge (an edge from a node to a strict ancestor in the DFS tree). Formally, low[u] = min{ disc[u], min(disc[w]) over all back edges (u, w), and min(low[v]) over all tree edges (u, v) } with proper parent handling. Edge (u, v) with parent u and DFS child v is a bridge if and only if low[v] > disc[u]. Vertex u (non-root) is an articulation point if and only if there exists a DFS child v such that low[v] >= disc[u]. The DFS root is an articulation point if and only if it has at least two DFS children. These conditions arise because low[v] > disc[u] implies no back edge from v’s subtree can reach u or any ancestor of u, so removing (u, v) disconnects the subtree. Similarly, low[v] >= disc[u] implies v’s subtree cannot reach an ancestor of u without passing through u, making u critical for connectivity. For disconnected graphs, apply DFS from each unvisited node.

04When to Use

Use bridges and articulation points when you need to analyze reliability, chokepoints, or redundancy in undirected networks. Typical scenarios include: network design (finding single points of failure in computer, road, or power grids); query decomposition (contracting bridges to form a tree for faster queries); and solving problems that ask for the number of connected components after removing certain edges or vertices. In competitive programming, they are essential for: building the bridge tree (2-edge-connected components) to answer path or component queries efficiently; computing biconnected components and constructing the block-cut tree to reduce problems to tree DP; and detecting whether the graph is already 2-edge- or 2-vertex-connected (no bridges or no articulation points). Choose this method when the graph is undirected and static (no online edge insertions/deletions) and input sizes are large—since the algorithm is linear O(V + E). For directed graphs, different notions apply (strong articulation points/bridges) and require other algorithms. If the graph changes over time, consider dynamic bridge-finding data structures, but they are more complex. Finally, leveraging these decompositions often turns a hard general-graph problem into a simpler tree problem.

⚠️Common Mistakes

  1. Misusing the parent edge: In DFS, when exploring u -> v, do not treat the edge back to u’s parent as a back edge; otherwise low[u] will be incorrectly small. Always skip the exact parent edge in undirected graphs. 2) Wrong inequalities: For bridges, the correct test is low[v] > disc[u] (strict). Using >= would misclassify edges on cycles. For articulation points (non-root), use low[v] >= disc[u] (non-strict). 3) Root handling: The DFS root is an articulation point only if it has at least two DFS children. Counting back edges from the root or using the non-root rule for the root leads to errors. 4) Disconnected graphs: Run DFS from every unvisited node. Otherwise, you will miss bridges/articulation points in other components. 5) Parallel edges and self-loops: In multigraphs, parallel edges can prevent an edge from being a bridge even if one copy is the DFS tree edge. Ensure your parent-skip logic only skips the specific parent edge; the other parallel edge should be recognized as a back edge. Self-loops are never bridges but can affect low values if not filtered. 6) Recursion depth: On large graphs, recursive DFS may overflow the stack. Consider iterative DFS or increase stack size when the environment allows. 7) Using directed-graph logic: These definitions and tests are for undirected graphs; directed graphs require different algorithms. 8) Forgetting to initialize arrays and timers: Uninitialized disc/low values or reusing global state between test cases often causes subtle bugs.

Key Formulas

Low-link Definition

Explanation: The low-link of u is the smallest discovery time reachable from u using zero or more tree edges and at most one back edge. It aggregates information from back edges and children.

Bridge Test

Explanation: If the child v’s subtree cannot reach u or any ancestor of u via a back edge, then removing edge (u, v) disconnects the graph. The strict inequality ensures cycles are not misclassified.

Articulation Test (Non-root)

Explanation: If some child v cannot reach an ancestor of u without passing through u, then removing u disconnects v’s subtree from the rest.

Articulation Test (Root)

Explanation: A DFS root is an articulation point only when it has at least two DFS children, meaning removal of the root separates those child subtrees.

Time Complexity

Explanation: Each vertex and edge is visited a constant number of times during DFS. Therefore, the total running time grows linearly with vertices plus edges.

Handshake Lemma

Explanation: This identity helps reason about linear passes over adjacency lists. Iterating over all adjacency lists touches exactly 2|E| neighbor entries in an undirected graph.

Bridge Tree Property

Explanation: After contracting 2-edge-connected components, each (connected) component becomes a tree. If there are c connected components, the total edges equal nodes minus c.

Distinct Discovery Times

Explanation: Discovery times are unique and strictly increase as vertices are first visited. This underpins ancestor checks like disc[w] < disc[u] for back edges.

Complexity Analysis

The DFS-based algorithm for finding bridges and articulation points runs in O(V + E) time. Each vertex u is discovered once, assigned a discovery time disc[u] and low-link value low[u]. For each undirected edge (u, v), the algorithm examines it at most twice (once from u and once from v). Updating low values and performing tests such as low[v] > disc[u] or low[v] >= disc[u] are O(1) per edge visit. Thus, the total work across all adjacency lists is proportional to the sum of vertex degrees, which equals 2, yielding linear time in the size of the graph. Space usage is O(V + E). We store adjacency lists requiring O(V + E) memory. Arrays disc, low, parent, and boolean flags (e.g., i) take O(V). If we record bridges as pairs, they take up to O(E) in the worst case (e.g., a tree). The recursion stack in a recursive implementation can grow to O(V) in the worst case (e.g., a long chain), so the effective auxiliary space may be O(V). Implementations that construct additional structures, such as the bridge tree (contracted 2-edge-connected components) or block-cut tree (for articulation points and biconnected components), also use O(V + E) space because each original edge contributes a constant number of entities in the derived tree. Overall, both time and space are linear, making the technique scalable to large graphs typical in competitive programming constraints.

Code Examples

Find Bridges and Articulation Points with One DFS (Undirected Graph)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Computes bridges and articulation points in an undirected graph using DFS.
5// - 0-indexed vertices [0..n-1]
6// - Handles disconnected graphs
7// - Works with parallel edges; the non-parent parallel edge serves as a back edge
8
9struct Graph {
10 int n;
11 vector<vector<int>> adj; // adjacency list
12 Graph(int n) : n(n), adj(n) {}
13 void add_edge(int u, int v) {
14 adj[u].push_back(v);
15 adj[v].push_back(u);
16 }
17};
18
19struct BridgesAP {
20 int n;
21 const vector<vector<int>> &adj;
22 vector<int> disc, low, parent;
23 vector<bool> is_art;
24 vector<pair<int,int>> bridges;
25 int timer;
26
27 BridgesAP(const Graph &g)
28 : n(g.n), adj(g.adj), disc(n, -1), low(n, -1), parent(n, -1), is_art(n, false), timer(0) {}
29
30 void dfs(int u) {
31 disc[u] = low[u] = timer++; // set discovery time and initial low
32 int child_count = 0; // number of DFS children of u
33
34 for (int v : adj[u]) {
35 if (v == parent[u]) {
36 // Skip the exact parent edge in undirected graphs
37 continue;
38 }
39 if (disc[v] == -1) {
40 // Tree edge u -> v
41 parent[v] = u;
42 child_count++;
43 dfs(v);
44 // After recursion, update low[u] using child's low
45 low[u] = min(low[u], low[v]);
46
47 // Bridge test: If v's subtree cannot reach u or above
48 if (low[v] > disc[u]) {
49 bridges.emplace_back(min(u, v), max(u, v));
50 }
51 // Articulation test for non-root u
52 if (parent[u] != -1 && low[v] >= disc[u]) {
53 is_art[u] = true;
54 }
55 } else {
56 // Back edge u -> v to an ancestor (disc[v] < disc[u])
57 // Use discovery times to detect ancestor; in undirected graphs
58 // a forward edge to a discovered descendant shouldn't appear here
59 low[u] = min(low[u], disc[v]);
60 }
61 }
62 // Special rule for root: articulation iff it has >= 2 DFS children
63 if (parent[u] == -1 && child_count >= 2) {
64 is_art[u] = true;
65 }
66 }
67
68 void run() {
69 for (int u = 0; u < n; ++u) {
70 if (disc[u] == -1) dfs(u);
71 }
72 sort(bridges.begin(), bridges.end());
73 bridges.erase(unique(bridges.begin(), bridges.end()), bridges.end());
74 }
75};
76
77int main() {
78 ios::sync_with_stdio(false);
79 cin.tie(nullptr);
80
81 // Example usage:
82 // Input format:
83 // n m
84 // m lines of edges u v (0-indexed)
85
86 int n, m;
87 if (!(cin >> n >> m)) return 0;
88 Graph g(n);
89 for (int i = 0; i < m; ++i) {
90 int u, v; cin >> u >> v; g.add_edge(u, v);
91 }
92
93 BridgesAP solver(g);
94 solver.run();
95
96 // Output articulation points
97 cout << "Articulation Points:\n";
98 for (int u = 0; u < n; ++u) if (solver.is_art[u]) cout << u << " ";
99 cout << "\n";
100
101 // Output bridges
102 cout << "Bridges:\n";
103 for (auto [u, v] : solver.bridges) cout << u << " " << v << "\n";
104
105 return 0;
106}
107

This program runs a DFS to compute discovery times and low-link values. It marks an edge (u, v) as a bridge if low[v] > disc[u] and marks a vertex u as an articulation point if it satisfies the non-root or root rule. It iterates from each unvisited node to handle disconnected graphs. Parent edges are skipped to avoid misclassifying them as back edges. Parallel edges are fine: one is the parent edge, another becomes a back edge that prevents a false bridge.

Time: O(V + E)Space: O(V + E)
Build the Bridge Tree (2-Edge-Connected Components)
1#include <bits/stdc++.h>
2using namespace std;
3
4// Build 2-edge-connected components by first finding bridges, then contracting
5// non-bridge edges. The resulting bridge tree is a forest (a tree per component).
6// Useful for path queries where only bridges matter.
7
8struct Edge { int u, v; };
9
10struct Graph {
11 int n; vector<vector<pair<int,int>>> adj; // (to, edge_id)
12 vector<Edge> edges;
13 Graph(int n): n(n), adj(n) {}
14 int add_edge(int u, int v){
15 int id = (int)edges.size();
16 edges.push_back({u,v});
17 adj[u].push_back({v,id});
18 adj[v].push_back({u,id});
19 return id;
20 }
21};
22
23struct Bridges {
24 int n; const Graph &g; int timer;
25 vector<int> disc, low, peid; // peid: parent edge id
26 vector<char> is_bridge; // mark bridges by edge id
27 Bridges(const Graph &g): n(g.n), g(g), timer(0), disc(n,-1), low(n,-1), peid(n,-1), is_bridge(g.edges.size(), 0) {}
28
29 void dfs(int u){
30 disc[u] = low[u] = timer++;
31 for (auto [v, id] : g.adj[u]){
32 if (id == peid[u]) continue; // skip parent edge
33 if (disc[v] == -1){
34 peid[v] = id;
35 dfs(v);
36 low[u] = min(low[u], low[v]);
37 if (low[v] > disc[u]) is_bridge[id] = 1;
38 } else {
39 // back edge to ancestor (disc[v] < disc[u])
40 low[u] = min(low[u], disc[v]);
41 }
42 }
43 }
44
45 void run(){
46 for (int u = 0; u < n; ++u) if (disc[u] == -1) dfs(u);
47 }
48};
49
50// After marking bridges, assign component ids by DFS that ignores bridges
51struct BridgeTreeBuilder {
52 const Graph &g; Bridges &br;
53 vector<int> comp; // component id per vertex
54 int comps;
55
56 BridgeTreeBuilder(const Graph &g, Bridges &br): g(g), br(br), comp(g.n, -1), comps(0) {}
57
58 void dfs_comp(int u, int cid){
59 comp[u] = cid;
60 for (auto [v, id] : g.adj[u]){
61 if (comp[v] != -1) continue;
62 if (br.is_bridge[id]) continue; // don't cross bridges
63 dfs_comp(v, cid);
64 }
65 }
66
67 vector<vector<int>> build_tree(){
68 // Assign component ids
69 for (int u = 0; u < g.n; ++u) if (comp[u] == -1) dfs_comp(u, comps++);
70 // Tree has 'comps' nodes. Add edges for each bridge.
71 vector<vector<int>> tree(comps);
72 for (int id = 0; id < (int)g.edges.size(); ++id){
73 if (!br.is_bridge[id]) continue;
74 int u = g.edges[id].u;
75 int v = g.edges[id].v;
76 int a = comp[u], b = comp[v];
77 if (a == b) continue; // should not happen for a bridge
78 tree[a].push_back(b);
79 tree[b].push_back(a);
80 }
81 return tree;
82 }
83};
84
85int main(){
86 ios::sync_with_stdio(false);
87 cin.tie(nullptr);
88
89 int n, m; if (!(cin >> n >> m)) return 0;
90 Graph g(n);
91 for (int i = 0; i < m; ++i){ int u,v; cin >> u >> v; g.add_edge(u,v); }
92
93 Bridges br(g); br.run();
94 BridgeTreeBuilder builder(g, br);
95 auto tree = builder.build_tree();
96
97 // Output components and tree
98 cout << "Components (2-edge-connected):\n";
99 for (int u = 0; u < n; ++u) cout << u << ": " << builder.comp[u] << '\n';
100
101 cout << "Bridge Tree adjacency:\n";
102 for (int i = 0; i < (int)tree.size(); ++i){
103 cout << i << ":";
104 for (int v : tree[i]) cout << ' ' << v;
105 cout << '\n';
106 }
107 return 0;
108}
109

This code assigns 2-edge-connected components by ignoring edges marked as bridges. Contracting each component produces a forest (a tree per connected component). The result is the bridge tree, where queries like distances or DP over components become easier because trees have no cycles. Edges are tracked by IDs so we can precisely mark and skip bridges.

Time: O(V + E)Space: O(V + E)
Block-Cut Tree via Tarjan’s Biconnected Components
1#include <bits/stdc++.h>
2using namespace std;
3
4// Build the block-cut tree: articulation points (original vertex nodes) connected to
5// biconnected component nodes. The final tree is bipartite: [0..n-1] for original
6// vertices and [n..n+bcc_cnt-1] for block nodes.
7
8struct Graph {
9 int n; vector<vector<int>> adj;
10 Graph(int n): n(n), adj(n) {}
11 void add_edge(int u, int v){ adj[u].push_back(v); adj[v].push_back(u); }
12};
13
14struct BlockCutTree {
15 const Graph &g; int n;
16 vector<int> disc, low, parent;
17 int timer = 0;
18 vector<pair<int,int>> st; // stack of edges in current DFS path
19 vector<vector<int>> bcc; // list of biconnected components as vertex lists
20 vector<char> is_art;
21
22 BlockCutTree(const Graph &g): g(g), n(g.n), disc(n,-1), low(n,-1), parent(n,-1), is_art(n,0) {}
23
24 void pop_component(int u, int v){
25 // Pop edges until (u,v) is popped; collect unique vertices in this block
26 vector<int> verts;
27 unordered_set<int> seen;
28 while (!st.empty()){
29 auto [a,b] = st.back(); st.pop_back();
30 if (!seen.count(a)) { seen.insert(a); verts.push_back(a); }
31 if (!seen.count(b)) { seen.insert(b); verts.push_back(b); }
32 if ((a==u && b==v) || (a==v && b==u)) break;
33 }
34 bcc.push_back(move(verts));
35 }
36
37 void dfs(int u){
38 disc[u] = low[u] = timer++;
39 int child_count = 0;
40 for (int v : g.adj[u]){
41 if (v == parent[u]) continue;
42 if (disc[v] == -1){
43 parent[v] = u;
44 child_count++;
45 st.emplace_back(u, v);
46 dfs(v);
47 low[u] = min(low[u], low[v]);
48 // If v's subtree cannot reach u's ancestors, we found a block
49 if (low[v] >= disc[u]){
50 if (parent[u] != -1) is_art[u] = 1; // non-root AP rule
51 pop_component(u, v);
52 }
53 } else if (disc[v] < disc[u]){
54 // back edge to ancestor only (avoid pushing descendant -> ancestor twice)
55 st.emplace_back(u, v);
56 low[u] = min(low[u], disc[v]);
57 }
58 }
59 if (parent[u] == -1 && child_count >= 2) is_art[u] = 1; // root AP rule
60 }
61
62 // Returns: block-cut tree adjacency, size = n + bcc_cnt
63 vector<vector<int>> build(){
64 for (int u = 0; u < n; ++u) if (disc[u] == -1) dfs(u);
65 int bcc_cnt = (int)bcc.size();
66 vector<vector<int>> BCT(n + bcc_cnt);
67 for (int i = 0; i < bcc_cnt; ++i){
68 int block_node = n + i; // block node index
69 for (int v : bcc[i]){
70 // connect original vertex v to block node
71 BCT[v].push_back(block_node);
72 BCT[block_node].push_back(v);
73 }
74 }
75 return BCT;
76 }
77};
78
79int main(){
80 ios::sync_with_stdio(false);
81 cin.tie(nullptr);
82
83 int n, m; if (!(cin >> n >> m)) return 0;
84 Graph g(n);
85 for (int i = 0; i < m; ++i){ int u,v; cin >> u >> v; g.add_edge(u,v); }
86
87 BlockCutTree solver(g);
88 auto BCT = solver.build();
89
90 cout << "Articulation Points:\n";
91 for (int u = 0; u < n; ++u) if (solver.is_art[u]) cout << u << ' ';
92 cout << '\n';
93
94 cout << "Biconnected Components (as vertex lists):\n";
95 for (int i = 0; i < (int)solver.bcc.size(); ++i){
96 cout << "Block " << i << ":";
97 for (int v : solver.bcc[i]) cout << ' ' << v;
98 cout << '\n';
99 }
100
101 cout << "Block-Cut Tree adjacency (0.." << (int)BCT.size()-1 << "):\n";
102 for (int i = 0; i < (int)BCT.size(); ++i){
103 cout << i << ":";
104 for (int v : BCT[i]) cout << ' ' << v;
105 cout << '\n';
106 }
107 return 0;
108}
109

This implementation computes biconnected components by keeping a stack of edges during DFS. Whenever low[v] >= disc[u], we pop edges up to (u, v) to form a new block. Articulation points are identified by the same rule used for cut vertices. The block-cut tree is built by creating a node for each block and connecting it to all original vertices in that block.

Time: O(V + E)Space: O(V + E)