From 569c6ae452dc014f34c6a45b8549dfca6b69d7f2 Mon Sep 17 00:00:00 2001 From: Oleksii Trekhleb Date: Thu, 17 May 2018 07:40:13 +0300 Subject: [PATCH] Add Hamiltonian cycle. --- README.md | 4 +- .../graph/hamiltonian-cycle/README.md | 48 +++++++ .../__test__/hamiltonianCycle.test.js | 90 ++++++++++++ .../hamiltonian-cycle/hamiltonianCycle.js | 133 ++++++++++++++++++ 4 files changed, 274 insertions(+), 1 deletion(-) create mode 100644 src/algorithms/graph/hamiltonian-cycle/README.md create mode 100644 src/algorithms/graph/hamiltonian-cycle/__test__/hamiltonianCycle.test.js create mode 100644 src/algorithms/graph/hamiltonian-cycle/hamiltonianCycle.js diff --git a/README.md b/README.md index 16b0395a..30f808e3 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,8 @@ * [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) - Fleury's algorithm - Visit every edge once + * [Eulerian Path and Eulerian Circuit](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/eulerian-path) - Fleury's algorithm - Visit every edge exactly once + * [Hamiltonian Cycle](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/hamiltonian-cycle) - Visit every vertex exactly once * [Strongly Connected Components](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/strongly-connected-components) - Kosaraju's algorithm * **Uncategorized** * [Tower of Hanoi](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/uncategorized/hanoi-tower) @@ -111,6 +112,7 @@ * [Maximum Subarray](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/maximum-subarray) * [Bellman-Ford Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/bellman-ford) - finding shortest path to all graph vertices * **Backtracking** + * [Hamiltonian Cycle](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/hamiltonian-cycle) - Visit every vertex exactly once * [N-Queens Problem](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/uncategorized/n-queens) * **Branch & Bound** diff --git a/src/algorithms/graph/hamiltonian-cycle/README.md b/src/algorithms/graph/hamiltonian-cycle/README.md new file mode 100644 index 00000000..74d8ed15 --- /dev/null +++ b/src/algorithms/graph/hamiltonian-cycle/README.md @@ -0,0 +1,48 @@ +# Hamiltonian Path + +**Hamiltonian path** (or **traceable path**) is a path in an +undirected or directed graph that visits each vertex exactly once. +A **Hamiltonian cycle** (or **Hamiltonian circuit**) is a +Hamiltonian path that is a cycle. Determining whether such paths +and cycles exist in graphs is the **Hamiltonian path problem**. + +![Hamiltonian cycle](https://upload.wikimedia.org/wikipedia/commons/6/6c/Hamiltonian_path_3d.svg) + +One possible Hamiltonian cycle through every vertex of a +dodecahedron is shown in red – like all platonic solids, the +dodecahedron is Hamiltonian. + +## Naive Algorithm + +Generate all possible configurations of vertices and print a +configuration that satisfies the given constraints. There +will be `n!` (n factorial) configurations. + +``` +while there are untried configurations +{ + generate the next configuration + if ( there are edges between two consecutive vertices of this + configuration and there is an edge from the last vertex to + the first ). + { + print this configuration; + break; + } +} +``` + +## Backtracking Algorithm + +Create an empty path array and add vertex `0` to it. Add other +vertices, starting from the vertex `1`. Before adding a vertex, +check for whether it is adjacent to the previously added vertex +and not already added. If we find such a vertex, we add the +vertex as part of the solution. If we do not find a vertex +then we return false. + +## References + +- [Hamiltonian path on Wikipedia](https://en.wikipedia.org/wiki/Hamiltonian_path) +- [Hamiltonian path on YouTube](https://www.youtube.com/watch?v=dQr4wZCiJJ4) +- [Hamiltonian cycle on GeeksForGeeks](https://www.geeksforgeeks.org/backtracking-set-7-hamiltonian-cycle/) diff --git a/src/algorithms/graph/hamiltonian-cycle/__test__/hamiltonianCycle.test.js b/src/algorithms/graph/hamiltonian-cycle/__test__/hamiltonianCycle.test.js new file mode 100644 index 00000000..eab0d333 --- /dev/null +++ b/src/algorithms/graph/hamiltonian-cycle/__test__/hamiltonianCycle.test.js @@ -0,0 +1,90 @@ +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import Graph from '../../../../data-structures/graph/Graph'; +import hamiltonianCycle from '../hamiltonianCycle'; + +describe('hamiltonianCycle', () => { + it('should find hamiltonian paths 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 edgeAB = new GraphEdge(vertexA, vertexB); + const edgeAE = new GraphEdge(vertexA, vertexE); + const edgeAC = new GraphEdge(vertexA, vertexC); + const edgeBE = new GraphEdge(vertexB, vertexE); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeBD = new GraphEdge(vertexB, vertexD); + const edgeCD = new GraphEdge(vertexC, vertexD); + const edgeDE = new GraphEdge(vertexD, vertexE); + + const graph = new Graph(); + graph + .addEdge(edgeAB) + .addEdge(edgeAE) + .addEdge(edgeAC) + .addEdge(edgeBE) + .addEdge(edgeBC) + .addEdge(edgeBD) + .addEdge(edgeCD) + .addEdge(edgeDE); + + const hamiltonianCycleSet = hamiltonianCycle(graph); + + expect(hamiltonianCycleSet.length).toBe(8); + + expect(hamiltonianCycleSet[0][0].getKey()).toBe(vertexA.getKey()); + expect(hamiltonianCycleSet[0][1].getKey()).toBe(vertexB.getKey()); + expect(hamiltonianCycleSet[0][2].getKey()).toBe(vertexE.getKey()); + expect(hamiltonianCycleSet[0][3].getKey()).toBe(vertexD.getKey()); + expect(hamiltonianCycleSet[0][4].getKey()).toBe(vertexC.getKey()); + + expect(hamiltonianCycleSet[1][0].getKey()).toBe(vertexA.getKey()); + expect(hamiltonianCycleSet[1][1].getKey()).toBe(vertexB.getKey()); + expect(hamiltonianCycleSet[1][2].getKey()).toBe(vertexC.getKey()); + expect(hamiltonianCycleSet[1][3].getKey()).toBe(vertexD.getKey()); + expect(hamiltonianCycleSet[1][4].getKey()).toBe(vertexE.getKey()); + + expect(hamiltonianCycleSet[2][0].getKey()).toBe(vertexA.getKey()); + expect(hamiltonianCycleSet[2][1].getKey()).toBe(vertexE.getKey()); + expect(hamiltonianCycleSet[2][2].getKey()).toBe(vertexB.getKey()); + expect(hamiltonianCycleSet[2][3].getKey()).toBe(vertexD.getKey()); + expect(hamiltonianCycleSet[2][4].getKey()).toBe(vertexC.getKey()); + + expect(hamiltonianCycleSet[3][0].getKey()).toBe(vertexA.getKey()); + expect(hamiltonianCycleSet[3][1].getKey()).toBe(vertexE.getKey()); + expect(hamiltonianCycleSet[3][2].getKey()).toBe(vertexD.getKey()); + expect(hamiltonianCycleSet[3][3].getKey()).toBe(vertexB.getKey()); + expect(hamiltonianCycleSet[3][4].getKey()).toBe(vertexC.getKey()); + }); + + it('should return false for graph without Hamiltonian path', () => { + 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 edgeAE = new GraphEdge(vertexA, vertexE); + const edgeBE = new GraphEdge(vertexB, vertexE); + const edgeBC = new GraphEdge(vertexB, vertexC); + const edgeBD = new GraphEdge(vertexB, vertexD); + const edgeCD = new GraphEdge(vertexC, vertexD); + + const graph = new Graph(); + graph + .addEdge(edgeAB) + .addEdge(edgeAE) + .addEdge(edgeBE) + .addEdge(edgeBC) + .addEdge(edgeBD) + .addEdge(edgeCD); + + const hamiltonianCycleSet = hamiltonianCycle(graph); + + expect(hamiltonianCycleSet.length).toBe(0); + }); +}); diff --git a/src/algorithms/graph/hamiltonian-cycle/hamiltonianCycle.js b/src/algorithms/graph/hamiltonian-cycle/hamiltonianCycle.js new file mode 100644 index 00000000..9f73a564 --- /dev/null +++ b/src/algorithms/graph/hamiltonian-cycle/hamiltonianCycle.js @@ -0,0 +1,133 @@ +import GraphVertex from '../../../data-structures/graph/GraphVertex'; + +/** + * @param {number[][]} adjacencyMatrix + * @param {object} verticesIndices + * @param {GraphVertex[]} cycle + * @param {GraphVertex} vertexCandidate + * @return {boolean} + */ +function isSafe(adjacencyMatrix, verticesIndices, cycle, vertexCandidate) { + const endVertex = cycle[cycle.length - 1]; + + // Get end and candidate vertices indices in adjacency matrix. + const candidateVertexAdjacencyIndex = verticesIndices[vertexCandidate.getKey()]; + const endVertexAdjacencyIndex = verticesIndices[endVertex.getKey()]; + + // Check if last vertex in the path and candidate vertex are adjacent. + if (!adjacencyMatrix[endVertexAdjacencyIndex][candidateVertexAdjacencyIndex]) { + return false; + } + + // Check if vertexCandidate is being added to the path for the first time. + const candidateDuplicate = cycle.find(vertex => vertex.getKey() === vertexCandidate.getKey()); + + return !candidateDuplicate; +} + +/** + * @param {number[][]} adjacencyMatrix + * @param {object} verticesIndices + * @param {GraphVertex[]} cycle + * @return {boolean} + */ +function isCycle(adjacencyMatrix, verticesIndices, cycle) { + // Check if first and last vertices in hamiltonian path are adjacent. + + // Get start and end vertices from the path. + const startVertex = cycle[0]; + const endVertex = cycle[cycle.length - 1]; + + // Get start/end vertices indices in adjacency matrix. + const startVertexAdjacencyIndex = verticesIndices[startVertex.getKey()]; + const endVertexAdjacencyIndex = verticesIndices[endVertex.getKey()]; + + // Check if we can go from end vertex to the start one. + return !!adjacencyMatrix[endVertexAdjacencyIndex][startVertexAdjacencyIndex]; +} + +/** + * @param {number[][]} adjacencyMatrix + * @param {GraphVertex[]} vertices + * @param {object} verticesIndices + * @param {GraphVertex[][]} cycles + * @param {GraphVertex[]} cycle + */ +function hamiltonianCycleRecursive({ + adjacencyMatrix, + vertices, + verticesIndices, + cycles, + cycle, +}) { + // Clone cycle in order to prevent it from modification by other DFS branches. + const currentCycle = [...cycle].map(vertex => new GraphVertex(vertex.value)); + + if (vertices.length === currentCycle.length) { + // Hamiltonian path is found. + // Now we need to check if it is cycle or not. + if (isCycle(adjacencyMatrix, verticesIndices, currentCycle)) { + // Another solution has been found. Save it. + cycles.push(currentCycle); + } + return; + } + + for (let vertexIndex = 0; vertexIndex < vertices.length; vertexIndex += 1) { + // Get vertex candidate that we will try to put into next path step and see if it fits. + const vertexCandidate = vertices[vertexIndex]; + + // Check if it is safe to put vertex candidate to cycle. + if (isSafe(adjacencyMatrix, verticesIndices, currentCycle, vertexCandidate)) { + // Add candidate vertex to cycle path. + currentCycle.push(vertexCandidate); + + // Try to find other vertices in cycle. + hamiltonianCycleRecursive({ + adjacencyMatrix, + vertices, + verticesIndices, + cycles, + cycle: currentCycle, + }); + + // Remove candidate vertex from cycle path in order to try another one. + currentCycle.pop(); + } + } +} + +/** + * @param {Graph} graph + * @return {GraphVertex[][]} + */ +export default function hamiltonianCycle(graph) { + // Gather some information about the graph that we will need to during + // the problem solving. + const verticesIndices = graph.getVerticesIndices(); + const adjacencyMatrix = graph.getAdjacencyMatrix(); + const vertices = graph.getAllVertices(); + + // Define start vertex. We will always pick the first one + // this it doesn't matter which vertex to pick in a cycle. + // Every vertex is in a cycle so we can start from any of them. + const startVertex = vertices[0]; + + // Init cycles array that will hold all solutions. + const cycles = []; + + // Init cycle array that will hold current cycle path. + const cycle = [startVertex]; + + // Try to find cycles recursively in Depth First Search order. + hamiltonianCycleRecursive({ + adjacencyMatrix, + vertices, + verticesIndices, + cycles, + cycle, + }); + + // Return found cycles. + return cycles; +}