UNPKG

lin-kernighan

Version:

Adaptive k-opt heuristic improving TSP tours by swapping edges to shorten routes.

195 lines (194 loc) 7.76 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "default", { enumerable: true, get: function() { return _default; } }); const _haversinematrix = require("haversine-matrix"); /** * Calculates the total distance of a tour by summing distances between consecutive cities. * @param tour Array of city indices representing the tour order * @param distanceMatrix 2D array containing distances between all city pairs * @returns Total distance of the tour */ const calculateTourDistance = (tour, distanceMatrix)=>{ let totalDistance = 0; for(let i = 0; i < tour.length; i++){ const from = tour[i]; const to = tour[(i + 1) % tour.length]; totalDistance += distanceMatrix[from][to]; } return totalDistance; }; /** * Generates an initial tour using the nearest neighbor heuristic. * Starts from city 0 and always visits the nearest unvisited city next. * @param distanceMatrix 2D array containing distances between all city pairs * @returns Array of city indices representing the initial tour */ const nearestNeighborTour = (distanceMatrix)=>{ const n = distanceMatrix.length; const visited = new Array(n).fill(false); const tour = [ 0 ]; visited[0] = true; for(let i = 1; i < n; i++){ let nearest = -1; let minDistance = Infinity; for(let j = 0; j < n; j++){ if (!visited[j] && distanceMatrix[tour[tour.length - 1]][j] < minDistance) { minDistance = distanceMatrix[tour[tour.length - 1]][j]; nearest = j; } } tour.push(nearest); visited[nearest] = true; } return tour; }; /** * Calculates the improvement delta for a 2-opt swap without performing it. * This avoids recalculating the entire tour distance. * @param tour Current tour as array of city indices * @param i Start position for the segment to reverse * @param k End position for the segment to reverse * @param distanceMatrix 2D array containing distances between all city pairs * @returns The change in tour distance (negative if improvement) */ const calculate2OptDelta = (tour, i, k, distanceMatrix)=>{ const n = tour.length; // Current edges that will be removed const edge1From = tour[i]; const edge1To = tour[(i + 1) % n]; const edge2From = tour[k]; const edge2To = tour[(k + 1) % n]; // New edges that will be added const newEdge1Distance = distanceMatrix[edge1From][edge2From]; const newEdge2Distance = distanceMatrix[edge1To][edge2To]; // Current edges that will be removed const oldEdge1Distance = distanceMatrix[edge1From][edge1To]; const oldEdge2Distance = distanceMatrix[edge2From][edge2To]; return newEdge1Distance + newEdge2Distance - (oldEdge1Distance + oldEdge2Distance); }; /** * Performs a 2-opt swap by reversing the segment between two positions in-place. * This modifies the tour array directly to avoid memory allocations. * @param tour Current tour as array of city indices (modified in-place) * @param i Start position for the segment to reverse * @param k End position for the segment to reverse */ const twoOptSwapInPlace = (tour, i, k)=>{ // Reverse the segment between i+1 and k let left = i + 1; let right = k; while(left < right){ [tour[left], tour[right]] = [ tour[right], tour[left] ]; left++; right--; } }; /** * Implements the Lin-Kernighan algorithm to optimize the tour. * Uses 2-opt and 3-opt edge swapping to find better tour configurations. * @param tour Initial tour as array of city indices * @param distanceMatrix 2D array containing distances between all city pairs * @returns Optimized tour as array of city indices */ const linKernighan = (tour, distanceMatrix)=>{ const n = tour.length; let bestTour = [ ...tour ]; let bestDistance = calculateTourDistance(bestTour, distanceMatrix); let improved = true; while(improved){ improved = false; // 2-opt improvements with delta calculation and early termination for(let i = 0; i < n - 1; i++){ for(let k = i + 1; k < n; k++){ const delta = calculate2OptDelta(bestTour, i, k, distanceMatrix); if (delta < 0) { // Apply the improvement in-place twoOptSwapInPlace(bestTour, i, k); bestDistance += delta; improved = true; break; // Early termination - restart with improved tour } } if (improved) break; // Break outer loop too } // 3-opt improvements (more complex edge swapping) if (!improved) { for(let i = 0; i < n - 2; i++){ for(let j = i + 1; j < n - 1; j++){ for(let k = j + 1; k < n; k++){ // Try different 3-opt reconnection patterns const segments = [ bestTour.slice(0, i + 1), bestTour.slice(i + 1, j + 1), bestTour.slice(j + 1, k + 1), bestTour.slice(k + 1) ]; // Pattern 1: reverse middle segment const option1 = [ ...segments[0], ...segments[1].reverse(), ...segments[2], ...segments[3] ]; // Pattern 2: reverse last segment const option2 = [ ...segments[0], ...segments[1], ...segments[2].reverse(), ...segments[3] ]; // Pattern 3: swap middle segments const option3 = [ ...segments[0], ...segments[2], ...segments[1], ...segments[3] ]; const options = [ option1, option2, option3 ]; for (const option of options){ const distance = calculateTourDistance(option, distanceMatrix); if (distance < bestDistance) { bestTour = option; bestDistance = distance; improved = true; } } } } } } } return bestTour; }; /** * Solves the Traveling Salesman Problem using the Lin-Kernighan algorithm. * Takes geographic coordinates and returns an optimized tour order. * @param points Array of geographic points with latitude and longitude * @returns Array of points in optimized tour order */ const tsp = (points)=>{ if (points.length <= 1 || points.length === 2) { return points; } const distanceMatrix = (0, _haversinematrix.matrix)(points); // Generate initial tour using nearest neighbor heuristic let tour = nearestNeighborTour(distanceMatrix); // Optimize tour using Lin-Kernighan algorithm tour = linKernighan(tour, distanceMatrix); // Return points in optimized order return tour.map((index)=>points[index]); }; const _default = tsp; //# sourceMappingURL=index.js.map