Graph robustness
Robustness asks how much damage a graph can take. Removal tests give the definitions of bridges and cut vertices, and the bridge tree tells how many edges must be added to survive any single link failure.
Learning goals
- Test bridges and cut vertices by removal and compare with low-link
- Count the edges needed to make a connected graph 2-edge-connected
- State what 2-edge-connected and biconnected mean and how they differ
Prerequisites
A network is robust if no single failure splits it. Before trusting a clever linear-time algorithm it helps to see the slow, obviously correct definition: break each link, one at a time, and count the pieces.
Bridge removal
The definition-level test is simple: count the connected components, remove one edge, count again. If the number rose, the edge is a bridge. Doing this for every edge costs O(m · (n + m)), against O(n + m) for low-link, but it is a reliable reference to test the fast version against.
Run the “Remove each edge: two cycles and a bridge” preset: of 7 edges only 2-3 raises the count from 1 to 2, the other six leave one component. On the “Remove each edge: disconnected” preset the graph already has 2 components; the path edges 0-1 and 1-2 each raise the count to 3, the triangle edges do not, so the answer is 2 bridges.
Count components with union-find, skipping the removed edge. Vertices are the same on both sides of the comparison, so only the count of components changes.
1static int find(int[] p, int x) { while (p[x] != x) { p[x] = p[p[x]]; x = p[x]; } return x; }23static int components(int n, int[][] edges, int skipEdge, int skipVertex) { // -1 means "skip nothing"4int[] p = new int[n];5for (int i = 0; i < n; i++) p[i] = i;6for (int k = 0; k < edges.length; k++) {7int a = edges[k][0], b = edges[k][1];8if (k == skipEdge || a == skipVertex || b == skipVertex) continue;9p[find(p, a)] = find(p, b);10}11int count = 0;12for (int v = 0; v < n; v++) if (v != skipVertex && find(p, v) == v) count++;13return count;14}1516static List<Integer> bridgesByRemoval(int n, int[][] edges) {17int base = components(n, edges, -1, -1);18List<Integer> out = new ArrayList<>();19for (int k = 0; k < edges.length; k++)20if (components(n, edges, k, -1) > base) out.add(k); // removing edge k splits a component21return out;22}
Robustness: remove an edge or vertex, and how many edges to add
Interactive · edit the inputundirected graph · 6 vertices · 7 edges
edges whose removal splits the graph
The graph starts with 1 component. Remove each edge in turn and count components again: if the count rises, that edge is a bridge. This brute force is O(m·(n+m)); the low-link DFS does it in O(n+m).
1static int components(int n, int[][] edges, int skipEdge, int skipVertex) { // -1 means "skip nothing"2int[] p = new int[n];3for (int i = 0; i < n; i++) p[i] = i;4for (int k = 0; k < edges.length; k++) {5int a = edges[k][0], b = edges[k][1];6if (k == skipEdge || a == skipVertex || b == skipVertex) continue;7p[find(p, a)] = find(p, b);8}9int count = 0;10for (int v = 0; v < n; v++) if (v != skipVertex && find(p, v) == v) count++;11return count;12}13static List<Integer> bridgesByRemoval(int n, int[][] edges) {14int base = components(n, edges, -1, -1);15List<Integer> out = new ArrayList<>();16for (int k = 0; k < edges.length; k++)17if (components(n, edges, k, -1) > base) out.add(k); // removing edge k splits a component18return out;19}20static List<Integer> articulationByRemoval(int n, int[][] edges) {21int base = components(n, edges, -1, -1);22List<Integer> out = new ArrayList<>();23for (int v = 0; v < n; v++)24if (components(n, edges, -1, v) > base) out.add(v); // removing vertex v splits its component25return out;26}27static int edgesToMakeBridgeless(int n, int[][] edges) { // connected graph: edges to add so no bridge remains28boolean[] isBridge = new boolean[edges.length];29for (int k : bridgesByRemoval(n, edges)) isBridge[k] = true;30int[] p = new int[n];31for (int i = 0; i < n; i++) p[i] = i;32for (int k = 0; k < edges.length; k++) if (!isBridge[k]) p[find(p, edges[k][0])] = find(p, edges[k][1]);33int[] degree = new int[n]; // degree in the bridge tree34for (int k = 0; k < edges.length; k++)35if (isBridge[k]) { degree[find(p, edges[k][0])]++; degree[find(p, edges[k][1])]++; }36int leaves = 0;37for (int v = 0; v < n; v++) if (find(p, v) == v && degree[v] == 1) leaves++;38return (leaves + 1) / 2; // every leaf needs a new edge; one edge serves two leaves39}
Live state
components
1
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L14 | components=1 | The graph starts with 1 component. Remove each edge in turn and count components again: if the count rises, that edge is a bridge. This brute force is O(m·(n+m)); the low-link DFS does it in O(n+m). |
| 2 | L17 | edge=0-1 components=1 base=1 | Remove 0-1: 1 component (was 1). The count did not rise: another route connects its ends. |
| 3 | L17 | edge=1-2 components=1 base=1 | Remove 1-2: 1 component (was 1). The count did not rise: another route connects its ends. |
| 4 | L17 | edge=2-0 components=1 base=1 | Remove 2-0: 1 component (was 1). The count did not rise: another route connects its ends. |
| 5 | L17 | edge=3-4 components=1 base=1 | Remove 3-4: 1 component (was 1). The count did not rise: another route connects its ends. |
| 6 | L17 | edge=4-5 components=1 base=1 | Remove 4-5: 1 component (was 1). The count did not rise: another route connects its ends. |
| 7 | L17 | edge=5-3 components=1 base=1 | Remove 5-3: 1 component (was 1). The count did not rise: another route connects its ends. |
| 8 | L17 | edge=2-3 components=2 base=1 | Remove 2-3: 2 components (was 1). The count rose: it is a bridge. |
| 9 | L18 | return=1 | 1 bridge: 2-3. |
Articulation removal
The same idea works for vertices: remove a vertex together with its edges, then count the components of the **remaining** vertices. If that count is higher than before, the vertex is an articulation point.
Be careful with isolated vertices: removing one leaves the others untouched and lowers the count by one, so it is not an articulation point. In the “Remove each vertex: bowtie” preset, removing vertex 2 leaves {0, 1} and {3, 4}: 2 components against 1 before. Every other vertex leaves one component.
Use this brute force on small inputs as an oracle; for the real graph use the DFS of the previous lesson.
1static List<Integer> articulationByRemoval(int n, int[][] edges) {2int base = components(n, edges, -1, -1);3List<Integer> out = new ArrayList<>();4for (int v = 0; v < n; v++)5if (components(n, edges, -1, v) > base) out.add(v); // removing v splits its component6return out;7}
Robustness: remove an edge or vertex, and how many edges to add
Interactive · edit the inputundirected graph · 6 vertices · 7 edges
edges whose removal splits the graph
The graph starts with 1 component. Remove each edge in turn and count components again: if the count rises, that edge is a bridge. This brute force is O(m·(n+m)); the low-link DFS does it in O(n+m).
1static int components(int n, int[][] edges, int skipEdge, int skipVertex) { // -1 means "skip nothing"2int[] p = new int[n];3for (int i = 0; i < n; i++) p[i] = i;4for (int k = 0; k < edges.length; k++) {5int a = edges[k][0], b = edges[k][1];6if (k == skipEdge || a == skipVertex || b == skipVertex) continue;7p[find(p, a)] = find(p, b);8}9int count = 0;10for (int v = 0; v < n; v++) if (v != skipVertex && find(p, v) == v) count++;11return count;12}13static List<Integer> bridgesByRemoval(int n, int[][] edges) {14int base = components(n, edges, -1, -1);15List<Integer> out = new ArrayList<>();16for (int k = 0; k < edges.length; k++)17if (components(n, edges, k, -1) > base) out.add(k); // removing edge k splits a component18return out;19}20static List<Integer> articulationByRemoval(int n, int[][] edges) {21int base = components(n, edges, -1, -1);22List<Integer> out = new ArrayList<>();23for (int v = 0; v < n; v++)24if (components(n, edges, -1, v) > base) out.add(v); // removing vertex v splits its component25return out;26}27static int edgesToMakeBridgeless(int n, int[][] edges) { // connected graph: edges to add so no bridge remains28boolean[] isBridge = new boolean[edges.length];29for (int k : bridgesByRemoval(n, edges)) isBridge[k] = true;30int[] p = new int[n];31for (int i = 0; i < n; i++) p[i] = i;32for (int k = 0; k < edges.length; k++) if (!isBridge[k]) p[find(p, edges[k][0])] = find(p, edges[k][1]);33int[] degree = new int[n]; // degree in the bridge tree34for (int k = 0; k < edges.length; k++)35if (isBridge[k]) { degree[find(p, edges[k][0])]++; degree[find(p, edges[k][1])]++; }36int leaves = 0;37for (int v = 0; v < n; v++) if (find(p, v) == v && degree[v] == 1) leaves++;38return (leaves + 1) / 2; // every leaf needs a new edge; one edge serves two leaves39}
Live state
components
1
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L14 | components=1 | The graph starts with 1 component. Remove each edge in turn and count components again: if the count rises, that edge is a bridge. This brute force is O(m·(n+m)); the low-link DFS does it in O(n+m). |
| 2 | L17 | edge=0-1 components=1 base=1 | Remove 0-1: 1 component (was 1). The count did not rise: another route connects its ends. |
| 3 | L17 | edge=1-2 components=1 base=1 | Remove 1-2: 1 component (was 1). The count did not rise: another route connects its ends. |
| 4 | L17 | edge=2-0 components=1 base=1 | Remove 2-0: 1 component (was 1). The count did not rise: another route connects its ends. |
| 5 | L17 | edge=3-4 components=1 base=1 | Remove 3-4: 1 component (was 1). The count did not rise: another route connects its ends. |
| 6 | L17 | edge=4-5 components=1 base=1 | Remove 4-5: 1 component (was 1). The count did not rise: another route connects its ends. |
| 7 | L17 | edge=5-3 components=1 base=1 | Remove 5-3: 1 component (was 1). The count did not rise: another route connects its ends. |
| 8 | L17 | edge=2-3 components=2 base=1 | Remove 2-3: 2 components (was 1). The count rose: it is a bridge. |
| 9 | L18 | return=1 | 1 bridge: 2-3. |
Connectivity augmentation
**Augmentation**: given a connected graph, what is the fewest number of edges to add so that no bridge remains (the graph becomes 2-edge-connected)? Contract the 2-edge-connected components into the bridge tree. A **leaf** of that tree is a component with exactly one bridge; unless a new edge ends inside it, that bridge stays. One new edge can touch at most two leaves, so at least ⌈leaves / 2⌉ edges are needed, and that many always suffice.
The answer is (leaves + 1) / 2 in integer arithmetic, and 0 when the bridge tree is a single point. The “Add edges: a path needs one” preset (0-1 1-2 2-3) has 2 leaves and needs 1 edge, closing the path into a cycle. The “Add edges: a star with four leaves” preset has 4 leaves and needs 2. The “Add edges: already 2-edge-connected” preset is a cycle and needs 0.
The sufficiency claim is a theorem (Eswaran and Tarjan); the exhaustive search in this course's test suite confirms the formula on every small graph tried. The animation reports the count, not the exact edges to add.
1static int edgesToMakeBridgeless(int n, int[][] edges) { // connected graph: edges to add so that no bridge remains2boolean[] isBridge = new boolean[edges.length];3for (int k : bridgesByRemoval(n, edges)) isBridge[k] = true;4int[] p = new int[n];5for (int i = 0; i < n; i++) p[i] = i;6for (int k = 0; k < edges.length; k++)7if (!isBridge[k]) p[find(p, edges[k][0])] = find(p, edges[k][1]); // contract the 2-edge-connected components8int[] degree = new int[n]; // degree in the bridge tree9for (int k = 0; k < edges.length; k++)10if (isBridge[k]) { degree[find(p, edges[k][0])]++; degree[find(p, edges[k][1])]++; }11int leaves = 0;12for (int v = 0; v < n; v++) if (find(p, v) == v && degree[v] == 1) leaves++;13return (leaves + 1) / 2; // every leaf needs a new edge; one edge serves two leaves14}
Robustness: remove an edge or vertex, and how many edges to add
Interactive · edit the inputundirected graph · 6 vertices · 7 edges
edges whose removal splits the graph
The graph starts with 1 component. Remove each edge in turn and count components again: if the count rises, that edge is a bridge. This brute force is O(m·(n+m)); the low-link DFS does it in O(n+m).
1static int components(int n, int[][] edges, int skipEdge, int skipVertex) { // -1 means "skip nothing"2int[] p = new int[n];3for (int i = 0; i < n; i++) p[i] = i;4for (int k = 0; k < edges.length; k++) {5int a = edges[k][0], b = edges[k][1];6if (k == skipEdge || a == skipVertex || b == skipVertex) continue;7p[find(p, a)] = find(p, b);8}9int count = 0;10for (int v = 0; v < n; v++) if (v != skipVertex && find(p, v) == v) count++;11return count;12}13static List<Integer> bridgesByRemoval(int n, int[][] edges) {14int base = components(n, edges, -1, -1);15List<Integer> out = new ArrayList<>();16for (int k = 0; k < edges.length; k++)17if (components(n, edges, k, -1) > base) out.add(k); // removing edge k splits a component18return out;19}20static List<Integer> articulationByRemoval(int n, int[][] edges) {21int base = components(n, edges, -1, -1);22List<Integer> out = new ArrayList<>();23for (int v = 0; v < n; v++)24if (components(n, edges, -1, v) > base) out.add(v); // removing vertex v splits its component25return out;26}27static int edgesToMakeBridgeless(int n, int[][] edges) { // connected graph: edges to add so no bridge remains28boolean[] isBridge = new boolean[edges.length];29for (int k : bridgesByRemoval(n, edges)) isBridge[k] = true;30int[] p = new int[n];31for (int i = 0; i < n; i++) p[i] = i;32for (int k = 0; k < edges.length; k++) if (!isBridge[k]) p[find(p, edges[k][0])] = find(p, edges[k][1]);33int[] degree = new int[n]; // degree in the bridge tree34for (int k = 0; k < edges.length; k++)35if (isBridge[k]) { degree[find(p, edges[k][0])]++; degree[find(p, edges[k][1])]++; }36int leaves = 0;37for (int v = 0; v < n; v++) if (find(p, v) == v && degree[v] == 1) leaves++;38return (leaves + 1) / 2; // every leaf needs a new edge; one edge serves two leaves39}
Live state
components
1
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L14 | components=1 | The graph starts with 1 component. Remove each edge in turn and count components again: if the count rises, that edge is a bridge. This brute force is O(m·(n+m)); the low-link DFS does it in O(n+m). |
| 2 | L17 | edge=0-1 components=1 base=1 | Remove 0-1: 1 component (was 1). The count did not rise: another route connects its ends. |
| 3 | L17 | edge=1-2 components=1 base=1 | Remove 1-2: 1 component (was 1). The count did not rise: another route connects its ends. |
| 4 | L17 | edge=2-0 components=1 base=1 | Remove 2-0: 1 component (was 1). The count did not rise: another route connects its ends. |
| 5 | L17 | edge=3-4 components=1 base=1 | Remove 3-4: 1 component (was 1). The count did not rise: another route connects its ends. |
| 6 | L17 | edge=4-5 components=1 base=1 | Remove 4-5: 1 component (was 1). The count did not rise: another route connects its ends. |
| 7 | L17 | edge=5-3 components=1 base=1 | Remove 5-3: 1 component (was 1). The count did not rise: another route connects its ends. |
| 8 | L17 | edge=2-3 components=2 base=1 | Remove 2-3: 2 components (was 1). The count rose: it is a bridge. |
| 9 | L18 | return=1 | 1 bridge: 2-3. |
Edge-biconnectivity intro
Two notions of “survives one failure” are easy to confuse. A graph is **2-edge-connected** if it is connected and has no bridge: no single edge failure disconnects it. It is **biconnected** (2-vertex-connected) if it is connected, has at least three vertices, and has no cut vertex: no single vertex failure disconnects it.
Every biconnected graph is 2-edge-connected, but not the other way round. The bowtie (two triangles sharing vertex 2) has no bridge, so it is 2-edge-connected, but vertex 2 is a cut vertex, so it is not biconnected. Both properties have a clean characterisation (Whitney and Menger): a graph is 2-edge-connected exactly when every pair of vertices has two edge-disjoint paths, and biconnected exactly when every pair has two internally vertex-disjoint paths.
The code checks both properties with the removal helpers. Higher levels (3-edge-connected, k-vertex-connected) need max-flow ideas, which is where the next chapter, Network Flow and Minimum Cuts, comes in.
| Property | Test | Survives | Example |
|---|---|---|---|
| 2-edge-connected | connected and no bridge | any one edge failing | a cycle; the bowtie |
| Biconnected | connected, 3+ vertices, no cut vertex | any one vertex failing | a cycle; not the bowtie |
| Tree | connected, every edge a bridge | nothing | a path |
1static boolean isTwoEdgeConnected(int n, int[][] edges) {2return components(n, edges, -1, -1) == 1 && bridgesByRemoval(n, edges).isEmpty();3}45static boolean isBiconnected(int n, int[][] edges) {6return n >= 3 && components(n, edges, -1, -1) == 1 && articulationByRemoval(n, edges).isEmpty();7}
Robustness: remove an edge or vertex, and how many edges to add
Interactive · edit the inputundirected graph · 6 vertices · 7 edges
edges whose removal splits the graph
The graph starts with 1 component. Remove each edge in turn and count components again: if the count rises, that edge is a bridge. This brute force is O(m·(n+m)); the low-link DFS does it in O(n+m).
1static int components(int n, int[][] edges, int skipEdge, int skipVertex) { // -1 means "skip nothing"2int[] p = new int[n];3for (int i = 0; i < n; i++) p[i] = i;4for (int k = 0; k < edges.length; k++) {5int a = edges[k][0], b = edges[k][1];6if (k == skipEdge || a == skipVertex || b == skipVertex) continue;7p[find(p, a)] = find(p, b);8}9int count = 0;10for (int v = 0; v < n; v++) if (v != skipVertex && find(p, v) == v) count++;11return count;12}13static List<Integer> bridgesByRemoval(int n, int[][] edges) {14int base = components(n, edges, -1, -1);15List<Integer> out = new ArrayList<>();16for (int k = 0; k < edges.length; k++)17if (components(n, edges, k, -1) > base) out.add(k); // removing edge k splits a component18return out;19}20static List<Integer> articulationByRemoval(int n, int[][] edges) {21int base = components(n, edges, -1, -1);22List<Integer> out = new ArrayList<>();23for (int v = 0; v < n; v++)24if (components(n, edges, -1, v) > base) out.add(v); // removing vertex v splits its component25return out;26}27static int edgesToMakeBridgeless(int n, int[][] edges) { // connected graph: edges to add so no bridge remains28boolean[] isBridge = new boolean[edges.length];29for (int k : bridgesByRemoval(n, edges)) isBridge[k] = true;30int[] p = new int[n];31for (int i = 0; i < n; i++) p[i] = i;32for (int k = 0; k < edges.length; k++) if (!isBridge[k]) p[find(p, edges[k][0])] = find(p, edges[k][1]);33int[] degree = new int[n]; // degree in the bridge tree34for (int k = 0; k < edges.length; k++)35if (isBridge[k]) { degree[find(p, edges[k][0])]++; degree[find(p, edges[k][1])]++; }36int leaves = 0;37for (int v = 0; v < n; v++) if (find(p, v) == v && degree[v] == 1) leaves++;38return (leaves + 1) / 2; // every leaf needs a new edge; one edge serves two leaves39}
Live state
components
1
Execution trace (dry run table)Show 9 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L14 | components=1 | The graph starts with 1 component. Remove each edge in turn and count components again: if the count rises, that edge is a bridge. This brute force is O(m·(n+m)); the low-link DFS does it in O(n+m). |
| 2 | L17 | edge=0-1 components=1 base=1 | Remove 0-1: 1 component (was 1). The count did not rise: another route connects its ends. |
| 3 | L17 | edge=1-2 components=1 base=1 | Remove 1-2: 1 component (was 1). The count did not rise: another route connects its ends. |
| 4 | L17 | edge=2-0 components=1 base=1 | Remove 2-0: 1 component (was 1). The count did not rise: another route connects its ends. |
| 5 | L17 | edge=3-4 components=1 base=1 | Remove 3-4: 1 component (was 1). The count did not rise: another route connects its ends. |
| 6 | L17 | edge=4-5 components=1 base=1 | Remove 4-5: 1 component (was 1). The count did not rise: another route connects its ends. |
| 7 | L17 | edge=5-3 components=1 base=1 | Remove 5-3: 1 component (was 1). The count did not rise: another route connects its ends. |
| 8 | L17 | edge=2-3 components=2 base=1 | Remove 2-3: 2 components (was 1). The count rose: it is a bridge. |
| 9 | L18 | return=1 | 1 bridge: 2-3. |
Practice ladder
Now try it yourself
Easy
- Find if Path Exists in Graph
Medium
- Critical Connections in a Network
- Number of Operations to Make Network Connected
Advanced
- Minimum Number of Days to Disconnect Island
- Remove Max Number of Edges to Keep Graph Fully Traversable
Key takeaways
- Removal tests are the definitions of bridges and cut vertices and make a good oracle for the DFS algorithms.
- To make a connected graph 2-edge-connected add ⌈leaves / 2⌉ edges, where leaves are leaves of the bridge tree.
- Biconnected implies 2-edge-connected, not the reverse.