diff --git a/README.md b/README.md index 5ea8d24..96b8443 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ * [Topological Sorting](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/topological-sorting) - DFS method * [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) + * [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 * Shortest Path Faster Algorithm (SPFA) * **Uncategorized** diff --git a/src/algorithms/graph/articulation-points/__test__/articulationPoints.test.js b/src/algorithms/graph/articulation-points/__test__/articulationPoints.test.js index 03b12ed..b021305 100644 --- a/src/algorithms/graph/articulation-points/__test__/articulationPoints.test.js +++ b/src/algorithms/graph/articulation-points/__test__/articulationPoints.test.js @@ -21,7 +21,7 @@ describe('articulationPoints', () => { .addEdge(edgeBC) .addEdge(edgeCD); - const articulationPointsSet = articulationPoints(graph); + const articulationPointsSet = Object.values(articulationPoints(graph)); expect(articulationPointsSet.length).toBe(2); expect(articulationPointsSet[0].getKey()).toBe(vertexC.getKey()); @@ -47,7 +47,7 @@ describe('articulationPoints', () => { .addEdge(edgeBC) .addEdge(edgeCD); - const articulationPointsSet = articulationPoints(graph); + const articulationPointsSet = Object.values(articulationPoints(graph)); expect(articulationPointsSet.length).toBe(1); expect(articulationPointsSet[0].getKey()).toBe(vertexC.getKey()); @@ -75,7 +75,7 @@ describe('articulationPoints', () => { .addEdge(edgeBC) .addEdge(edgeCD); - const articulationPointsSet = articulationPoints(graph); + const articulationPointsSet = Object.values(articulationPoints(graph)); expect(articulationPointsSet.length).toBe(1); expect(articulationPointsSet[0].getKey()).toBe(vertexC.getKey()); @@ -114,7 +114,7 @@ describe('articulationPoints', () => { .addEdge(edgeGF) .addEdge(edgeFH); - const articulationPointsSet = articulationPoints(graph); + const articulationPointsSet = Object.values(articulationPoints(graph)); expect(articulationPointsSet.length).toBe(4); expect(articulationPointsSet[0].getKey()).toBe(vertexF.getKey()); @@ -156,7 +156,7 @@ describe('articulationPoints', () => { .addEdge(edgeGF) .addEdge(edgeFH); - const articulationPointsSet = articulationPoints(graph); + const articulationPointsSet = Object.values(articulationPoints(graph)); expect(articulationPointsSet.length).toBe(4); expect(articulationPointsSet[0].getKey()).toBe(vertexF.getKey()); @@ -187,7 +187,7 @@ describe('articulationPoints', () => { .addEdge(edgeCD) .addEdge(edgeDE); - const articulationPointsSet = articulationPoints(graph); + const articulationPointsSet = Object.values(articulationPoints(graph)); expect(articulationPointsSet.length).toBe(2); expect(articulationPointsSet[0].getKey()).toBe(vertexD.getKey()); @@ -224,7 +224,7 @@ describe('articulationPoints', () => { .addEdge(edgeEG) .addEdge(edgeFG); - const articulationPointsSet = articulationPoints(graph); + const articulationPointsSet = Object.values(articulationPoints(graph)); expect(articulationPointsSet.length).toBe(1); expect(articulationPointsSet[0].getKey()).toBe(vertexC.getKey()); diff --git a/src/algorithms/graph/articulation-points/articulationPoints.js b/src/algorithms/graph/articulation-points/articulationPoints.js index 8b0d707..0dc9da7 100644 --- a/src/algorithms/graph/articulation-points/articulationPoints.js +++ b/src/algorithms/graph/articulation-points/articulationPoints.js @@ -17,7 +17,7 @@ class VisitMetadata { * Tarjan's algorithm for finding articulation points in graph. * * @param {Graph} graph - * @return {GraphVertex[]} + * @return {Object} */ export default function articulationPoints(graph) { // Set of vertices we've already visited during DFS. @@ -109,5 +109,5 @@ export default function articulationPoints(graph) { // Do Depth First Search traversal over submitted graph. depthFirstSearch(graph, startVertex, dfsCallbacks); - return Object.values(articulationPointsSet); + return articulationPointsSet; } diff --git a/src/algorithms/graph/bridges/__test__/graphBridges.test.js b/src/algorithms/graph/bridges/__test__/graphBridges.test.js index c50efec..0183752 100644 --- a/src/algorithms/graph/bridges/__test__/graphBridges.test.js +++ b/src/algorithms/graph/bridges/__test__/graphBridges.test.js @@ -21,7 +21,7 @@ describe('graphBridges', () => { .addEdge(edgeBC) .addEdge(edgeCD); - const bridges = graphBridges(graph); + const bridges = Object.values(graphBridges(graph)); expect(bridges.length).toBe(3); expect(bridges[0].getKey()).toBe(edgeCD.getKey()); @@ -48,7 +48,7 @@ describe('graphBridges', () => { .addEdge(edgeBC) .addEdge(edgeCD); - const bridges = graphBridges(graph); + const bridges = Object.values(graphBridges(graph)); expect(bridges.length).toBe(1); expect(bridges[0].getKey()).toBe(edgeCD.getKey()); @@ -87,7 +87,7 @@ describe('graphBridges', () => { .addEdge(edgeGF) .addEdge(edgeFH); - const bridges = graphBridges(graph); + const bridges = Object.values(graphBridges(graph)); expect(bridges.length).toBe(3); expect(bridges[0].getKey()).toBe(edgeFH.getKey()); @@ -128,7 +128,7 @@ describe('graphBridges', () => { .addEdge(edgeGF) .addEdge(edgeFH); - const bridges = graphBridges(graph); + const bridges = Object.values(graphBridges(graph)); expect(bridges.length).toBe(3); expect(bridges[0].getKey()).toBe(edgeFH.getKey()); @@ -158,7 +158,7 @@ describe('graphBridges', () => { .addEdge(edgeCD) .addEdge(edgeDE); - const bridges = graphBridges(graph); + const bridges = Object.values(graphBridges(graph)); expect(bridges.length).toBe(2); expect(bridges[0].getKey()).toBe(edgeDE.getKey()); @@ -195,7 +195,7 @@ describe('graphBridges', () => { .addEdge(edgeEG) .addEdge(edgeFG); - const bridges = graphBridges(graph); + const bridges = Object.values(graphBridges(graph)); expect(bridges.length).toBe(1); expect(bridges[0].getKey()).toBe(edgeCD.getKey()); diff --git a/src/algorithms/graph/bridges/graphBridges.js b/src/algorithms/graph/bridges/graphBridges.js index 48b16b0..58c6286 100644 --- a/src/algorithms/graph/bridges/graphBridges.js +++ b/src/algorithms/graph/bridges/graphBridges.js @@ -12,7 +12,7 @@ class VisitMetadata { /** * @param {Graph} graph - * @return {GraphVertex[]} + * @return {Object} */ export default function graphBridges(graph) { // Set of vertices we've already visited during DFS. @@ -91,5 +91,5 @@ export default function graphBridges(graph) { // Do Depth First Search traversal over submitted graph. depthFirstSearch(graph, startVertex, dfsCallbacks); - return Object.values(bridges); + return bridges; } diff --git a/src/algorithms/graph/eulerian-path/__test__/eulerianPath.test.js b/src/algorithms/graph/eulerian-path/__test__/eulerianPath.test.js new file mode 100644 index 0000000..d10d980 --- /dev/null +++ b/src/algorithms/graph/eulerian-path/__test__/eulerianPath.test.js @@ -0,0 +1,139 @@ +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import Graph from '../../../../data-structures/graph/Graph'; +import eulerianPath from '../eulerianPath'; + +describe('eulerianPath', () => { + it('should throw an error when graph is not Eulerian', () => { + function findEulerianPathInNotEulerianGraph() { + 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 edgeAB = new GraphEdge(vertexA, vertexB); + const edgeAC = new GraphEdge(vertexA, vertexC); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeBD = new GraphEdge(vertexB, vertexD); + const edgeCE = new GraphEdge(vertexC, vertexE); + + const graph = new Graph(); + + graph + .addEdge(edgeAB) + .addEdge(edgeAC) + .addEdge(edgeBC) + .addEdge(edgeBD) + .addEdge(edgeCE); + + eulerianPath(graph); + } + + expect(findEulerianPathInNotEulerianGraph).toThrowError(); + }); + + it('should find Eulerian Circuit 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 edgeAB = new GraphEdge(vertexA, vertexB); + const edgeAE = new GraphEdge(vertexA, vertexE); + const edgeAF = new GraphEdge(vertexA, vertexF); + const edgeAG = new GraphEdge(vertexA, vertexG); + const edgeGF = new GraphEdge(vertexG, vertexF); + const edgeBE = new GraphEdge(vertexB, vertexE); + const edgeEB = new GraphEdge(vertexE, vertexB); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeED = new GraphEdge(vertexE, vertexD); + const edgeCD = new GraphEdge(vertexC, vertexD); + + const graph = new Graph(); + + graph + .addEdge(edgeAB) + .addEdge(edgeAE) + .addEdge(edgeAF) + .addEdge(edgeAG) + .addEdge(edgeGF) + .addEdge(edgeBE) + .addEdge(edgeEB) + .addEdge(edgeBC) + .addEdge(edgeED) + .addEdge(edgeCD); + + const graphEdgesCount = graph.getAllEdges().length; + + const eulerianPathSet = eulerianPath(graph); + + expect(eulerianPathSet.length).toBe(graphEdgesCount + 1); + + expect(eulerianPathSet[0].getKey()).toBe(vertexA.getKey()); + expect(eulerianPathSet[1].getKey()).toBe(vertexB.getKey()); + expect(eulerianPathSet[2].getKey()).toBe(vertexE.getKey()); + expect(eulerianPathSet[3].getKey()).toBe(vertexB.getKey()); + expect(eulerianPathSet[4].getKey()).toBe(vertexC.getKey()); + expect(eulerianPathSet[5].getKey()).toBe(vertexD.getKey()); + expect(eulerianPathSet[6].getKey()).toBe(vertexE.getKey()); + expect(eulerianPathSet[7].getKey()).toBe(vertexA.getKey()); + expect(eulerianPathSet[8].getKey()).toBe(vertexF.getKey()); + expect(eulerianPathSet[9].getKey()).toBe(vertexG.getKey()); + expect(eulerianPathSet[10].getKey()).toBe(vertexA.getKey()); + }); + + it('should find Eulerian Path 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 edgeAB = new GraphEdge(vertexA, vertexB); + const edgeAC = new GraphEdge(vertexA, vertexC); + const edgeBD = new GraphEdge(vertexB, vertexD); + const edgeDC = new GraphEdge(vertexD, vertexC); + const edgeCE = new GraphEdge(vertexC, vertexE); + const edgeEF = new GraphEdge(vertexE, vertexF); + const edgeFH = new GraphEdge(vertexF, vertexH); + const edgeFG = new GraphEdge(vertexF, vertexG); + const edgeHG = new GraphEdge(vertexH, vertexG); + + const graph = new Graph(); + + graph + .addEdge(edgeAB) + .addEdge(edgeAC) + .addEdge(edgeBD) + .addEdge(edgeDC) + .addEdge(edgeCE) + .addEdge(edgeEF) + .addEdge(edgeFH) + .addEdge(edgeFG) + .addEdge(edgeHG); + + const graphEdgesCount = graph.getAllEdges().length; + + const eulerianPathSet = eulerianPath(graph); + + expect(eulerianPathSet.length).toBe(graphEdgesCount + 1); + + expect(eulerianPathSet[0].getKey()).toBe(vertexC.getKey()); + expect(eulerianPathSet[1].getKey()).toBe(vertexA.getKey()); + expect(eulerianPathSet[2].getKey()).toBe(vertexB.getKey()); + expect(eulerianPathSet[3].getKey()).toBe(vertexD.getKey()); + expect(eulerianPathSet[4].getKey()).toBe(vertexC.getKey()); + expect(eulerianPathSet[5].getKey()).toBe(vertexE.getKey()); + expect(eulerianPathSet[6].getKey()).toBe(vertexF.getKey()); + expect(eulerianPathSet[7].getKey()).toBe(vertexH.getKey()); + expect(eulerianPathSet[8].getKey()).toBe(vertexG.getKey()); + expect(eulerianPathSet[9].getKey()).toBe(vertexF.getKey()); + }); +}); diff --git a/src/algorithms/graph/eulerian-path/eulerianPath.js b/src/algorithms/graph/eulerian-path/eulerianPath.js new file mode 100644 index 0000000..0222fcb --- /dev/null +++ b/src/algorithms/graph/eulerian-path/eulerianPath.js @@ -0,0 +1,101 @@ +import graphBridges from '../bridges/graphBridges'; + +/** + * Fleury's algorithm of finding Eulerian Path (visit all graph edges exactly once). + * + * @param {Graph} graph + * @return {GraphVertex[]} + */ +export default function eulerianPath(graph) { + const eulerianPathVertices = []; + + // Set that contains all vertices with even rank (number of neighbors). + const evenRankVertices = {}; + + // Set that contains all vertices with odd rank (number of neighbors). + const oddRankVertices = {}; + + // Set of all not visited edges. + const notVisitedEdges = {}; + graph.getAllEdges().forEach((vertex) => { + notVisitedEdges[vertex.getKey()] = vertex; + }); + + // Detect whether graph contains Eulerian Circuit or Eulerian Path or none of them. + /** @params {GraphVertex} vertex */ + graph.getAllVertices().forEach((vertex) => { + if (vertex.getDegree() % 2) { + oddRankVertices[vertex.getKey()] = vertex; + } else { + evenRankVertices[vertex.getKey()] = vertex; + } + }); + + // Check whether we're dealing with Eulerian Circuit or Eulerian Path only. + // Graph would be an Eulerian Circuit in case if all its vertices has even degree. + // If not all vertices have even degree then graph must contain only two odd-degree + // vertices in order to have Euler Path. + const isCircuit = !Object.values(oddRankVertices).length; + + if (!isCircuit && Object.values(oddRankVertices).length !== 2) { + throw new Error('Eulerian path must contain two odd-ranked vertices'); + } + + // Pick start vertex for traversal. + let startVertex = null; + + if (isCircuit) { + // For Eulerian Circuit it doesn't matter from what vertex to start thus we'll just + // peek a first node. + const evenVertexKey = Object.keys(evenRankVertices)[0]; + startVertex = evenRankVertices[evenVertexKey]; + } else { + // For Eulerian Path we need to start from one of two odd-degree vertices. + const oddVertexKey = Object.keys(oddRankVertices)[0]; + startVertex = oddRankVertices[oddVertexKey]; + } + + // Start traversing the graph. + let currentVertex = startVertex; + while (Object.values(notVisitedEdges).length) { + // Add current vertex to Eulerian path. + eulerianPathVertices.push(currentVertex); + + // Detect all bridges in graph. + // We need to do it in order to not delete bridges if there are other edges + // exists for deletion. + const bridges = graphBridges(graph); + + // Peek the next edge to delete from graph. + const currentEdges = currentVertex.getEdges(); + /** @var {GraphEdge} edgeToDelete */ + let edgeToDelete = null; + if (currentEdges.length === 1) { + // If there is only one edge left we need to peek it. + [edgeToDelete] = currentEdges; + } else { + // If there are many edges left then we need to peek any of those except bridges. + [edgeToDelete] = currentEdges.filter(edge => !bridges[edge.getKey()]); + } + + // Detect next current vertex. + if (currentVertex.getKey() === edgeToDelete.startVertex.getKey()) { + currentVertex = edgeToDelete.endVertex; + } else { + currentVertex = edgeToDelete.startVertex; + } + + // Delete edge from not visited edges set. + delete notVisitedEdges[edgeToDelete.getKey()]; + + // If last edge were deleted then add finish vertex to Eulerian Path. + if (Object.values(notVisitedEdges).length === 0) { + eulerianPathVertices.push(currentVertex); + } + + // Delete the edge from graph. + graph.deleteEdge(edgeToDelete); + } + + return eulerianPathVertices; +}