|
| 1 | +/* |
| 2 | +Tarjan's Algorithm to find all Strongly Connected Components (SCCs) in a directed graph. |
| 3 | +It performs a DFS traversal while keeping track of the discovery and low-link values |
| 4 | +to identify root nodes of SCCs. |
| 5 | +
|
| 6 | +Complexity: |
| 7 | + Time: O(V + E), where V: vertices and E: edges. |
| 8 | + Space: O(V), for stack | discovery arrays | result. |
| 9 | +
|
| 10 | +Reference: |
| 11 | + https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm |
| 12 | + https://cp-algorithms.com/graph/strongly-connected-components.html |
| 13 | +*/ |
| 14 | + |
| 15 | +/** |
| 16 | + * Finds all strongly connected components in a directed graph using Tarjan's algorithm. |
| 17 | + * |
| 18 | + * @param {Object} graph - Directed graph represented as an adjacency list. |
| 19 | + * @returns {Array<Array<string|number>>} - List of strongly connected components (each SCC is a list of nodes). |
| 20 | + * @throws {Error} If the input graph is invalid or empty |
| 21 | + */ |
| 22 | +function TarjanSCC(graph) { |
| 23 | + // Input validation |
| 24 | + if (!graph || typeof graph !== 'object' || Array.isArray(graph)) { |
| 25 | + throw new Error( |
| 26 | + 'Graph must be a non-null object representing an adjacency list' |
| 27 | + ) |
| 28 | + } |
| 29 | + |
| 30 | + if (Object.keys(graph).length === 0) { |
| 31 | + return [] |
| 32 | + } |
| 33 | + |
| 34 | + const ids = {} // Discovery time of each node |
| 35 | + const low = {} // Lowest discovery time reachable from the node |
| 36 | + const onStack = {} // To track if a node is on the recursion stack |
| 37 | + const stack = [] // Stack to hold the current path |
| 38 | + const result = [] // Array to store SCCs |
| 39 | + let time = 0 // Global timer for discovery time |
| 40 | + |
| 41 | + /** |
| 42 | + * Convert node to its proper type (number if numeric string, otherwise string) |
| 43 | + * @param {string|number} node |
| 44 | + * @returns {string|number} |
| 45 | + */ |
| 46 | + function convertNode(node) { |
| 47 | + return !isNaN(node) && String(Number(node)) === String(node) |
| 48 | + ? Number(node) |
| 49 | + : node |
| 50 | + } |
| 51 | + |
| 52 | + /** |
| 53 | + * Recursive DFS function to explore the graph and find SCCs |
| 54 | + * @param {string|number} node |
| 55 | + */ |
| 56 | + function dfs(node) { |
| 57 | + if (!(node in graph)) { |
| 58 | + throw new Error(`Node ${node} not found in graph`) |
| 59 | + } |
| 60 | + |
| 61 | + ids[node] = low[node] = time++ |
| 62 | + stack.push(node) |
| 63 | + onStack[node] = true |
| 64 | + |
| 65 | + // Explore all neighbours |
| 66 | + const neighbors = graph[node] |
| 67 | + if (!Array.isArray(neighbors)) { |
| 68 | + throw new Error(`Neighbors of node ${node} must be an array`) |
| 69 | + } |
| 70 | + |
| 71 | + for (const neighbor of neighbors) { |
| 72 | + const convertedNeighbor = convertNode(neighbor) |
| 73 | + if (!(convertedNeighbor in ids)) { |
| 74 | + dfs(convertedNeighbor) |
| 75 | + low[node] = Math.min(low[node], low[convertedNeighbor]) |
| 76 | + } else if (onStack[convertedNeighbor]) { |
| 77 | + low[node] = Math.min(low[node], ids[convertedNeighbor]) |
| 78 | + } |
| 79 | + } |
| 80 | + |
| 81 | + // If the current node is the root of an SCC |
| 82 | + if (ids[node] === low[node]) { |
| 83 | + const scc = [] |
| 84 | + let current |
| 85 | + do { |
| 86 | + current = stack.pop() |
| 87 | + onStack[current] = false |
| 88 | + scc.push(convertNode(current)) |
| 89 | + } while (current !== node) |
| 90 | + result.push(scc) |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + // Run DFS for all unvisited nodes |
| 95 | + try { |
| 96 | + for (const node in graph) { |
| 97 | + const convertedNode = convertNode(node) |
| 98 | + if (!(convertedNode in ids)) { |
| 99 | + dfs(convertedNode) |
| 100 | + } |
| 101 | + } |
| 102 | + } catch (error) { |
| 103 | + throw new Error(`Error during graph traversal: ${error.message}`) |
| 104 | + } |
| 105 | + |
| 106 | + return result |
| 107 | +} |
| 108 | + |
| 109 | +export { TarjanSCC } |
0 commit comments