UNPKG

tsp-solver-nn

Version:

A Typescript implementation of the Nearest Neighbor with 2-opt and 3-opt optimizations to solve the travelling salesman problem (TSP).

192 lines (191 loc) 6.41 kB
// src/TSPSolverNN.ts var TSPSolverNN = class _TSPSolverNN { #KNEAREST = 10; // used on 3-opt restricted. Specifies how many neighbors to consider. Bigger values may improve results but increase computation time. #neighborList = []; // auxiliary used on 3-opt restricted. #distanceMatrix = []; // represents the distance between each pair of cities. distanceMatrix[i][j] is the distance from city i to city j. /** * Validates the distance matrix for TSP. * Checks for square shape, non-negative values, and no NaNs. */ static isValidDistanceMatrix(distanceMatrix) { if (!Array.isArray(distanceMatrix) || distanceMatrix.length === 0) return false; const n = distanceMatrix.length; for (let i = 0; i < n; i++) { if (!Array.isArray(distanceMatrix[i]) || distanceMatrix[i].length !== n) { return false; } for (let j = 0; j < n; j++) { const value = distanceMatrix[i][j]; if (typeof value !== "number" || isNaN(value) || value < 0) { return false; } } } return true; } /** * Solves the path variant of the Traveling Salesman Problem (TSP), * using Nearest Neighbour + optional 2-opt / 3-opt refinements. */ getPath(distanceMatrix, optimizations = { twoOpt: true, threeOpt: true }) { if (!_TSPSolverNN.isValidDistanceMatrix(distanceMatrix)) { throw new Error("Invalid distance matrix"); } this.#distanceMatrix = distanceMatrix; this.#buildNeighborList(this.#KNEAREST); let route = this.#nearestNeighbour(false); if (optimizations.twoOpt) { route = this.#twoOpt(route, false); } if (optimizations.threeOpt) { route = this.#threeOptRestricted(route, false); } return { route, distance: this.#calculateDistance(route, false) }; } /** * Solves the closed cycle variant of the TSP, * using Nearest Neighbour + optional 2-opt / 3-opt refinements. */ getCycle(distanceMatrix, optimizations = { twoOpt: true, threeOpt: true }) { if (!_TSPSolverNN.isValidDistanceMatrix(distanceMatrix)) { throw new Error("Invalid distance matrix"); } this.#distanceMatrix = distanceMatrix; this.#buildNeighborList(this.#KNEAREST); let route = this.#nearestNeighbour(true); if (optimizations.twoOpt) { route = this.#twoOpt(route, true); } if (optimizations.threeOpt) { route = this.#threeOptRestricted(route, true); } return { route, distance: this.#calculateDistance(route, true) }; } // --- Shared NN builder #nearestNeighbour(closeCycle) { const numPoints = this.#distanceMatrix.length; const visited = new Array(numPoints).fill(false); const route = []; let currentPoint = 0; visited[currentPoint] = true; route.push(currentPoint); for (let i = 1; i < numPoints; i++) { let nearest = -1; let minDistance = Number.POSITIVE_INFINITY; for (let j = 0; j < numPoints; j++) { if (!visited[j] && this.#distanceMatrix[currentPoint][j] < minDistance) { minDistance = this.#distanceMatrix[currentPoint][j]; nearest = j; } } currentPoint = nearest; visited[currentPoint] = true; route.push(currentPoint); } if (closeCycle) route.push(route[0]); return route; } #calculateDistance(route, isCycle) { let distance = 0; for (let i = 0; i < route.length - 1; i++) { distance += this.#distanceMatrix[route[i]][route[i + 1]]; } if (isCycle) { distance += this.#distanceMatrix[route[route.length - 1]][route[0]]; } return distance; } // --- 2-opt refinement #twoOpt(route, isCycle) { let improved = true; while (improved) { improved = false; for (let i = 1; i < route.length - 2; i++) { for (let k = i + 1; k < route.length - 1; k++) { const newRoute = route.slice(); newRoute.splice(i, k - i + 1, ...route.slice(i, k + 1).reverse()); if (this.#calculateDistance(newRoute, isCycle) < this.#calculateDistance(route, isCycle)) { route = newRoute; improved = true; } } } } return route; } // --- Restricted 3-opt refinement #threeOptRestricted(route, isCycle) { let improved = true; while (improved) { improved = false; const n = route.length; for (let i = 0; i < n - 4; i++) { for (let j = i + 2; j < n - 2; j++) { for (let k = j + 2; k < n; k++) { if (!isCycle && i === 0 && k === n - 1) continue; const a = route[i]; const b = route[i + 1]; const c = route[j]; const d = route[j + 1]; const e = route[k]; if (!this.#isNeighbor(a, c) && !this.#isNeighbor(b, d) && !this.#isNeighbor(c, e)) continue; const newRoutes = this.#generate3OptMoves(route, i, j, k); for (const newRoute of newRoutes) { if (this.#calculateDistance(newRoute, isCycle) < this.#calculateDistance(route, isCycle)) { route = newRoute; improved = true; break; } } if (improved) break; } if (improved) break; } if (improved) break; } } return route; } // --- Generate all possible 3-opt reconnections #generate3OptMoves(route, i, j, k) { const A = route.slice(0, i + 1); const B = route.slice(i + 1, j + 1); const C = route.slice(j + 1, k + 1); const D = route.slice(k + 1); return [ [...A, ...B, ...C, ...D], // original [...A, ...B.reverse(), ...C, ...D], [...A, ...B, ...C.reverse(), ...D], [...A, ...B.reverse(), ...C.reverse(), ...D], [...A, ...C, ...B, ...D], [...A, ...C.reverse(), ...B, ...D], [...A, ...C, ...B.reverse(), ...D], [...A, ...C.reverse(), ...B.reverse(), ...D] ]; } #buildNeighborList(k) { const n = this.#distanceMatrix.length; this.#neighborList = []; for (let i = 0; i < n; i++) { const neighbors = [...Array(n).keys()].filter((j) => j !== i).sort((a, b) => this.#distanceMatrix[i][a] - this.#distanceMatrix[i][b]).slice(0, k); this.#neighborList.push(neighbors); } } #isNeighbor(u, v) { return this.#neighborList[u].includes(v); } }; export { TSPSolverNN };