Skip to content

Commit 258e2bc

Browse files
committed
Added: Tarjan's SCC algorithm and test cases
1 parent 1d252d7 commit 258e2bc

File tree

2 files changed

+181
-0
lines changed

2 files changed

+181
-0
lines changed

Diff for: Graphs/TarjanSCC.js

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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 }

Diff for: Graphs/test/TarjanSCC.test.js

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { TarjanSCC } from '../TarjanSCC.js'
2+
3+
test('Test Case 1 - Simple graph with two SCCs', () => {
4+
const graph = {
5+
0: [1],
6+
1: [2],
7+
2: [0, 3],
8+
3: [4],
9+
4: []
10+
}
11+
const result = TarjanSCC(graph)
12+
13+
// Sort the components before comparison since order doesn't matter
14+
const expected = [[4], [3], [0, 2, 1]].map((comp) => comp.sort())
15+
const actual = result.map((comp) => comp.sort())
16+
17+
expect(actual).toEqual(expect.arrayContaining(expected))
18+
})
19+
20+
test('Test Case 2 - All nodes in one SCC', () => {
21+
const graph = {
22+
A: ['B'],
23+
B: ['C'],
24+
C: ['A']
25+
}
26+
27+
const result = TarjanSCC(graph)
28+
29+
// Sort the components before comparison since order doesn't matter
30+
const expected = [['A', 'B', 'C']].map((comp) => comp.sort())
31+
const actual = result.map((comp) => comp.sort())
32+
33+
expect(actual).toEqual(expect.arrayContaining(expected))
34+
})
35+
36+
test('Test Case 3 - Disconnected nodes', () => {
37+
const graph = {
38+
1: [],
39+
2: [],
40+
3: []
41+
}
42+
43+
const result = TarjanSCC(graph)
44+
45+
// Sort the components before comparison since order doesn't matter
46+
const expected = [[1], [2], [3]].map((comp) => comp.sort())
47+
const actual = result.map((comp) => comp.sort())
48+
49+
expect(actual).toEqual(expect.arrayContaining(expected))
50+
})
51+
52+
test('Test Case 4 - Complex Graph', () => {
53+
const graph = {
54+
0: [1],
55+
1: [2, 3],
56+
2: [0],
57+
3: [4],
58+
4: [5],
59+
5: [3]
60+
}
61+
62+
const result = TarjanSCC(graph)
63+
64+
// Sort the components before comparison since order doesn't matter
65+
const expected = [
66+
[0, 2, 1],
67+
[3, 5, 4]
68+
].map((comp) => comp.sort())
69+
const actual = result.map((comp) => comp.sort())
70+
71+
expect(actual).toEqual(expect.arrayContaining(expected))
72+
})

0 commit comments

Comments
 (0)