Skip to content

Commit aaf8b59

Browse files
committed
feat: add tarjan algorithm for strongly connected components
1 parent 4acf117 commit aaf8b59

File tree

2 files changed

+149
-0
lines changed

2 files changed

+149
-0
lines changed

graph/tarjan.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/**
2+
* @function tarjan
3+
* @description Given a graph, find the strongly connected components(SCC). A set of nodes form a SCC if there is a path between all pairs of points within that set.
4+
* @Complexity_Analysis
5+
* Time complexity: O(V + E). We perform a DFS of (V + E)
6+
* Space Complexity: O(V). We hold numerous structures all of which at worst holds O(V) nodes.
7+
* @param {[number, number][][]} graph - The graph in adjacency list form
8+
* @return {number[][]} - An array of SCCs, where an SCC is an array with the indices of each node within that SCC.
9+
* @see https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
10+
*/
11+
export const tarjan = (graph: number[][]): number[][] => {
12+
const dfs = (node: number) => {
13+
discovery[node] = index;
14+
low[node] = index;
15+
++index;
16+
stack.push(node);
17+
stackContains[node] = true;
18+
19+
for (const child of graph[node]) {
20+
if (low[child] === -1) {
21+
dfs(child);
22+
if (low[child] < low[node]) {
23+
// Child node loops back to this node's ancestor. Update the low node.
24+
low[node] = low[child];
25+
}
26+
} else if (stackContains[child] && low[node] > discovery[child]) {
27+
// Found a backedge. Update the low for this node if needed.
28+
low[node] = discovery[child];
29+
}
30+
}
31+
32+
if (discovery[node] == low[node]) {
33+
// node is the root of a SCC. Gather the SCC's nodes from the stack.
34+
let scc: number[] = [];
35+
let i = stack.length - 1;
36+
while (stack[i] != node) {
37+
scc.push(stack[i]);
38+
stackContains[stack[i]] = false;
39+
stack.pop();
40+
--i;
41+
}
42+
scc.push(stack[i]);
43+
stack.pop();
44+
stackContains[stack[i]] = false;
45+
sccs.push(scc);
46+
}
47+
}
48+
49+
if (graph.length === 0) {
50+
return [];
51+
}
52+
53+
let index = 0;
54+
// The order in which we discover nodes
55+
let discovery: number[] = Array(graph.length).fill(-1);
56+
// For each node, holds the furthest ancestor it can reach
57+
let low: number[] = Array(graph.length).fill(-1);
58+
// Holds the nodes we have visited in a DFS traversal and are considering to group into a SCC
59+
let stack: number[] = [];
60+
// Holds the elements in the stack.
61+
let stackContains = Array(graph.length).fill(false);
62+
let sccs: number[][] = [];
63+
64+
for (let i = 0; i < graph.length; ++i) {
65+
if (low[i] === -1) {
66+
dfs(i);
67+
}
68+
}
69+
return sccs;
70+
}
71+

graph/test/tarjan.test.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { tarjan } from "../tarjan";
2+
3+
describe("tarjan", () => {
4+
5+
it("it should return no sccs for empty graph", () => {
6+
expect(tarjan([])).toStrictEqual([]);
7+
});
8+
9+
it("it should return one scc for graph with one element", () => {
10+
expect(tarjan([[]])).toStrictEqual([[0]]);
11+
});
12+
13+
it("it should return one scc for graph with element that points to itself", () => {
14+
expect(tarjan([[0]])).toStrictEqual([[0]]);
15+
});
16+
17+
it("it should return one scc for two element graph with cycle", () => {
18+
expect(tarjan([[1], [0]])).toStrictEqual([[1, 0]]);
19+
});
20+
21+
it("should return one scc for each element for straight line", () => {
22+
expect(tarjan([[1], [2], [3], []])).toStrictEqual([[3], [2], [1], [0]]);
23+
});
24+
25+
it("should return sccs for straight line with backedge in middle", () => {
26+
expect(tarjan([[1], [2], [3, 0], []])).toStrictEqual([[3], [2, 1, 0]]);
27+
});
28+
29+
it("should return sccs for straight line with backedge from end to middle", () => {
30+
expect(tarjan([[1], [2], [3], [1]])).toStrictEqual([[3, 2, 1], [0]]);
31+
});
32+
33+
it("should return scc for each element for graph with no edges", () => {
34+
expect(tarjan([[], [], [], []])).toStrictEqual([[0], [1], [2], [3]]);
35+
});
36+
37+
it("should return sccs disconnected graph", () => {
38+
expect(tarjan([[1, 2], [0, 2], [0, 1], []])).toStrictEqual([[2, 1, 0], [3]]);
39+
});
40+
41+
it("should return sccs disconnected graph", () => {
42+
expect(tarjan([[1, 2], [0, 2], [0, 1], [4], [5], [3]])).toStrictEqual([[2, 1, 0], [5, 4, 3]]);
43+
});
44+
45+
it("should return single scc", () => {
46+
expect(tarjan([[1], [2], [3], [0, 4], [3]])).toStrictEqual([[4, 3, 2, 1, 0]]);
47+
});
48+
49+
it("should return one scc for complete connected graph", () => {
50+
const input = [[1, 2, 3, 4], [0, 2, 3, 4], [0, 1, 3, 4], [0, 1, 2, 4], [0, 1, 2, 3]];
51+
expect(tarjan(input)).toStrictEqual([[4, 3, 2, 1, 0]]);
52+
});
53+
54+
it("should return sccs", () => {
55+
const input = [[1], [2], [0, 3], [4], []];
56+
expect(tarjan(input)).toStrictEqual([[4], [3], [2, 1, 0]]);
57+
});
58+
59+
it("should return sccs", () => {
60+
const input = [[1], [2], [0, 3, 4], [0], [5], [6, 7], [2, 4], [8], [5, 9], [5]];
61+
const expected = [[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]];
62+
expect(tarjan(input)).toStrictEqual(expected);
63+
});
64+
65+
it("should return sccs", () => {
66+
const input = [[1], [0, 2], [0, 3], [4], [5, 7], [6], [4, 7], []];
67+
const expected = [[7], [6, 5, 4], [3], [2, 1, 0]];
68+
expect(tarjan(input)).toStrictEqual(expected);
69+
});
70+
71+
it("should return sccs where first scc cannot reach second scc", () => {
72+
const input = [[1], [2], [0], [4], [5], [2, 3]];
73+
const expected = [[2, 1, 0], [5, 4, 3]];
74+
expect(tarjan(input)).toStrictEqual(expected);
75+
});
76+
77+
})
78+

0 commit comments

Comments
 (0)