diff --git a/README.md b/README.md index 4705bda1..c990b240 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) - for both directed and undirected graphs + * [Detect Cycle](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/detect-cycle) - for both directed and undirected graphs (DFS and Disjoint Set based versions) * Topological Sorting * Eulerian path, Eulerian circuit * Strongly Connected Component algorithm diff --git a/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js b/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js index 0e827852..c3f99031 100644 --- a/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js +++ b/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycle.test.js @@ -27,10 +27,15 @@ describe('detectUndirectedCycle', () => { .addEdge(edgeBC) .addEdge(edgeCD); - expect(detectUndirectedCycle(graph)).toBeFalsy(); + expect(detectUndirectedCycle(graph)).toBeNull(); graph.addEdge(edgeDE); - expect(detectUndirectedCycle(graph)).toBeTruthy(); + expect(detectUndirectedCycle(graph)).toEqual({ + B: vertexC, + C: vertexD, + D: vertexE, + E: vertexB, + }); }); }); diff --git a/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycleUsingDisjointSet.test.js b/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycleUsingDisjointSet.test.js new file mode 100644 index 00000000..157273a5 --- /dev/null +++ b/src/algorithms/graph/detect-cycle/__test__/detectUndirectedCycleUsingDisjointSet.test.js @@ -0,0 +1,36 @@ +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import Graph from '../../../../data-structures/graph/Graph'; +import detectUndirectedCycleUsingDisjointSet from '../detectUndirectedCycleUsingDisjointSet'; + +describe('detectUndirectedCycleUsingDisjointSet', () => { + it('should detect undirected 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 edgeAF = new GraphEdge(vertexA, vertexF); + const edgeAB = new GraphEdge(vertexA, vertexB); + const edgeBE = new GraphEdge(vertexB, vertexE); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeCD = new GraphEdge(vertexC, vertexD); + const edgeDE = new GraphEdge(vertexD, vertexE); + + const graph = new Graph(); + graph + .addEdge(edgeAF) + .addEdge(edgeAB) + .addEdge(edgeBE) + .addEdge(edgeBC) + .addEdge(edgeCD); + + expect(detectUndirectedCycleUsingDisjointSet(graph)).toBeFalsy(); + + graph.addEdge(edgeDE); + + expect(detectUndirectedCycleUsingDisjointSet(graph)).toBeTruthy(); + }); +}); diff --git a/src/algorithms/graph/detect-cycle/detectUndirectedCycle.js b/src/algorithms/graph/detect-cycle/detectUndirectedCycle.js index 3c6b6217..5bcc9bb6 100644 --- a/src/algorithms/graph/detect-cycle/detectUndirectedCycle.js +++ b/src/algorithms/graph/detect-cycle/detectUndirectedCycle.js @@ -1,32 +1,59 @@ -import DisjointSet from '../../../data-structures/disjoint-set/DisjointSet'; +import depthFirstSearch from '../depth-first-search/depthFirstSearch'; /** - * Detect cycle in undirected graph using disjoint sets. + * Detect cycle in undirected graph using Depth First Search. * * @param {Graph} graph */ - export default function detectUndirectedCycle(graph) { - // Create initial singleton disjoint sets for each graph vertex. - /** @param {GraphVertex} graphVertex */ - const keyExtractor = graphVertex => graphVertex.getKey(); - const disjointSet = new DisjointSet(keyExtractor); - graph.getAllVertices().forEach(graphVertex => disjointSet.makeSet(graphVertex)); + let cycle = null; - // Go trough all graph edges one by one and check if edge vertices are from the - // different sets. In this case joint those sets together. Do this until you find - // an edge where to edge vertices are already in one set. This means that current - // edge will create a cycle. - let cycleFound = false; - /** @param {GraphEdge} graphEdge */ - graph.getAllEdges().forEach((graphEdge) => { - if (disjointSet.inSameSet(graphEdge.startVertex, graphEdge.endVertex)) { - // Cycle found. - cycleFound = true; - } else { - disjointSet.union(graphEdge.startVertex, graphEdge.endVertex); - } - }); + // List of vertices that we have visited. + const visitedVertices = {}; - return cycleFound; + // List of parents vertices for every visited vertex. + const parents = {}; + + // Callbacks for DFS traversing. + const callbacks = { + allowTraversal: ({ currentVertex, nextVertex }) => { + // Don't allow further traversal in case if cycle has been detected. + if (cycle) { + return false; + } + + // Don't allow traversal from child back to its parent. + const currentVertexParent = parents[currentVertex.getKey()]; + const currentVertexParentKey = currentVertexParent ? currentVertexParent.getKey() : null; + + return currentVertexParentKey !== nextVertex.getKey(); + }, + enterVertex: ({ currentVertex, previousVertex }) => { + if (visitedVertices[currentVertex.getKey()]) { + // Compile cycle path based on parents of previous vertices. + cycle = {}; + + let currentCycleVertex = currentVertex; + let previousCycleVertex = previousVertex; + + while (previousCycleVertex.getKey() !== currentVertex.getKey()) { + cycle[currentCycleVertex.getKey()] = previousCycleVertex; + currentCycleVertex = previousCycleVertex; + previousCycleVertex = parents[previousCycleVertex.getKey()]; + } + + cycle[currentCycleVertex.getKey()] = previousCycleVertex; + } else { + // Add next vertex to visited set. + visitedVertices[currentVertex.getKey()] = currentVertex; + parents[currentVertex.getKey()] = previousVertex; + } + }, + }; + + // Start DFS traversing. + const startVertex = graph.getAllVertices()[0]; + depthFirstSearch(graph, startVertex, callbacks); + + return cycle; } diff --git a/src/algorithms/graph/detect-cycle/detectUndirectedCycleUsingDisjointSet.js b/src/algorithms/graph/detect-cycle/detectUndirectedCycleUsingDisjointSet.js new file mode 100644 index 00000000..90ca744d --- /dev/null +++ b/src/algorithms/graph/detect-cycle/detectUndirectedCycleUsingDisjointSet.js @@ -0,0 +1,31 @@ +import DisjointSet from '../../../data-structures/disjoint-set/DisjointSet'; + +/** + * Detect cycle in undirected graph using disjoint sets. + * + * @param {Graph} graph + */ +export default function detectUndirectedCycleUsingDisjointSet(graph) { + // Create initial singleton disjoint sets for each graph vertex. + /** @param {GraphVertex} graphVertex */ + const keyExtractor = graphVertex => graphVertex.getKey(); + const disjointSet = new DisjointSet(keyExtractor); + graph.getAllVertices().forEach(graphVertex => disjointSet.makeSet(graphVertex)); + + // Go trough all graph edges one by one and check if edge vertices are from the + // different sets. In this case joint those sets together. Do this until you find + // an edge where to edge vertices are already in one set. This means that current + // edge will create a cycle. + let cycleFound = false; + /** @param {GraphEdge} graphEdge */ + graph.getAllEdges().forEach((graphEdge) => { + if (disjointSet.inSameSet(graphEdge.startVertex, graphEdge.endVertex)) { + // Cycle found. + cycleFound = true; + } else { + disjointSet.union(graphEdge.startVertex, graphEdge.endVertex); + } + }); + + return cycleFound; +}