diff --git a/README.md b/README.md index 4f5f9242..aedb0471 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,7 @@ a set of rules that precisely defines a sequence of operations. * [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 + * [Travelling Salesman Problem](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/travelling-salesman) - brute force algorithm * **Uncategorized** * [Tower of Hanoi](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/uncategorized/hanoi-tower) * [N-Queens Problem](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/uncategorized/n-queens) @@ -105,6 +106,7 @@ algorithm is an abstraction higher than a computer program. * **Brute Force** - look at all the possibilities and selects the best solution * [Maximum Subarray](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/maximum-subarray) + * [Travelling Salesman Problem](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/travelling-salesman) * **Greedy** - choose the best option at the current time, without any consideration for the future * [Unbound Knapsack Problem](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/sets/knapsack-problem) * [Dijkstra Algorithm](https://github.com/trekhleb/javascript-algorithms/tree/master/src/algorithms/graph/dijkstra) - finding shortest path to all graph vertices diff --git a/src/algorithms/graph/travelling-salesman/README.md b/src/algorithms/graph/travelling-salesman/README.md new file mode 100644 index 00000000..6690cd47 --- /dev/null +++ b/src/algorithms/graph/travelling-salesman/README.md @@ -0,0 +1,26 @@ +# Travelling Salesman Problem + +The travelling salesman problem (TSP) asks the following question: +"Given a list of cities and the distances between each pair of +cities, what is the shortest possible route that visits each city +and returns to the origin city?" + +![Travelling Salesman](https://upload.wikimedia.org/wikipedia/commons/1/11/GLPK_solution_of_a_travelling_salesman_problem.svg) + +Solution of a travelling salesman problem: the black line shows +the shortest possible loop that connects every red dot. + +![Travelling Salesman Graph](https://upload.wikimedia.org/wikipedia/commons/3/30/Weighted_K4.svg) + +TSP can be modelled as an undirected weighted graph, such that +cities are the graph's vertices, paths are the graph's edges, +and a path's distance is the edge's weight. It is a minimization +problem starting and finishing at a specified vertex after having +visited each other vertex exactly once. Often, the model is a +complete graph (i.e. each pair of vertices is connected by an +edge). If no path exists between two cities, adding an arbitrarily +long edge will complete the graph without affecting the optimal tour. + +## References + +- [Wikipedia](https://en.wikipedia.org/wiki/Travelling_salesman_problem) diff --git a/src/algorithms/graph/travelling-salesman/__test__/bfTravellingSalesman.test.js b/src/algorithms/graph/travelling-salesman/__test__/bfTravellingSalesman.test.js new file mode 100644 index 00000000..54eb07dc --- /dev/null +++ b/src/algorithms/graph/travelling-salesman/__test__/bfTravellingSalesman.test.js @@ -0,0 +1,51 @@ +import GraphVertex from '../../../../data-structures/graph/GraphVertex'; +import GraphEdge from '../../../../data-structures/graph/GraphEdge'; +import Graph from '../../../../data-structures/graph/Graph'; +import bfTravellingSalesman from '../bfTravellingSalesman'; + +describe('bfTravellingSalesman', () => { + it('should solve problem for simple graph', () => { + const vertexA = new GraphVertex('A'); + const vertexB = new GraphVertex('B'); + const vertexC = new GraphVertex('C'); + const vertexD = new GraphVertex('D'); + + const edgeAB = new GraphEdge(vertexA, vertexB, 1); + const edgeBD = new GraphEdge(vertexB, vertexD, 1); + const edgeDC = new GraphEdge(vertexD, vertexC, 1); + const edgeCA = new GraphEdge(vertexC, vertexA, 1); + + const edgeBA = new GraphEdge(vertexB, vertexA, 5); + const edgeDB = new GraphEdge(vertexD, vertexB, 8); + const edgeCD = new GraphEdge(vertexC, vertexD, 7); + const edgeAC = new GraphEdge(vertexA, vertexC, 4); + const edgeAD = new GraphEdge(vertexA, vertexD, 2); + const edgeDA = new GraphEdge(vertexD, vertexA, 3); + const edgeBC = new GraphEdge(vertexB, vertexC, 3); + const edgeCB = new GraphEdge(vertexC, vertexB, 9); + + const graph = new Graph(true); + graph + .addEdge(edgeAB) + .addEdge(edgeBD) + .addEdge(edgeDC) + .addEdge(edgeCA) + .addEdge(edgeBA) + .addEdge(edgeDB) + .addEdge(edgeCD) + .addEdge(edgeAC) + .addEdge(edgeAD) + .addEdge(edgeDA) + .addEdge(edgeBC) + .addEdge(edgeCB); + + const salesmanPath = bfTravellingSalesman(graph); + + expect(salesmanPath.length).toBe(4); + + expect(salesmanPath[0].getKey()).toEqual(vertexA.getKey()); + expect(salesmanPath[1].getKey()).toEqual(vertexB.getKey()); + expect(salesmanPath[2].getKey()).toEqual(vertexD.getKey()); + expect(salesmanPath[3].getKey()).toEqual(vertexC.getKey()); + }); +}); diff --git a/src/algorithms/graph/travelling-salesman/bfTravellingSalesman.js b/src/algorithms/graph/travelling-salesman/bfTravellingSalesman.js new file mode 100644 index 00000000..a13fa241 --- /dev/null +++ b/src/algorithms/graph/travelling-salesman/bfTravellingSalesman.js @@ -0,0 +1,104 @@ +/** + * Get all possible paths + * @param {GraphVertex} startVertex + * @param {GraphVertex[][]} [paths] + * @param {GraphVertex[]} [path] + */ +function findAllPaths(startVertex, paths = [], path = []) { + // Clone path. + const currentPath = [...path]; + + // Add startVertex to the path. + currentPath.push(startVertex); + + // Generate visited set from path. + const visitedSet = currentPath.reduce((accumulator, vertex) => { + const updatedAccumulator = { ...accumulator }; + updatedAccumulator[vertex.getKey()] = vertex; + + return updatedAccumulator; + }, {}); + + // Get all unvisited neighbors of startVertex. + const unvisitedNeighbors = startVertex.getNeighbors().filter((neighbor) => { + return !visitedSet[neighbor.getKey()]; + }); + + // If there no unvisited neighbors then treat current path as complete and save it. + if (!unvisitedNeighbors.length) { + paths.push(currentPath); + + return paths; + } + + // Go through all the neighbors. + for (let neighborIndex = 0; neighborIndex < unvisitedNeighbors.length; neighborIndex += 1) { + const currentUnvisitedNeighbor = unvisitedNeighbors[neighborIndex]; + findAllPaths(currentUnvisitedNeighbor, paths, currentPath); + } + + return paths; +} + +/** + * @param {number[][]} adjacencyMatrix + * @param {object} verticesIndices + * @param {GraphVertex[]} cycle + * @return {number} + */ +function getCycleWeight(adjacencyMatrix, verticesIndices, cycle) { + let weight = 0; + + for (let cycleIndex = 1; cycleIndex < cycle.length; cycleIndex += 1) { + const fromVertex = cycle[cycleIndex - 1]; + const toVertex = cycle[cycleIndex]; + const fromVertexIndex = verticesIndices[fromVertex.getKey()]; + const toVertexIndex = verticesIndices[toVertex.getKey()]; + weight += adjacencyMatrix[fromVertexIndex][toVertexIndex]; + } + + return weight; +} + +/** + * BRUTE FORCE approach to solve Traveling Salesman Problem. + * + * @param {Graph} graph + * @return {GraphVertex[]} + */ +export default function bfTravellingSalesman(graph) { + // Pick starting point from where we will traverse the graph. + const startVertex = graph.getAllVertices()[0]; + + // BRUTE FORCE. + // Generate all possible paths from startVertex. + const allPossiblePaths = findAllPaths(startVertex); + + // Filter out paths that are not cycles. + const allPossibleCycles = allPossiblePaths.filter((path) => { + /** @var {GraphVertex} */ + const lastVertex = path[path.length - 1]; + const lastVertexNeighbors = lastVertex.getNeighbors(); + + return lastVertexNeighbors.includes(startVertex); + }); + + // Go through all possible cycles and pick the one with minimum overall tour weight. + const adjacencyMatrix = graph.getAdjacencyMatrix(); + const verticesIndices = graph.getVerticesIndices(); + let salesmanPath = []; + let salesmanPathWeight = null; + for (let cycleIndex = 0; cycleIndex < allPossibleCycles.length; cycleIndex += 1) { + const currentCycle = allPossibleCycles[cycleIndex]; + const currentCycleWeight = getCycleWeight(adjacencyMatrix, verticesIndices, currentCycle); + + // If current cycle weight is smaller then previous ones treat current cycle as most optimal. + if (salesmanPathWeight === null || currentCycleWeight < salesmanPathWeight) { + salesmanPath = currentCycle; + salesmanPathWeight = currentCycleWeight; + } + } + + // Return the solution. + return salesmanPath; +}