diff --git a/README.md b/README.md index 19fb15f7..56d5387b 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ * [Breadth-First Search](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/breadth-first-search) (BFS) * [Dijkstra Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/dijkstra) - finding shortest path to all graph vertices * [Bellman-Ford Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/bellman-ford) - finding shortest path to all graph vertices - * [Detect Cycle](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/detect-cycle) + * [Detect Cycle](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/detect-cycle) - for both: directed and undirected graphs * Topological Sorting * Eulerian path, Eulerian circuit * Strongly Connected Component algorithm diff --git a/src/algorithms/graph/detect-cycle/__test__/detectDirectedCycle.test.js b/src/algorithms/graph/detect-cycle/__test__/detectDirectedCycle.test.js new file mode 100644 index 00000000..64a41493 --- /dev/null +++ b/src/algorithms/graph/detect-cycle/__test__/detectDirectedCycle.test.js @@ -0,0 +1,42 @@ +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import Graph from '../../../../data-structures/graph/Graph'; +import detectDirectedCycle from '../detectDirectedCycle'; + +describe('detectDirectedCycle', () => { + it('should detect directed cycle', () => { + 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 edgeAB = new GraphEdge(vertexA, vertexB); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeAC = new GraphEdge(vertexA, vertexC); + const edgeDA = new GraphEdge(vertexD, vertexA); + const edgeDE = new GraphEdge(vertexD, vertexE); + const edgeEF = new GraphEdge(vertexE, vertexF); + const edgeFD = new GraphEdge(vertexF, vertexD); + + const graph = new Graph(true); + graph + .addEdge(edgeAB) + .addEdge(edgeBC) + .addEdge(edgeAC) + .addEdge(edgeDA) + .addEdge(edgeDE) + .addEdge(edgeEF); + + expect(detectDirectedCycle(graph)).toBeNull(); + + graph.addEdge(edgeFD); + + expect(detectDirectedCycle(graph)).toEqual({ + D: vertexF, + F: vertexE, + E: vertexD, + }); + }); +}); diff --git a/src/algorithms/graph/detect-cycle/detectDirectedCycle.js b/src/algorithms/graph/detect-cycle/detectDirectedCycle.js new file mode 100644 index 00000000..73b2b224 --- /dev/null +++ b/src/algorithms/graph/detect-cycle/detectDirectedCycle.js @@ -0,0 +1,93 @@ +import depthFirstSearch from '../depth-first-search/depthFirstSearch'; + +/** + * Detect cycle in directed graph using Depth First Search. + * + * @param {Graph} graph + */ +export default function detectDirectedCycle(graph) { + let cycle = null; + + // Will store parents (previous vertices) for all visited nodes. + // This will be needed in order to specify what path exactly is a cycle. + const dfsParentMap = {}; + + // White set (UNVISITED) contains all the vertices that haven't been visited at all. + const whiteSet = {}; + + // Gray set (VISITING) contains all the vertices that are being visited right now + // (in current path). + const graySet = {}; + + // Black set (VISITED) contains all the vertices that has been fully visited. + // Meaning that all children of the vertex has been visited. + const blackSet = {}; + + // If we encounter vertex in gray set it means that we've found a cycle. + // Because when vertex in gray set it means that its neighbors or its neighbors + // neighbors are still being explored. + + // Init white set and add all vertices to it. + /** @param {GraphVertex} vertex */ + graph.getAllVertices().forEach((vertex) => { + whiteSet[vertex.getKey()] = vertex; + }); + + // Describe BFS callbacks. + const callbacks = { + enterVertex: ({ currentVertex, previousVertex }) => { + if (graySet[currentVertex.getKey()]) { + // If current vertex already in grey set it means that cycle is detected. + // Let's detect cycle path. + cycle = {}; + + let currentCycleVertex = currentVertex; + let previousCycleVertex = previousVertex; + + while (previousCycleVertex.getKey() !== currentVertex.getKey()) { + cycle[currentCycleVertex.getKey()] = previousCycleVertex; + currentCycleVertex = previousCycleVertex; + previousCycleVertex = dfsParentMap[previousCycleVertex.getKey()]; + } + + cycle[currentCycleVertex.getKey()] = previousCycleVertex; + } else { + // Otherwise let's add current vertex to gray set and remove it from white set. + graySet[currentVertex.getKey()] = currentVertex; + delete whiteSet[currentVertex.getKey()]; + + // Update DFS parents list. + dfsParentMap[currentVertex.getKey()] = previousVertex; + } + }, + leaveVertex: ({ currentVertex }) => { + // If all node's children has been visited let's remove it from gray set + // and move it to the black set meaning that all its neighbors are visited. + blackSet[currentVertex.getKey()] = currentVertex; + delete graySet[currentVertex.getKey()]; + }, + allowTraversal: ({ nextVertex }) => { + // If cycle was detected we must forbid all further traversing since it will + // cause infinite traversal loop. + if (cycle) { + return false; + } + + // Allow traversal only for the vertices that are not in black set + // since all black set vertices have been already visited. + return !blackSet[nextVertex.getKey()]; + }, + }; + + // Start exploring vertices. + while (Object.keys(whiteSet).length) { + // Pick fist vertex to start BFS from. + const firstWhiteKey = Object.keys(whiteSet)[0]; + const startVertex = whiteSet[firstWhiteKey]; + + // Do Depth First Search. + depthFirstSearch(graph, startVertex, callbacks); + } + + return cycle; +}