org.jgrapht.alg.tour.TwoOptHeuristicTSP Maven / Gradle / Ivy
/*
* (C) Copyright 2018-2021, by Dimitrios Michail and Contributors.
*
* JGraphT : a free Java graph-theory library
*
* See the CONTRIBUTORS.md file distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the
* GNU Lesser General Public License v2.1 or later
* which is available at
* http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
*
* SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
*/
package org.jgrapht.alg.tour;
import org.jgrapht.*;
import org.jgrapht.alg.interfaces.*;
import org.jgrapht.util.*;
import java.util.*;
import static org.jgrapht.util.ArrayUtil.*;
/**
* The 2-opt heuristic algorithm for the TSP 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 exactly once and returns to the origin city?".
*
*
*
* This is an implementation of the 2-opt improvement heuristic algorithm. The algorithm generates
* passes initial tours and then iteratively improves the tours until a local minimum is
* reached. In each iteration it applies the best possible 2-opt move which means to find the best
* pair of edges $(i,i+1)$ and $(j,j+1)$ such that replacing them with $(i,j)$ and $(i+1,j+1)$
* minimizes the tour length. The default initial tours use RandomTour, however an alternative
* algorithm can be provided to create the initial tour. Initial tours generated using
* NearestNeighborHeuristicTSP give good results and performance.
*
*
*
* See wikipedia for more details.
*
*
* This implementation can also be used in order to try to improve an existing tour. See method
* {@link #improveTour(GraphPath)}.
*
* @param the graph vertex type
* @param the graph edge type
*
* @author Dimitrios Michail
* @author Hannes Wellmann
*/
public class TwoOptHeuristicTSP
extends
HamiltonianCycleAlgorithmBase
implements
HamiltonianCycleImprovementAlgorithm
{
private final int passes;
private final HamiltonianCycleAlgorithm initializer;
private final double minCostImprovement;
private Graph graph;
private int n;
private double[][] dist;
private Map index;
private List revIndex;
/**
* Constructor. By default one initial random tour is used.
*/
public TwoOptHeuristicTSP()
{
this(1, new Random());
}
/**
* Constructor
*
* @param passes how many initial random tours to check
*/
public TwoOptHeuristicTSP(int passes)
{
this(passes, new Random());
}
/**
* Constructor
*
* @param passes how many initial random tours to check
* @param seed seed for the random number generator
*/
public TwoOptHeuristicTSP(int passes, long seed)
{
this(passes, new Random(seed));
}
/**
* Constructor
*
* @param passes how many initial random tours to check
* @param rng random number generator
*/
public TwoOptHeuristicTSP(int passes, Random rng)
{
this(passes, new RandomTourTSP<>(rng));
}
/**
* Constructor
*
* @param passes how many initial random tours to check
* @param rng random number generator
* @param minCostImprovement Minimum cost improvement per iteration
*/
public TwoOptHeuristicTSP(int passes, Random rng, double minCostImprovement)
{
this(passes, new RandomTourTSP<>(rng), minCostImprovement);
}
/**
* Constructor
*
* @param initializer Algorithm to generate initial tour
*/
public TwoOptHeuristicTSP(HamiltonianCycleAlgorithm initializer)
{
this(1, initializer);
}
/**
* Constructor
*
* @param passes how many initial tours to check
* @param initializer Algorithm to generate initial tour
*/
public TwoOptHeuristicTSP(int passes, HamiltonianCycleAlgorithm initializer)
{
this(passes, initializer, 1e-8);
}
/**
* Constructor
*
* @param passes how many initial tours to check
* @param initializer Algorithm to generate initial tours
* @param minCostImprovement Minimum cost improvement per iteration
*/
public TwoOptHeuristicTSP(
int passes, HamiltonianCycleAlgorithm initializer, double minCostImprovement)
{
if (passes < 1) {
throw new IllegalArgumentException("passes must be at least one");
}
this.passes = passes;
this.initializer =
Objects.requireNonNull(initializer, "Initial solver algorithm cannot be null");
this.minCostImprovement = Math.abs(minCostImprovement);
}
// algorithm
/**
* Computes a 2-approximate tour.
*
* @param graph the input graph
* @return a tour
* @throws IllegalArgumentException if the graph is not undirected
* @throws IllegalArgumentException if the graph is not complete
* @throws IllegalArgumentException if the graph contains no vertices
*/
@Override
public GraphPath getTour(Graph graph)
{
checkGraph(graph);
if (graph.vertexSet().size() == 1) {
return getSingletonTour(graph);
}
// Initialize vertex index and distances
init(graph);
// Execute 2-opt for the specified number of passes and a new permutation in each pass
GraphPath best = tourToPath(improve(createInitialTour()));
for (int i = 1; i < passes; i++) {
GraphPath other = tourToPath(improve(createInitialTour()));
if (other.getWeight() < best.getWeight()) {
best = other;
}
}
return best;
}
/**
* Try to improve a tour by running the 2-opt heuristic.
*
* @param tour a tour
* @return a possibly improved tour
*/
@Override
public GraphPath improveTour(GraphPath tour)
{
init(tour.getGraph());
return tourToPath(improve(pathToTour(tour)));
}
/**
* Initialize graph and mapping to integer vertices.
*
* @param graph the input graph
*/
private void init(Graph graph)
{
this.graph = graph;
this.n = graph.vertexSet().size();
this.dist = new double[n][n];
VertexToIntegerMapping vertex2index = new VertexToIntegerMapping<>(graph.vertexSet());
this.index = vertex2index.getVertexMap();
this.revIndex = vertex2index.getIndexList();
for (E e : graph.edgeSet()) {
V s = graph.getEdgeSource(e);
int si = index.get(s);
V t = graph.getEdgeTarget(e);
int ti = index.get(t);
double weight = graph.getEdgeWeight(e);
dist[si][ti] = weight;
dist[ti][si] = weight;
}
}
/**
* Create an initial tour
*
* @return a complete tour
*/
private int[] createInitialTour()
{
return pathToTour(initializer.getTour(graph));
}
/**
* Improve the tour using the 2-opt heuristic. In each iteration it applies the best possible
* 2-opt move which means to find the best pair of edges $(i,i+1)$ and $(j,j+1)$ such that
* replacing them with $(i,j)$ and $(i+1,j+1)$ minimizes the tour length.
*
*
* The returned array instance might or might not be the input array.
*
* @param tour the input tour
* @return a possibly improved tour
*/
private int[] improve(int[] tour)
{
double minChange;
while (true) {
minChange = -minCostImprovement;
int mini = -1;
int minj = -1;
for (int i = 0; i < n - 2; i++) {
for (int j = i + 2; j < n; j++) {
int ci = tour[i];
int ci1 = tour[i + 1];
int cj = tour[j];
int cj1 = tour[j + 1];
double change = dist[ci][cj] + dist[ci1][cj1] - dist[ci][ci1] - dist[cj][cj1];
if (change < minChange) {
minChange = change;
mini = i;
minj = j;
}
}
}
if (mini != -1 && minj != -1) {
// apply move: reverse path from mini+1 to minj (both inclusive)
reverse(tour, mini + 1, minj);
} else {
return tour;
}
}
}
/**
* Transform from an array representation to a graph path.
*
* @param tour an array containing the index of the vertices of the tour
* @return a graph path
*/
private GraphPath tourToPath(int[] tour)
{
List tourVertices = new ArrayList<>(n + 1);
for (int vi : tour) {
V v = revIndex.get(vi);
tourVertices.add(v);
}
return closedVertexListToTour(tourVertices, graph);
}
/**
* Transform from a path representation to an array representation.
*
* @param path graph path
* @return an array containing the index of the vertices of the tour
*/
private int[] pathToTour(GraphPath path)
{
boolean[] visited = new boolean[n];
List vertexList = path.getVertexList(); // first and last element are the starting vertex
if (vertexList.size() != n + 1) {
throw new IllegalArgumentException("Not a valid tour");
}
int[] tour = new int[n + 1];
for (int i = 0; i < n; i++) {
int vi = index.get(vertexList.get(i));
if (visited[vi]) {
throw new IllegalArgumentException("Not a valid tour");
}
visited[vi] = true;
tour[i] = vi;
}
tour[n] = tour[0];
return tour;
}
}