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

Graphs/TarjanSCC.js

Lines changed: 109 additions & 0 deletions
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 }

Graphs/test/TarjanSCC.test.js

Lines changed: 72 additions & 0 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy