Bridge Finding
An edge in an undirected connected graph is a bridge iff removing it disconnects the graph. A naive solution to finding bridges in a graph is to:
1.Delete an edge E
2.Perform DFS Exploration to check if the Graph is still connected
3.Restore Edge E. E is a bridge only if DFS exploration determines that the graph is disconnected without E.
A more efficient solution, which can find bridges in linear time, is to perform a DFS (depth-first-search) on the graph. At each step:
1. Number the vertex with a counter. The first vertex visited should be labelled 0, the second vertex labelled 1, etc.
2. Each vertex should also keep track of the lowest numbered vertex that can be reached with the DFS. This can be done recursively by looking at the smallest “low” of its children
3. If the lowest vertex that can be reached with the DFS is greater than its own label, that means the edge with its parent is a bridge. This is because the vertex cannot reach its parent with the DFS, implying that the edge is not part of a cycle.
Applications
- Finding vulnerabilities in Graphs and Electrical Circuits
Complexity
-
Time: worst Naive: )), Efficient: ) -
Space: worst )
References
Implementations
Python
-
1
# TODO: Implement in Python
JavaScript
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
console.log(`original array = [${D[0].join(', ')}]`); function pow(base, expo) { let ans = 1; for (let i = 0; i < expo; i++) { ans *= base; } return ans; } function digit(i, exp) { return parseInt(D[0][i] / pow(10, exp) % 10); } for (let exp = 0; exp < 3; exp++) { console.log(`Digit: ${exp}`); let i; for (i = 0; i < D[0].length; i++) { const d = digit(i, exp); D[2][d] += 1; } for (i = 1; i < 10; i++) { D[2][i] += D[2][i - 1]; } for (i = D[0].length - 1; i >= 0; i--) { const d = digit(i, exp); D[2][d] -= 1; D[1][D[2][d]] = D[0][i]; } for (i = 0; i < D[0].length; i++) { D[0][i] = D[1][i]; } for (i = 0; i < 10; i++) { D[2][i] = 0; } } console.log(`sorted array = [${D[0].join(', ')}]`);
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
console.log(`original array = [${D[0].join(', ')}]`); function mergeSort(start, end) { if (Math.abs(end - start) <= 1) return; let mergeFrom = 0; let mergeTo = 1; let width; let i; for (width = 1; width < end; width *= 2) { for (i = 0; i < end; i += 2 * width) { merge(mergeFrom, i, Math.min(i + width, end), Math.min(i + 2 * width, end), mergeTo); } // this could be copy(mergeTo, mergeFrom, start, end); // but it is more effecient to swap the input arrays // if you did copy here, you wouldn't need the copy at the end mergeFrom = (mergeFrom === 0 ? 1 : 0); mergeTo = 1 - mergeFrom; } if (mergeFrom !== 0) { copy(mergeFrom, mergeTo, start, end); } } function merge(mergeFrom, start, middle, end, mergeTo) { let i = start; let j = middle; let k; // in an actual merge implementation, mergeFrom and mergeTo would be arrays // here for the ability to trace what is going on better, the arrays are D[mergeFrom] and D[mergeTo] for (k = start; k < end; k++) { if (i < middle && (j >= end || D[mergeFrom][i] <= D[mergeFrom][j])) { D[mergeTo][k] = D[mergeFrom][i]; i += 1; } else { D[mergeTo][k] = D[mergeFrom][j]; j += 1; } } } function copy(mergeFrom, mergeTo, start, end) { let i; for (i = start; i < end; i++) { D[mergeTo][i] = D[mergeFrom][i]; } } mergeSort(0, D[0].length); console.log(`sorted array = [${D[0].join(', ')}]`);
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
console.log(`original array = [${D.join(', ')}]`); function mergeSort(start, end) { if (Math.abs(end - start) <= 1) return []; const middle = Math.ceil((start + end) / 2); mergeSort(start, middle); mergeSort(middle, end); console.log(`divide left[${start}, ${middle - 1}], right[${middle}, ${end - 1}]`); return mergeSort.merge(start, middle, end); } mergeSort.merge = (start, middle, end) => { const leftSize = middle - start; const rightSize = end - middle; const maxSize = Math.max(leftSize, rightSize); const size = end - start; const left = []; const right = []; let i; for (i = 0; i < maxSize; i++) { if (i < leftSize) { left.push(D[start + i]); } if (i < rightSize) { right.push(D[middle + i]); } } console.log(`left array = [${left.join(', ')}], ` + `right array = [${right.join(', ')}]`); i = 0; while (i < size) { if (left[0] && right[0]) { if (left[0] > right[0]) { D[start + i] = right.shift(); console.log(`rewrite from right array[${i}] = ${D[start + i]}`); } else { D[start + i] = left.shift(); console.log(`rewrite from left array[${i}] = ${D[start + i]}`); } } else if (left[0]) { D[start + i] = left.shift(); console.log(`rewrite from left array[${i}] = ${D[start + i]}`); } else { D[start + i] = right.shift(); console.log(`rewrite from right array[${i}] = ${D[start + i]}`); } i++; } const tempArray = []; for (i = start; i < end; i++) tempArray.push(D[i]); console.log(`merged array = [${tempArray.join(', ')}]`); }; mergeSort(0, D.length); console.log(`sorted array = [${D.join(', ')}]`);
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
function DFSExplore(graph, source) { const stack = [[source, null]]; const visited = []; let node; let prev; let i; let temp; for (i = 0; i < graph.length; i++) { visited.push(false); } while (stack.length > 0) { temp = stack.pop(); node = temp[0]; prev = temp[1]; if (!visited[node]) { visited[node] = true; for (i = 0; i < graph.length; i++) { if (graph[node][i]) { stack.push([i, node]); } } } } return visited; } const visited = DFSExplore(G, 0); let check = true; for (let i = 0; i < visited.length; i++) check &= visited[i]; if (check) { console.log('The Graph is CONNECTED'); } else { console.log('The Graph is NOT CONNECTED'); }
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
function DFS(node, parent, weight) { // node = current node, parent = previous node if (minWeight < weight) return; if (node === e) { if (minWeight > weight) { minWeight = weight; } return; } D[node] = true; // label current node as discovered for (let i = 0; i < G[node].length; i++) { if (G[node][i]) { // if the path from current node to the i-th node exists if (!D[i]) { // if the i-th node is not labeled as discovered DFS(i, node, weight + G[node][i]); // recursively call DFS } } } D[node] = false; // label current node as undiscovered } const s = Randomize.Integer({ min: 0, max: G.length - 1 }); // s = start node let e; // e = end node do { e = Randomize.Integer({ min: 0, max: G.length - 1 }); } while (s === e); const MAX_VALUE = Infinity; let minWeight = MAX_VALUE; console.log(`finding the shortest path from ${s} to ${e}`); let D = []; // D[i] indicates whether the i-th node is discovered or not for (let i = 0; i < G.length; i++) D.push(false); DFS(s, undefined, 0); if (minWeight === MAX_VALUE) { console.log(`there is no path from ${s} to ${e}`); } else { console.log(`the shortest path from ${s} to ${e} is ${minWeight}`); }
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
const G = [ // G[i][j] indicates whether the path from the i-th node to the j-th node exists or not [0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ]; function DFS(node, parent) { // node = current node, parent = previous node for (let i = 0; i < G[node].length; i++) { if (G[node][i]) { // if current node has the i-th node as a child DFS(i, node); // recursively call DFS } } } DFS(0);
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
let D; // D[i] indicates whether the i-th node is discovered or not function DFS(node, parent, weight) { // node = current node, parent = previous node D[node] = true; // label current node as discovered for (let i = 0; i < G[node].length; i++) { if (G[node][i]) { // if the edge from current node to the i-th node exists if (!D[i]) { // if the i-th node is not labeled as discovered DFS(i, node, weight + G[node][i]); // recursively call DFS } } } D[node] = false; // label current node as undiscovered } for (let i = 0; i < G.length; i++) { // start from every node console.log(`start from ${i}`); D = []; for (let j = 0; j < G.length; j++) D.push(false); DFS(i, undefined, 0); }
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127
const G = [ [0, 1, 0, 0, 1, 0], [1, 0, 0, 0, 1, 0], [0, 0, 0, 1, 0, 0], [0, 0, 1, 0, 1, 1], [1, 1, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0], ]; /* NOTE: Code assumes NO parallel edges */ let timer = 0; // adj keeps track of the neighbors of each node const bridges = []; const adj = []; const util = (u, disc, low, parent) => { // u is the node that is currently being processed in the DFS (depth-first search) // disc is the numbering of the vertices in the DFS, starting at 0 // low[v] is the lowest numbered vertex that can be reached from vertex v along the DFS // parent is the node that u came from // visited [u] = true; disc[u] = low[u] = timer++; console.log(`Nodes adjacent to ${u} are: [ ${adj[u]} ]`); /* adj [u].forEach (function (v) { graphTracer.visit (v, u).delay (); graphTracer.leave (v, u).delay (); }); */ const trace = (v) => { }; adj[u].forEach((v) => { if (disc[v] > -1 && v === parent) { trace(v); console.log(`${u}'s neighbor ${v} is u's parent. Not visiting it.`); } else if (disc[v] > -1 && v !== parent) { trace(v); console.log(`${u}'s neighbor ${v} is not u's parent. Comparing low[u] with disc[v]`); if (low[u] > disc[v]) { console.log(`low[${u}] is greater than disc[${v}]. Setting low[${u}] to disc[${v}]`); low[u] = disc[v]; } } if (disc[v] === -1) { trace(v); console.log(`${u}'s neighbor ${v} has not been visited yet`); console.log(`recursively calling util (${v}, [${disc}], [${low}],${u})`); util(v, disc, low, u); console.log('--------------------------------------------------------------------'); console.log(`Setting low [${u}] to ${Math.min(low[u], low[v])}`); low[u] = Math.min(low[u], low[v]); if (low[v] === disc[v]) { console.log(`low [${v}] === disc [${v}], low[${v}]=${low[v]}, disc[${v}]=${disc[v]}`); console.log(`${u} -> ${v} is a bridge. Adding ${u}->${v}to the set of bridges found`); bridges.push([u, v]); } } }); }; (function findBridges(graph) { const disc = filledArray(graph.length, -1); const low = filledArray(graph.length, -1); function filledArray(length, value) { return Array(...Array(length)).map(Number.prototype.valueOf, value); } // PRECOMPUTATION: store every node's neighbor info in auxiliary array for efficient retrieval later (function computeAdj() { graph.forEach((config) => { const temp = []; config.forEach((isEdge, i) => { isEdge && temp.push(i); }); adj.push(temp); }); }()); console.log(`Initializing: <b>disc</b>: ${disc} <b>low</b>: ${low}`); console.log(''); console.log('Beginning efficient Bridge Finding'); console.log('NOTE: call to util () follows pattern: util (nodeToVisit, disc, low, parent). See code for clarity'); console.log(''); console.log('Starting the main for loop (for each node)'); for (let v = 0; v < graph.length; v++) { if (disc[v] === -1) { console.log(`${v} has not been visited yet. Calling util (${v}, [${disc}], [${low}],${v}) from the for loop`); util(v, disc, low, v); console.log(`Returned in for loop after util (${v}, [${disc}], [${low}], [${v}])`); } } }(G)); console.log(`There are ${bridges.length} bridges in the Graph`); for (let i = 0; i < bridges.length; i++) { console.log(`${bridges[i][0]}-->${bridges[i][1]}`); } console.log('NOTE: All bridges are both ways (just like in the Naive Algorithm) because the Graph is undirected. So, edge A->B and B->A, both are bridges');
-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
const G = [ [0, 1, 0, 0, 0, 0], [1, 0, 0, 1, 1, 0], [0, 0, 0, 1, 0, 0], [0, 1, 1, 0, 1, 1], [0, 1, 0, 1, 0, 0], [0, 0, 0, 1, 0, 0], ]; // Depth First Search Exploration Algorithm to test connectedness of the Graph (see Graph algorithms/DFS/exploration), without the tracer & logger commands function DFSExplore(graph, source) { const stack = [[source, null]]; const visited = {}; let node; let prev; let i; let temp; while (stack.length > 0) { temp = stack.pop(); node = temp[0]; prev = temp[1]; if (!visited[node]) { visited[node] = true; for (i = 0; i < graph.length; i++) { if (graph[node][i]) { stack.push([i, node]); } } } } return visited; } function findBridges(graph) { let tempGraph; const bridges = []; let visited; for (let i = 0; i < graph.length; i++) { for (let j = 0; j < graph.length; j++) { if (graph[i][j]) { // check if an edge exists tempGraph = JSON.parse(JSON.stringify(graph)); tempGraph[i][j] = 0; tempGraph[j][i] = 0; visited = DFSExplore(tempGraph, 0); if (Object.keys(visited).length === graph.length) { console.log('Graph is CONNECTED. Edge is NOT a bridge'); } else { console.log('Graph is DISCONNECTED. Edge IS a bridge'); bridges.push([i, j]); } } } } return bridges; } const bridges = findBridges(G); console.log('The bridges are: '); for (const i in bridges) { console.log(`${bridges[i][0]} to ${bridges[i][1]}`); } console.log('NOTE: A bridge is both ways, i.e., from A to B and from B to A, because this is an Undirected Graph');