diff --git a/DIRECTORY.md b/DIRECTORY.md index 5e8e1f401a..dec9f2c4e8 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -163,6 +163,7 @@ * [NodeNeighbors](Graphs/NodeNeighbors.js) * [NumberOfIslands](Graphs/NumberOfIslands.js) * [PrimMST](Graphs/PrimMST.js) + * [TarjanSCC](Graphs/TarjanSCC.js) * **Hashes** * [MD5](Hashes/MD5.js) * [SHA1](Hashes/SHA1.js) diff --git a/Graphs/TarjanSCC.js b/Graphs/TarjanSCC.js new file mode 100644 index 0000000000..af2f3b3bae --- /dev/null +++ b/Graphs/TarjanSCC.js @@ -0,0 +1,109 @@ +/* +Tarjan's Algorithm to find all Strongly Connected Components (SCCs) in a directed graph. +It performs a DFS traversal while keeping track of the discovery and low-link values +to identify root nodes of SCCs. + +Complexity: + Time: O(V + E), where V: vertices and E: edges. + Space: O(V), for stack | discovery arrays | result. + +Reference: + https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm + https://cp-algorithms.com/graph/strongly-connected-components.html +*/ + +/** + * Finds all strongly connected components in a directed graph using Tarjan's algorithm. + * + * @param {Object} graph - Directed graph represented as an adjacency list. + * @returns {Array>} - List of strongly connected components (each SCC is a list of nodes). + * @throws {Error} If the input graph is invalid or empty + */ +function TarjanSCC(graph) { + // Input validation + if (!graph || typeof graph !== 'object' || Array.isArray(graph)) { + throw new Error( + 'Graph must be a non-null object representing an adjacency list' + ) + } + + if (Object.keys(graph).length === 0) { + return [] + } + + const ids = {} // Discovery time of each node + const low = {} // Lowest discovery time reachable from the node + const onStack = {} // To track if a node is on the recursion stack + const stack = [] // Stack to hold the current path + const result = [] // Array to store SCCs + let time = 0 // Global timer for discovery time + + /** + * Convert node to its proper type (number if numeric string, otherwise string) + * @param {string|number} node + * @returns {string|number} + */ + function convertNode(node) { + return !isNaN(node) && String(Number(node)) === String(node) + ? Number(node) + : node + } + + /** + * Recursive DFS function to explore the graph and find SCCs + * @param {string|number} node + */ + function dfs(node) { + if (!(node in graph)) { + throw new Error(`Node ${node} not found in graph`) + } + + ids[node] = low[node] = time++ + stack.push(node) + onStack[node] = true + + // Explore all neighbours + const neighbors = graph[node] + if (!Array.isArray(neighbors)) { + throw new Error(`Neighbors of node ${node} must be an array`) + } + + for (const neighbor of neighbors) { + const convertedNeighbor = convertNode(neighbor) + if (!(convertedNeighbor in ids)) { + dfs(convertedNeighbor) + low[node] = Math.min(low[node], low[convertedNeighbor]) + } else if (onStack[convertedNeighbor]) { + low[node] = Math.min(low[node], ids[convertedNeighbor]) + } + } + + // If the current node is the root of an SCC + if (ids[node] === low[node]) { + const scc = [] + let current + do { + current = stack.pop() + onStack[current] = false + scc.push(convertNode(current)) + } while (current !== node) + result.push(scc) + } + } + + // Run DFS for all unvisited nodes + try { + for (const node in graph) { + const convertedNode = convertNode(node) + if (!(convertedNode in ids)) { + dfs(convertedNode) + } + } + } catch (error) { + throw new Error(`Error during graph traversal: ${error.message}`) + } + + return result +} + +export { TarjanSCC } diff --git a/Graphs/test/TarjanSCC.test.js b/Graphs/test/TarjanSCC.test.js new file mode 100644 index 0000000000..68b05b8b3d --- /dev/null +++ b/Graphs/test/TarjanSCC.test.js @@ -0,0 +1,92 @@ +import { TarjanSCC } from '../TarjanSCC.js' + +test('Test Case 1 - Simple graph with two SCCs', () => { + const graph = { + 0: [1], + 1: [2], + 2: [0, 3], + 3: [4], + 4: [] + } + const result = TarjanSCC(graph) + + // Sort the components before comparison since order doesn't matter + const expected = [[4], [3], [0, 2, 1]].map((comp) => comp.sort()) + const actual = result.map((comp) => comp.sort()) + + expect(actual).toEqual(expect.arrayContaining(expected)) +}) + +test('Test Case 2 - All nodes in one SCC', () => { + const graph = { + A: ['B'], + B: ['C'], + C: ['A'] + } + + const result = TarjanSCC(graph) + + // Sort the components before comparison since order doesn't matter + const expected = [['A', 'B', 'C']].map((comp) => comp.sort()) + const actual = result.map((comp) => comp.sort()) + + expect(actual).toEqual(expect.arrayContaining(expected)) +}) + +test('Test Case 3 - Disconnected nodes', () => { + const graph = { + 1: [], + 2: [], + 3: [] + } + + const result = TarjanSCC(graph) + + // Sort the components before comparison since order doesn't matter + const expected = [[1], [2], [3]].map((comp) => comp.sort()) + const actual = result.map((comp) => comp.sort()) + + expect(actual).toEqual(expect.arrayContaining(expected)) +}) + +test('Test Case 4 - Complex Graph', () => { + const graph = { + 0: [1], + 1: [2, 3], + 2: [0], + 3: [4], + 4: [5], + 5: [3] + } + + const result = TarjanSCC(graph) + + // Sort the components before comparison since order doesn't matter + const expected = [ + [0, 2, 1], + [3, 5, 4] + ].map((comp) => comp.sort()) + const actual = result.map((comp) => comp.sort()) + + expect(actual).toEqual(expect.arrayContaining(expected)) +}) + +test('Edge Case - Null input should throw error', () => { + expect(() => TarjanSCC(null)).toThrow( + 'Graph must be a non-null object representing an adjacency list' + ) +}) + +test('Edge Case - Node with non-array neighbors should throw error', () => { + const graph = { + A: 'not-an-array' + } + expect(() => TarjanSCC(graph)).toThrow('Neighbors of node A must be an array') +}) + +test('Edge Case - Neighbor not in graph should throw error', () => { + const graph = { + A: ['B'] + } + expect(() => TarjanSCC(graph)).toThrow('Node B not found in graph') +})