|
| 1 | +import depthFirstSearch from '../depth-first-search/depthFirstSearch'; |
| 2 | + |
| 3 | +/** |
| 4 | + * Detect cycle in directed graph using Depth First Search. |
| 5 | + * |
| 6 | + * @param {Graph} graph |
| 7 | + */ |
| 8 | +export default function detectDirectedCycle(graph) { |
| 9 | + let cycle = null; |
| 10 | + |
| 11 | + // Will store parents (previous vertices) for all visited nodes. |
| 12 | + // This will be needed in order to specify what path exactly is a cycle. |
| 13 | + const dfsParentMap = {}; |
| 14 | + |
| 15 | + // White set (UNVISITED) contains all the vertices that haven't been visited at all. |
| 16 | + const whiteSet = {}; |
| 17 | + |
| 18 | + // Gray set (VISITING) contains all the vertices that are being visited right now |
| 19 | + // (in current path). |
| 20 | + const graySet = {}; |
| 21 | + |
| 22 | + // Black set (VISITED) contains all the vertices that has been fully visited. |
| 23 | + // Meaning that all children of the vertex has been visited. |
| 24 | + const blackSet = {}; |
| 25 | + |
| 26 | + // If we encounter vertex in gray set it means that we've found a cycle. |
| 27 | + // Because when vertex in gray set it means that its neighbors or its neighbors |
| 28 | + // neighbors are still being explored. |
| 29 | + |
| 30 | + // Init white set and add all vertices to it. |
| 31 | + /** @param {GraphVertex} vertex */ |
| 32 | + graph.getAllVertices().forEach((vertex) => { |
| 33 | + whiteSet[vertex.getKey()] = vertex; |
| 34 | + }); |
| 35 | + |
| 36 | + // Describe BFS callbacks. |
| 37 | + const callbacks = { |
| 38 | + enterVertex: ({ currentVertex, previousVertex }) => { |
| 39 | + if (graySet[currentVertex.getKey()]) { |
| 40 | + // If current vertex already in grey set it means that cycle is detected. |
| 41 | + // Let's detect cycle path. |
| 42 | + cycle = {}; |
| 43 | + |
| 44 | + let currentCycleVertex = currentVertex; |
| 45 | + let previousCycleVertex = previousVertex; |
| 46 | + |
| 47 | + while (previousCycleVertex.getKey() !== currentVertex.getKey()) { |
| 48 | + cycle[currentCycleVertex.getKey()] = previousCycleVertex; |
| 49 | + currentCycleVertex = previousCycleVertex; |
| 50 | + previousCycleVertex = dfsParentMap[previousCycleVertex.getKey()]; |
| 51 | + } |
| 52 | + |
| 53 | + cycle[currentCycleVertex.getKey()] = previousCycleVertex; |
| 54 | + } else { |
| 55 | + // Otherwise let's add current vertex to gray set and remove it from white set. |
| 56 | + graySet[currentVertex.getKey()] = currentVertex; |
| 57 | + delete whiteSet[currentVertex.getKey()]; |
| 58 | + |
| 59 | + // Update DFS parents list. |
| 60 | + dfsParentMap[currentVertex.getKey()] = previousVertex; |
| 61 | + } |
| 62 | + }, |
| 63 | + leaveVertex: ({ currentVertex }) => { |
| 64 | + // If all node's children has been visited let's remove it from gray set |
| 65 | + // and move it to the black set meaning that all its neighbors are visited. |
| 66 | + blackSet[currentVertex.getKey()] = currentVertex; |
| 67 | + delete graySet[currentVertex.getKey()]; |
| 68 | + }, |
| 69 | + allowTraversal: ({ nextVertex }) => { |
| 70 | + // If cycle was detected we must forbid all further traversing since it will |
| 71 | + // cause infinite traversal loop. |
| 72 | + if (cycle) { |
| 73 | + return false; |
| 74 | + } |
| 75 | + |
| 76 | + // Allow traversal only for the vertices that are not in black set |
| 77 | + // since all black set vertices have been already visited. |
| 78 | + return !blackSet[nextVertex.getKey()]; |
| 79 | + }, |
| 80 | + }; |
| 81 | + |
| 82 | + // Start exploring vertices. |
| 83 | + while (Object.keys(whiteSet).length) { |
| 84 | + // Pick fist vertex to start BFS from. |
| 85 | + const firstWhiteKey = Object.keys(whiteSet)[0]; |
| 86 | + const startVertex = whiteSet[firstWhiteKey]; |
| 87 | + |
| 88 | + // Do Depth First Search. |
| 89 | + depthFirstSearch(graph, startVertex, callbacks); |
| 90 | + } |
| 91 | + |
| 92 | + return cycle; |
| 93 | +} |
0 commit comments