From aaf8b599c333de05f220eeabe5db7bea7c29e11c Mon Sep 17 00:00:00 2001 From: Joshua Cao Date: Sat, 30 Dec 2023 15:03:06 -0800 Subject: [PATCH 1/2] feat: add tarjan algorithm for strongly connected components --- graph/tarjan.ts | 71 +++++++++++++++++++++++++++++++++++ graph/test/tarjan.test.ts | 78 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 graph/tarjan.ts create mode 100644 graph/test/tarjan.test.ts diff --git a/graph/tarjan.ts b/graph/tarjan.ts new file mode 100644 index 00000000..da16d7c6 --- /dev/null +++ b/graph/tarjan.ts @@ -0,0 +1,71 @@ +/** + * @function tarjan + * @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. + * @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. + * @see https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm + */ +export const tarjan = (graph: number[][]): 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] === -1) { + 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 = stack.length - 1; + while (stack[i] != node) { + scc.push(stack[i]); + stackContains[stack[i]] = false; + stack.pop(); + --i; + } + scc.push(stack[i]); + stack.pop(); + stackContains[stack[i]] = false; + sccs.push(scc); + } + } + + if (graph.length === 0) { + return []; + } + + let index = 0; + // The order in which we discover nodes + let discovery: number[] = Array(graph.length).fill(-1); + // For each node, holds the furthest ancestor it can reach + let low: number[] = Array(graph.length).fill(-1); + // 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[][] = []; + + for (let i = 0; i < graph.length; ++i) { + if (low[i] === -1) { + 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); + }); + +}) + From 05aac7bab7370b8e4c89d487afd66174fb616459 Mon Sep 17 00:00:00 2001 From: Joshua Cao Date: Tue, 2 Jan 2024 23:31:26 -0800 Subject: [PATCH 2/2] Address review. Add notes on topological sort. --- graph/tarjan.ts | 45 ++++++++++++++++++++++----------------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/graph/tarjan.ts b/graph/tarjan.ts index da16d7c6..7f2a2454 100644 --- a/graph/tarjan.ts +++ b/graph/tarjan.ts @@ -1,14 +1,29 @@ /** * @function tarjan - * @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. + * @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. + * @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; @@ -17,7 +32,7 @@ export const tarjan = (graph: number[][]): number[][] => { stackContains[node] = true; for (const child of graph[node]) { - if (low[child] === -1) { + if (low[child] === undefined) { dfs(child); if (low[child] < low[node]) { // Child node loops back to this node's ancestor. Update the low node. @@ -32,37 +47,21 @@ export const tarjan = (graph: number[][]): number[][] => { 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 = stack.length - 1; - while (stack[i] != node) { + let i; + for (i = stack.length - 1; stack[i] != node; --i) { scc.push(stack[i]); stackContains[stack[i]] = false; stack.pop(); - --i; } scc.push(stack[i]); - stack.pop(); stackContains[stack[i]] = false; + stack.pop(); sccs.push(scc); } } - if (graph.length === 0) { - return []; - } - - let index = 0; - // The order in which we discover nodes - let discovery: number[] = Array(graph.length).fill(-1); - // For each node, holds the furthest ancestor it can reach - let low: number[] = Array(graph.length).fill(-1); - // 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[][] = []; - for (let i = 0; i < graph.length; ++i) { - if (low[i] === -1) { + if (low[i] === undefined) { dfs(i); } }