Skip to content

Commit

Permalink
feat: Add kosajura algorithm for finding strong connected components (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
caojoshua authored Dec 31, 2023
1 parent 4acf117 commit 6e78b6c
Show file tree
Hide file tree
Showing 2 changed files with 147 additions and 0 deletions.
75 changes: 75 additions & 0 deletions graph/kosajaru.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// Compute the node priorities, which will be used to determine the order in which we perform transposed DFS.
const getNodePriorities = (graph: number[][], visited: boolean[], stack: number[], node: number) => {
if (visited[node]) {
return;
}
visited[node] = true;

for (const dest of graph[node]) {
getNodePriorities(graph, visited, stack, dest);
}
// Nodes that end their DFS earlier are pushed onto the stack first and have lower priority.
stack.push(node);
}

// Return the transpose of graph. The tranpose of a directed graph is a graph where each of the edges are flipped.
const transpose = (graph: number[][]): number[][] => {
let transposedGraph = Array(graph.length);
for (let i = 0; i < graph.length; ++i) {
transposedGraph[i] = [];
}

for (let i = 0; i < graph.length; ++i) {
for (let j = 0; j < graph[i].length; ++j) {
transposedGraph[graph[i][j]].push(i);
}
}

return transposedGraph;
}

// Computes the SCC that contains the given node
const gatherScc = (graph: number[][], visited: boolean[], node: number, scc: number[]) => {
if (visited[node]) {
return;
}
visited[node] = true;
scc.push(node);

for (const dest of graph[node]) {
gatherScc(graph, visited, dest, scc);
}
}

/**
* @function kosajaru
* @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 two DFS twice, and make sure to visit each disconnected graph. Each DFS is O(V + E).
* Space Complexity: O(V + E). This space is required for the transposed graph.
* @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/Kosaraju%27s_algorithm
*/
export const kosajaru = (graph: number[][]): number[][] => {
let visited = Array(graph.length).fill(false);

let stack: number[] = [];
for (let i = 0; i < graph.length; ++i) {
getNodePriorities(graph, visited, stack, i);
}

const transposedGraph = transpose(graph);

let sccs = [];
visited.fill(false);
for (let i = stack.length - 1; i >= 0; --i) {
if (!visited[stack[i]]) {
let scc: number[] = [];
gatherScc(transposedGraph, visited, stack[i], scc);
sccs.push(scc);
}
}
return sccs;
}

72 changes: 72 additions & 0 deletions graph/test/kosajaru.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { kosajaru } from "../kosajaru";

describe("kosajaru", () => {

it("it should return no sccs for empty graph", () => {
expect(kosajaru([])).toStrictEqual([]);
});

it("it should return one scc for graph with one element", () => {
expect(kosajaru([[]])).toStrictEqual([[0]]);
});

it("it should return one scc for graph with element that points to itself", () => {
expect(kosajaru([[0]])).toStrictEqual([[0]]);
});

it("it should return one scc for two element graph with cycle", () => {
expect(kosajaru([[1], [0]])).toStrictEqual([[0, 1]]);
});

it("should return one scc for each element for straight line", () => {
expect(kosajaru([[1], [2], [3], []])).toStrictEqual([[0], [1], [2], [3]]);
});

it("should return sccs for straight line with backedge in middle", () => {
expect(kosajaru([[1], [2], [3, 0], []])).toStrictEqual([[0, 2, 1], [3]]);
});

it("should return sccs for straight line with backedge from end to middle", () => {
expect(kosajaru([[1], [2], [3], [1]])).toStrictEqual([[0], [1, 3, 2]]);
});

it("should return scc for each element for graph with no edges", () => {
expect(kosajaru([[], [], [], []])).toStrictEqual([[3], [2], [1], [0]]);
});

it("should return sccs disconnected graph", () => {
expect(kosajaru([[1, 2], [0, 2], [0, 1], []])).toStrictEqual([[3], [0, 1, 2]]);
});

it("should return sccs disconnected graph", () => {
expect(kosajaru([[1, 2], [0, 2], [0, 1], [4], [5], [3]])).toStrictEqual([[3, 5, 4], [0, 1, 2]]);
});

it("should return single scc", () => {
expect(kosajaru([[1], [2], [3], [0, 4], [3]])).toStrictEqual([[0, 3, 2, 1, 4]]);
});

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(kosajaru(input)).toStrictEqual([[0, 1, 2, 3, 4]]);
});

it("should return sccs", () => {
const input = [[1], [2], [0, 3], [4], []];
expect(kosajaru(input)).toStrictEqual([[0, 2, 1], [3], [4]]);
});

it("should return sccs", () => {
const input = [[1], [2], [0, 3, 4], [0], [5], [6, 7], [2, 4], [8], [5, 9], [5]];
const expected = [[0, 2, 1, 6, 5, 4, 8, 7, 9, 3]];
expect(kosajaru(input)).toStrictEqual(expected);
});

it("should return sccs", () => {
const input = [[1], [0, 2], [0, 3], [4], [5, 7], [6], [4, 7], []];
const expected = [[0, 1, 2], [3], [4, 6, 5], [7]];
expect(kosajaru(input)).toStrictEqual(expected);
});

})

0 comments on commit 6e78b6c

Please sign in to comment.