diff --git a/graph/tarjan.ts b/graph/tarjan.ts new file mode 100644 index 00000000..7f2a2454 --- /dev/null +++ b/graph/tarjan.ts @@ -0,0 +1,70 @@ +/** + * @function tarjan + * @description Given a graph, find the strongly connected components(SCC) in reverse topological order. A set of nodes form a SCC if there is a path between all pairs of points within that set. + * @Complexity_Analysis + * Time complexity: O(V + E). We perform a DFS of (V + E) + * Space Complexity: O(V). We hold numerous structures all of which at worst holds O(V) nodes. + * @param {[number, number][][]} graph - The graph in adjacency list form + * @return {number[][]} - An array of SCCs, where an SCC is an array with the indices of each node within that SCC. The order of SCCs in the array are in reverse topological order. + * @see https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm + */ +export const tarjan = (graph: number[][]): number[][] => { + if (graph.length === 0) { + return []; + } + + let index = 0; + // The order in which we discover nodes + let discovery: number[] = Array(graph.length); + // For each node, holds the furthest ancestor it can reach + let low: number[] = Array(graph.length).fill(undefined); + // Holds the nodes we have visited in a DFS traversal and are considering to group into a SCC + let stack: number[] = []; + // Holds the elements in the stack. + let stackContains = Array(graph.length).fill(false); + let sccs: number[][] = []; + + const dfs = (node: number) => { + discovery[node] = index; + low[node] = index; + ++index; + stack.push(node); + stackContains[node] = true; + + for (const child of graph[node]) { + if (low[child] === undefined) { + dfs(child); + if (low[child] < low[node]) { + // Child node loops back to this node's ancestor. Update the low node. + low[node] = low[child]; + } + } else if (stackContains[child] && low[node] > discovery[child]) { + // Found a backedge. Update the low for this node if needed. + low[node] = discovery[child]; + } + } + + if (discovery[node] == low[node]) { + // node is the root of a SCC. Gather the SCC's nodes from the stack. + let scc: number[] = []; + let i; + for (i = stack.length - 1; stack[i] != node; --i) { + scc.push(stack[i]); + stackContains[stack[i]] = false; + stack.pop(); + } + scc.push(stack[i]); + stackContains[stack[i]] = false; + stack.pop(); + sccs.push(scc); + } + } + + for (let i = 0; i < graph.length; ++i) { + if (low[i] === undefined) { + dfs(i); + } + } + return sccs; +} + diff --git a/graph/test/tarjan.test.ts b/graph/test/tarjan.test.ts new file mode 100644 index 00000000..f2eca081 --- /dev/null +++ b/graph/test/tarjan.test.ts @@ -0,0 +1,78 @@ +import { tarjan } from "../tarjan"; + +describe("tarjan", () => { + + it("it should return no sccs for empty graph", () => { + expect(tarjan([])).toStrictEqual([]); + }); + + it("it should return one scc for graph with one element", () => { + expect(tarjan([[]])).toStrictEqual([[0]]); + }); + + it("it should return one scc for graph with element that points to itself", () => { + expect(tarjan([[0]])).toStrictEqual([[0]]); + }); + + it("it should return one scc for two element graph with cycle", () => { + expect(tarjan([[1], [0]])).toStrictEqual([[1, 0]]); + }); + + it("should return one scc for each element for straight line", () => { + expect(tarjan([[1], [2], [3], []])).toStrictEqual([[3], [2], [1], [0]]); + }); + + it("should return sccs for straight line with backedge in middle", () => { + expect(tarjan([[1], [2], [3, 0], []])).toStrictEqual([[3], [2, 1, 0]]); + }); + + it("should return sccs for straight line with backedge from end to middle", () => { + expect(tarjan([[1], [2], [3], [1]])).toStrictEqual([[3, 2, 1], [0]]); + }); + + it("should return scc for each element for graph with no edges", () => { + expect(tarjan([[], [], [], []])).toStrictEqual([[0], [1], [2], [3]]); + }); + + it("should return sccs disconnected graph", () => { + expect(tarjan([[1, 2], [0, 2], [0, 1], []])).toStrictEqual([[2, 1, 0], [3]]); + }); + + it("should return sccs disconnected graph", () => { + expect(tarjan([[1, 2], [0, 2], [0, 1], [4], [5], [3]])).toStrictEqual([[2, 1, 0], [5, 4, 3]]); + }); + + it("should return single scc", () => { + expect(tarjan([[1], [2], [3], [0, 4], [3]])).toStrictEqual([[4, 3, 2, 1, 0]]); + }); + + it("should return one scc for complete connected graph", () => { + const input = [[1, 2, 3, 4], [0, 2, 3, 4], [0, 1, 3, 4], [0, 1, 2, 4], [0, 1, 2, 3]]; + expect(tarjan(input)).toStrictEqual([[4, 3, 2, 1, 0]]); + }); + + it("should return sccs", () => { + const input = [[1], [2], [0, 3], [4], []]; + expect(tarjan(input)).toStrictEqual([[4], [3], [2, 1, 0]]); + }); + + it("should return sccs", () => { + const input = [[1], [2], [0, 3, 4], [0], [5], [6, 7], [2, 4], [8], [5, 9], [5]]; + const expected = [[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]]; + expect(tarjan(input)).toStrictEqual(expected); + }); + + it("should return sccs", () => { + const input = [[1], [0, 2], [0, 3], [4], [5, 7], [6], [4, 7], []]; + const expected = [[7], [6, 5, 4], [3], [2, 1, 0]]; + expect(tarjan(input)).toStrictEqual(expected); + }); + + it("should return sccs where first scc cannot reach second scc", () => { + const input = [[1], [2], [0], [4], [5], [2, 3]]; + const expected = [[2, 1, 0], [5, 4, 3]]; + expect(tarjan(input)).toStrictEqual(expected); + }); + +}) +