From 20d642b4024cc167fe894fd5dadf545ab552db01 Mon Sep 17 00:00:00 2001 From: Oleksii Trekhleb Date: Mon, 14 May 2018 07:13:07 +0300 Subject: [PATCH] Add SCC. --- README.md | 2 +- .../strongly-connected-components/README.md | 16 +++ .../stronglyConnectedComponents.test.js | 102 ++++++++++++++ .../stronglyConnectedComponents.js | 133 ++++++++++++++++++ src/data-structures/stack/Stack.js | 2 +- 5 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 src/algorithms/graph/strongly-connected-components/README.md create mode 100644 src/algorithms/graph/strongly-connected-components/__test__/stronglyConnectedComponents.test.js create mode 100644 src/algorithms/graph/strongly-connected-components/stronglyConnectedComponents.js diff --git a/README.md b/README.md index 96b8443a..899e01f0 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ * [Articulation Points](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/articulation-points) - Tarjan's algorithm (DFS based) * [Bridges](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/bridges) - DFS based algorithm * [Eulerian Path and Eulerian Circuit](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/eulerian-path) - Fleury's algorithm - * Strongly Connected Component algorithm + * [Strongly Connected Components](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/strongly-connected-components) - Kosaraju's algorithm * Shortest Path Faster Algorithm (SPFA) * **Uncategorized** * Union-Find diff --git a/src/algorithms/graph/strongly-connected-components/README.md b/src/algorithms/graph/strongly-connected-components/README.md new file mode 100644 index 00000000..54faaad4 --- /dev/null +++ b/src/algorithms/graph/strongly-connected-components/README.md @@ -0,0 +1,16 @@ +# Strongly Connected Component + +A directed graph is called **strongly connected** if there is a path +in each direction between each pair of vertices of the graph. +In a directed graph G that may not itself be strongly connected, +a pair of vertices `u` and `v` are said to be strongly connected +to each other if there is a path in each direction between them. + +![Strongly Connected](https://upload.wikimedia.org/wikipedia/commons/5/5c/Scc.png) + +Graph with strongly connected components marked + +## References + +- [Wikipedia](https://en.wikipedia.org/wiki/Strongly_connected_component) +- [YouTube](https://www.youtube.com/watch?v=RpgcYiky7uw) diff --git a/src/algorithms/graph/strongly-connected-components/__test__/stronglyConnectedComponents.test.js b/src/algorithms/graph/strongly-connected-components/__test__/stronglyConnectedComponents.test.js new file mode 100644 index 00000000..3379ca75 --- /dev/null +++ b/src/algorithms/graph/strongly-connected-components/__test__/stronglyConnectedComponents.test.js @@ -0,0 +1,102 @@ +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import Graph from '../../../../data-structures/graph/Graph'; +import stronglyConnectedComponents from '../stronglyConnectedComponents'; + +describe('stronglyConnectedComponents', () => { + it('should detect strongly connected components in simple graph', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + const vertexD = new GraphVertex('D'); + + const edgeAB = new GraphEdge(vertexA, vertexB); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeCA = new GraphEdge(vertexC, vertexA); + const edgeCD = new GraphEdge(vertexC, vertexD); + + const graph = new Graph(true); + + graph + .addEdge(edgeAB) + .addEdge(edgeBC) + .addEdge(edgeCA) + .addEdge(edgeCD); + + const components = stronglyConnectedComponents(graph); + + expect(components).toBeDefined(); + expect(components.length).toBe(2); + + expect(components[0][0].getKey()).toBe(vertexA.getKey()); + expect(components[0][1].getKey()).toBe(vertexC.getKey()); + expect(components[0][2].getKey()).toBe(vertexB.getKey()); + + expect(components[1][0].getKey()).toBe(vertexD.getKey()); + }); + + it('should detect strongly connected components in graph', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + const vertexD = new GraphVertex('D'); + const vertexE = new GraphVertex('E'); + const vertexF = new GraphVertex('F'); + const vertexG = new GraphVertex('G'); + const vertexH = new GraphVertex('H'); + const vertexI = new GraphVertex('I'); + const vertexJ = new GraphVertex('J'); + const vertexK = new GraphVertex('K'); + + const edgeAB = new GraphEdge(vertexA, vertexB); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeCA = new GraphEdge(vertexC, vertexA); + const edgeBD = new GraphEdge(vertexB, vertexD); + const edgeDE = new GraphEdge(vertexD, vertexE); + const edgeEF = new GraphEdge(vertexE, vertexF); + const edgeFD = new GraphEdge(vertexF, vertexD); + const edgeGF = new GraphEdge(vertexG, vertexF); + const edgeGH = new GraphEdge(vertexG, vertexH); + const edgeHI = new GraphEdge(vertexH, vertexI); + const edgeIJ = new GraphEdge(vertexI, vertexJ); + const edgeJG = new GraphEdge(vertexJ, vertexG); + const edgeJK = new GraphEdge(vertexJ, vertexK); + + const graph = new Graph(true); + + graph + .addEdge(edgeAB) + .addEdge(edgeBC) + .addEdge(edgeCA) + .addEdge(edgeBD) + .addEdge(edgeDE) + .addEdge(edgeEF) + .addEdge(edgeFD) + .addEdge(edgeGF) + .addEdge(edgeGH) + .addEdge(edgeHI) + .addEdge(edgeIJ) + .addEdge(edgeJG) + .addEdge(edgeJK); + + const components = stronglyConnectedComponents(graph); + + expect(components).toBeDefined(); + expect(components.length).toBe(4); + + expect(components[0][0].getKey()).toBe(vertexG.getKey()); + expect(components[0][1].getKey()).toBe(vertexJ.getKey()); + expect(components[0][2].getKey()).toBe(vertexI.getKey()); + expect(components[0][3].getKey()).toBe(vertexH.getKey()); + + expect(components[1][0].getKey()).toBe(vertexK.getKey()); + + expect(components[2][0].getKey()).toBe(vertexA.getKey()); + expect(components[2][1].getKey()).toBe(vertexC.getKey()); + expect(components[2][2].getKey()).toBe(vertexB.getKey()); + + expect(components[3][0].getKey()).toBe(vertexD.getKey()); + expect(components[3][1].getKey()).toBe(vertexF.getKey()); + expect(components[3][2].getKey()).toBe(vertexE.getKey()); + }); +}); diff --git a/src/algorithms/graph/strongly-connected-components/stronglyConnectedComponents.js b/src/algorithms/graph/strongly-connected-components/stronglyConnectedComponents.js new file mode 100644 index 00000000..80962773 --- /dev/null +++ b/src/algorithms/graph/strongly-connected-components/stronglyConnectedComponents.js @@ -0,0 +1,133 @@ +import Stack from '../../../data-structures/stack/Stack'; +import depthFirstSearch from '../depth-first-search/depthFirstSearch'; + +/** + * @param {Graph} graph + * @return {Stack} + */ +function getVerticesSortedByDfsFinishTime(graph) { + // Set of all visited vertices during DFS pass. + const visitedVerticesSet = {}; + + // Stack of vertices by finish time. + // All vertices in this stack are ordered by finished time in decreasing order. + // Vertex that has been finished first will be at the bottom of the stack and + // vertex that has been finished last will be at the top of the stack. + const verticesByDfsFinishTime = new Stack(); + + // Set of all vertices we're going to visit. + const notVisitedVerticesSet = {}; + graph.getAllVertices().forEach((vertex) => { + notVisitedVerticesSet[vertex.getKey()] = vertex; + }); + + // Specify DFS traversal callbacks. + const dfsCallbacks = { + enterVertex: ({ currentVertex }) => { + // Add current vertex to visited set. + visitedVerticesSet[currentVertex.getKey()] = currentVertex; + + // Delete current vertex from not visited set. + delete notVisitedVerticesSet[currentVertex.getKey()]; + }, + leaveVertex: ({ currentVertex }) => { + // Push vertex to the stack when leaving it. + // This will make stack to be ordered by finish time in decreasing order. + verticesByDfsFinishTime.push(currentVertex); + }, + allowTraversal: ({ nextVertex }) => { + // Don't allow to traverse the nodes that have been already visited. + return !visitedVerticesSet[nextVertex.getKey()]; + }, + }; + + // Do FIRST DFS PASS traversal for all graph vertices to fill the verticesByFinishTime stack. + while (Object.values(notVisitedVerticesSet).length) { + // Peek any vertex to start DFS traversal from. + const startVertexKey = Object.keys(notVisitedVerticesSet)[0]; + const startVertex = notVisitedVerticesSet[startVertexKey]; + delete notVisitedVerticesSet[startVertexKey]; + + depthFirstSearch(graph, startVertex, dfsCallbacks); + } + + return verticesByDfsFinishTime; +} + +/** + * @param {Graph} graph + * @param {Stack} verticesByFinishTime + * @return {*[]} + */ +function getSCCSets(graph, verticesByFinishTime) { + // Array of arrays of strongly connected vertices. + const stronglyConnectedComponentsSets = []; + + // Array that will hold all vertices that are being visited during one DFS run. + let stronglyConnectedComponentsSet = []; + + // Visited vertices set. + const visitedVerticesSet = {}; + + // Callbacks for DFS traversal. + const dfsCallbacks = { + enterVertex: ({ currentVertex }) => { + // Add current vertex to SCC set of current DFS round. + stronglyConnectedComponentsSet.push(currentVertex); + + // Add current vertex to visited set. + visitedVerticesSet[currentVertex.getKey()] = currentVertex; + }, + leaveVertex: ({ previousVertex }) => { + // Once DFS traversal is finished push the set of found strongly connected + // components during current DFS round to overall strongly connected components set. + // The sign that traversal is about to be finished is that we came back to start vertex + // which doesn't have parent. + if (previousVertex === null) { + stronglyConnectedComponentsSets.push([...stronglyConnectedComponentsSet]); + } + }, + allowTraversal: ({ nextVertex }) => { + // Don't allow traversal of already visited vertices. + return !visitedVerticesSet[nextVertex.getKey()]; + }, + }; + + while (!verticesByFinishTime.isEmpty()) { + /** @var {GraphVertex} startVertex */ + const startVertex = verticesByFinishTime.pop(); + + // Reset the set of strongly connected vertices. + stronglyConnectedComponentsSet = []; + + // Don't do DFS on already visited vertices. + if (!visitedVerticesSet[startVertex.getKey()]) { + // Do DFS traversal. + depthFirstSearch(graph, startVertex, dfsCallbacks); + } + } + + return stronglyConnectedComponentsSets; +} + +/** + * Kosaraju's algorithm. + * + * @param {Graph} graph + * @return {*[]} + */ +export default function stronglyConnectedComponents(graph) { + // In this algorithm we will need to do TWO DFS PASSES overt the graph. + + // Get stack of vertices ordered by DFS finish time. + // All vertices in this stack are ordered by finished time in decreasing order: + // Vertex that has been finished first will be at the bottom of the stack and + // vertex that has been finished last will be at the top of the stack. + const verticesByFinishTime = getVerticesSortedByDfsFinishTime(graph); + + // Reverse the graph. + graph.reverse(); + + // Do DFS once again on reversed graph. + return getSCCSets(graph, verticesByFinishTime); +} diff --git a/src/data-structures/stack/Stack.js b/src/data-structures/stack/Stack.js index a7108300..0cd95b07 100644 --- a/src/data-structures/stack/Stack.js +++ b/src/data-structures/stack/Stack.js @@ -31,7 +31,7 @@ export default class Stack { } /** - * @return {LinkedListNode} + * @return {*} */ pop() { const removedTail = this.linkedList.deleteTail();