org.jgrapht.alg.tour.NearestInsertionHeuristicTSP Maven / Gradle / Ivy
/*
* (C) Copyright 2019-2023, by Peter Harman 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 java.util.*;
import java.util.stream.*;
/**
* The nearest insertion 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?".
*
*
*
* Insertion heuristics are quite straightforward, and there are many variants to choose from. The
* basics of insertion heuristics is to start with a tour of a subset of all cities, and then
* inserting the rest by some heuristic. The initial sub-tour is often a triangle or the convex
* hull. One can also start with a single edge as sub-tour. This implementation uses the shortest
* edge by default as the initial sub-tour.
*
*
*
* The implementation of this class is based on:
* Nilsson, Christian. "Heuristics for the traveling salesman problem." Linkoping University 38
* (2003)
*
*
*
* This implementation can also be used in order to augment an existing partial tour. See
* constructor {@link #NearestInsertionHeuristicTSP(GraphPath)}.
*
*
*
* The runtime complexity of this class is $O(V^2)$.
*
*
*
* This algorithm requires that the graph is complete.
*
*
* @param the graph vertex type
* @param the graph edge type
*
* @author Peter Harman
*/
public class NearestInsertionHeuristicTSP
extends HamiltonianCycleAlgorithmBase
{
private GraphPath subtour;
/**
* Constructor. By default a sub-tour is chosen based on the shortest edge
*/
public NearestInsertionHeuristicTSP()
{
this(null);
}
/**
* Constructor
*
* Specifies an existing sub-tour that will be augmented to form a complete tour when
* {@link #getTour(org.jgrapht.Graph) } is called
*
* @param subtour Initial sub-tour, or null to start with shortest edge
*/
public NearestInsertionHeuristicTSP(GraphPath subtour)
{
this.subtour = subtour;
}
/**
* Computes a tour using the nearest insertion heuristic.
*
* @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
* @throws IllegalArgumentException If the specified sub-tour is for a different Graph instance
* @throws IllegalArgumentException If the graph does not contain specified sub-tour vertices
* @throws IllegalArgumentException If the graph does not contain specified sub-tour edges
*/
@Override
public GraphPath getTour(Graph graph)
{
checkGraph(graph);
if (graph.vertexSet().size() == 1) {
return getSingletonTour(graph);
}
return vertexListToTour(augment(subtour(graph), graph), graph);
}
/**
* Get or create a sub-tour to start augmenting
*
* @param graph The graph
* @return Vertices of an initial sub-tour
* @throws IllegalArgumentException If the specified sub-tour is for a different Graph instance
* @throws IllegalArgumentException If the graph does not contain specified sub-tour vertices
* @throws IllegalArgumentException If the graph does not contain specified sub-tour edges
*/
private List subtour(Graph graph)
{
List subtourVertices = new ArrayList<>();
if (subtour != null) {
if (subtour.getGraph() != null && !graph.equals(subtour.getGraph())) {
throw new IllegalArgumentException(
"Specified sub-tour is for a different Graph instance");
}
if (!graph.vertexSet().containsAll(subtour.getVertexList())) {
throw new IllegalArgumentException(
"Graph does not contain specified sub-tour vertices");
}
if (!graph.edgeSet().containsAll(subtour.getEdgeList())) {
throw new IllegalArgumentException(
"Graph does not contain specified sub-tour edges");
}
if (subtour.getStartVertex().equals(subtour.getEndVertex())) {
subtourVertices
.addAll(subtour.getVertexList().subList(1, subtour.getVertexList().size()));
} else {
subtourVertices.addAll(subtour.getVertexList());
}
}
if (subtourVertices.isEmpty()) {
// If no initial subtour exists, create one based on the shortest edge
E shortestEdge = Collections.min(
graph.edgeSet(),
(e1, e2) -> Double.compare(graph.getEdgeWeight(e1), graph.getEdgeWeight(e2)));
subtourVertices.add(graph.getEdgeSource(shortestEdge));
subtourVertices.add(graph.getEdgeTarget(shortestEdge));
}
return subtourVertices;
}
/**
* Initialise the Map storing closest unvisited vertex for each tour vertex
*
* @param tourVertices Current tour vertices
* @param unvisited Set of unvisited vertices
* @param graph The graph
* @return Map storing closest unvisited vertex for each tour vertex
*/
private Map> getClosest(List tourVertices, Set unvisited, Graph graph)
{
return tourVertices
.stream().collect(Collectors.toMap(v -> v, v -> getClosest(v, unvisited, graph)));
}
/**
* Determines closest unvisited vertex to a vertex in the current tour
*
* @param tourVertex Vertex in the current tour
* @param unvisited Set of vertices not in the current tour
* @param graph The graph
* @return Closest unvisited vertex
*/
private Closest getClosest(V tourVertex, Set unvisited, Graph graph)
{
V closest = null;
double minDist = Double.MAX_VALUE;
for (V unvisitedVertex : unvisited) {
double vDist = graph.getEdgeWeight(graph.getEdge(tourVertex, unvisitedVertex));
if (vDist < minDist) {
closest = unvisitedVertex;
minDist = vDist;
}
}
return new Closest<>(tourVertex, closest, minDist);
}
/**
* Update the Map storing closest unvisited vertex for each tour vertex
*
* @param currentClosest Map storing closest unvisited vertex for each tour vertex
* @param chosen Latest vertex added to tour
* @param unvisited Set of unvisited vertices
* @param graph The graph
*/
private void updateClosest(
Map> currentClosest, Closest chosen, Set unvisited, Graph graph)
{
// Update the set of unvisited vertices, and exit if none remain
unvisited.remove(chosen.getUnvisitedVertex());
if (unvisited.isEmpty()) {
currentClosest.clear();
return;
}
// Update any entries impacted by the choice of new vertex
currentClosest.replaceAll((v, c) -> {
if (chosen.getTourVertex().equals(v)
|| chosen.getUnvisitedVertex().equals(c.getUnvisitedVertex()))
{
return getClosest(v, unvisited, graph);
}
return c;
});
currentClosest.put(
chosen.getUnvisitedVertex(), getClosest(chosen.getUnvisitedVertex(), unvisited, graph));
}
/**
* Chooses the closest unvisited vertex to the sub-tour
*
* @param closestVertices Map storing closest unvisited vertex for each tour vertex
* @return First result of sorting values
*/
private Closest chooseClosest(Map> closestVertices)
{
return Collections.min(closestVertices.values());
}
/**
* Augment an existing tour to give a complete tour
*
* @param subtour The vertices of the existing tour
* @param graph The graph
* @return List of vertices representing the complete tour
*/
private List augment(List subtour, Graph graph)
{
Set unvisited = new HashSet<>(graph.vertexSet());
unvisited.removeAll(subtour);
return augment(subtour, getClosest(subtour, unvisited, graph), unvisited, graph);
}
/**
* Augment an existing tour to give a complete tour
*
* @param subtour The vertices of the existing tour
* @param closestVertices Map of data for closest unvisited vertices
* @param unvisited Set of unvisited vertices
* @param graph The graph
* @return List of vertices representing the complete tour
*/
private List augment(
List subtour, Map> closestVertices, Set unvisited, Graph graph)
{
while (!unvisited.isEmpty()) {
// Select a city not in the subtour, having the shortest distance to any one of the
// cities in the subtoor.
Closest closestVertex = chooseClosest(closestVertices);
// Determine the vertices either side of the selected tour vertex
int i = subtour.indexOf(closestVertex.getTourVertex());
V vertexBefore = subtour.get(i == 0 ? subtour.size() - 1 : i - 1);
V vertexAfter = subtour.get(i == subtour.size() - 1 ? 0 : i + 1);
// Find an edge in the subtour such that the cost of inserting the selected city between
// the edge’s cities will be minimal.
// Making assumption this is a neighbouring edge, test the edges before and after
double insertionCostBefore =
graph.getEdgeWeight(graph.getEdge(vertexBefore, closestVertex.getUnvisitedVertex()))
+ closestVertex.getDistance() - graph
.getEdgeWeight(graph.getEdge(vertexBefore, closestVertex.getTourVertex()));
double insertionCostAfter =
graph.getEdgeWeight(graph.getEdge(vertexAfter, closestVertex.getUnvisitedVertex()))
+ closestVertex.getDistance() - graph
.getEdgeWeight(graph.getEdge(vertexAfter, closestVertex.getTourVertex()));
// Add the selected vertex to the tour
if (insertionCostBefore < insertionCostAfter) {
subtour.add(i, closestVertex.getUnvisitedVertex());
} else {
subtour.add(i + 1, closestVertex.getUnvisitedVertex());
}
// Repeat until no more cities remain
updateClosest(closestVertices, closestVertex, unvisited, graph);
}
return subtour;
}
/**
* Class holding data for the closest unvisited vertex to a particular vertex in the tour.
*
* @param vertex type
*/
private static class Closest
implements Comparable>
{
private final V tourVertex;
private final V unvisitedVertex;
private final double distance;
Closest(V tourVertex, V unvisitedVertex, double distance)
{
this.tourVertex = tourVertex;
this.unvisitedVertex = unvisitedVertex;
this.distance = distance;
}
public V getTourVertex()
{
return tourVertex;
}
public V getUnvisitedVertex()
{
return unvisitedVertex;
}
public double getDistance()
{
return distance;
}
@Override
public int compareTo(Closest o)
{
return Double.compare(distance, o.distance);
}
}
}