/*
* Copyright © 2011 by Oleg Kovarik. All Rights Reserved
*/
package cz.cvut.felk.cig.jcop.problem.tspfast;
import cz.cvut.felk.cig.jcop.problem.*;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Traveling Salesman Problem - fast implementation.
* <p/>
* TSP problem has a list of cities and every city has distance to every other city. The goal is to find order in which
* to visit every city exactly once and minimize travel distance.
*
* @author Oleg Kovarik
*/
public class FastTSP extends BaseProblem implements StartingConfigurationProblem, GlobalSearchProblem, RandomConfigurationProblem {
/**
* List of all possible {@link FastInvertSubtourOperation}. First operations has source index 0,
* destination index 1..dimension-1. Then comes operation with source 1, destination 2..dimension-1 etc. Last is
* source index = dimension-2, destination dimension-1.
*/
protected List<FastInvertSubtourOperation> fastInvertSubtourOperations;
/**
* Matrix of distances between cities
*/
protected int[][] distances;
/**
* List of nearest neighbors
*/
protected int[][] nearestNeighbors;
/**
* Starting TSP configuration - cities visited in order in which they were created.
*/
protected Configuration startingConfiguration;
/**
* Static meta-data (input data statistics)
*/
protected TSPMetainfoStatic metainfo;
/**
* City coordinates
*/
private ArrayList<Double[]> coordinates;
/**
* Creates new TSP problem from matrix of distances.
* <p/>
* Distances matrix must be n*n, where n is number of cities. Any other matrix (larger, smaller) would raise
* ProblemFormatException.
* <p/>
* distance[i][i] (eg. distance from city to itself) is ignored value. Value -1 stands for impossible path.
*
* @param distances distance matrix. First index is source city, second destination
* @throws ProblemFormatException if distances is not n*n matrix
*/
public FastTSP(Integer[][] distances) throws ProblemFormatException {
StringBuffer labelStringBuffer = new StringBuffer("Dist={");
this.dimension = distances.length;
if (this.dimension < 2)
throw new ProblemFormatException("Dimension must be greater than one, " + this.dimension + " found");
// init starting configuration
List<Integer> startingConfigurationAttributes = new ArrayList<Integer>(this.dimension);
for (int i = 0; i < this.dimension; ++i)
startingConfigurationAttributes.add(i);
this.startingConfiguration = new Configuration(startingConfigurationAttributes, "TSP starting configuration");
this.initOperations();
this.calculateNearestNeighbors(20);
this.metainfo = new TSPMetainfoStatic(this.coordinates, this.distances);
}
/**
* Rounds double to integer using TSPLIB convention.
*
* @param x double to be rounded
* @return rounded value
*/
private int nint(double x) {
return ((x) >= 0 ? (int) Math.floor(x + .5) : (int) -Math.floor(.5 - x));
}
/**
* Loads TSP from a file.
*
* Expected format is:
*
* <pre>
* NAME : eil51
* COMMENT : 51-city problem (Christofides/Eilon)
* TYPE : TSP
* DIMENSION : 51
* EDGE_WEIGHT_TYPE : EUC_2D
* NODE_COORD_SECTION
* 1 37 -52.0
* 2 49 49
* (...)
* 51 30 40
* EOF
* </pre>
*
* Where recognized EDGE_WEIGHT_TYPEs are EUC_2D (using {@link Math#round(float)} and CEIL_2D
* (using {@link Math#ceil(double)}. TYPE has to be TSP. Name and comment are ignored (only added to label).
* Anything after EOF is ignored. Fractional parts of coordinations are ignored.
*
* @param configFile input file to load data from
* @throws IOException if problem loading file occurs
* @throws ProblemFormatException if line format is invalid
*/
public FastTSP(File configFile) throws IOException, ProblemFormatException {
BufferedReader br = new BufferedReader(new FileReader(configFile));
String line;
Pattern configPattern = Pattern.compile("^([A-Za-z0-9_]+) *: *(.*)");
Pattern startPattern = Pattern.compile("^NODE_COORD_SECTION");
Pattern stopPattern = Pattern.compile("^\\s*EOF");
Pattern nodePattern = Pattern.compile("^\\s*(\\d+)\\s+(-?\\d+(?:\\.\\d+)?)\\s+(-?\\d+(?:\\.\\d+)?)\\s*$");
boolean coordSection = false;
int lineCounter = 0;
String name = "";
String comment = "";
String type = "";
String edge = "";
this.dimension = 0;
coordinates = new ArrayList<Double[]>();
while ((line = br.readLine()) != null) {
Matcher m;
lineCounter++;
if (!coordSection) {
// config line
m = configPattern.matcher(line);
if (m.find()) {
if (m.group(1).equals("NAME")) name = m.group(2);
else if (m.group(1).equals("COMMENT")) comment = m.group(2);
else if (m.group(1).equals("TYPE")) type = m.group(2);
else if (m.group(1).equals("DIMENSION")) this.dimension = Integer.valueOf(m.group(2));
else if (m.group(1).equals("EDGE_WEIGHT_TYPE")) edge = m.group(2);
}
// coordinates started
m = startPattern.matcher(line);
if (m.find()) {
this.setLabel (name + " (" + comment + "; " + configFile.getName() + ")");
if (!type.equals("TSP")) {
throw new ProblemFormatException("Type must be TSP, " + type + " found");
}
if (!edge.equals("EUC_2D") && !edge.equals("CEIL_2D")) {
throw new ProblemFormatException("Edge type must be EUC_2D or CEIL_2D, " + edge + " found");
}
if (this.dimension < 2) {
throw new ProblemFormatException("Requires at least 2 cities, " + this.dimension + " found");
}
coordinates.ensureCapacity(this.dimension);
coordSection = true;
}
} else if (coordSection) {
// EOF
m = stopPattern.matcher(line);
if (m.find()) break;
m = nodePattern.matcher(line);
if (!m.find()) {
throw new ProblemFormatException(String.format(
"Line (%s) different from node or EOF found after NODE_COORD_SECTION: %s", line, lineCounter));
}
int index;
index = Integer.valueOf(m.group(1));
Double[] coordinate = {Double.valueOf(m.group(2)), Double.valueOf(m.group(3))};
if (index != coordinates.size() + 1) {
throw new ProblemFormatException(String.format(
"Found index %d on line (%s) \"%s\", expected %d",
index, lineCounter, line, coordinates.size() + 1));
}
coordinates.add(coordinate);
}
}
if (coordinates.size() != this.dimension) {
throw new ProblemFormatException(String.format(
"Required %d coordinates, found %d", this.dimension, coordinates.size()));
}
distances = new int[this.dimension][this.dimension];
for (int i = 0; i < this.dimension; ++i) {
// add distance to target city
for (int j = 0; j < this.dimension; ++j) {
if (i != j) {
int distance;
double z1 = (coordinates.get(i)[0] - coordinates.get(j)[0]) * (coordinates.get(i)[0] - coordinates.get(j)[0]);
double z2 = (coordinates.get(i)[1] - coordinates.get(j)[1]) * (coordinates.get(i)[1] - coordinates.get(j)[1]);
if (edge.equals("CEIL_2D")) {
distance = (int) Math.ceil(Math.sqrt(z1 + z2));
} else {
distance = nint(Math.sqrt(z1 + z2));
}
distances[i][j] = distance;
}
}
}
// init starting configuration
List<Integer> startingConfigurationAttributes = new ArrayList<Integer>(this.dimension);
for (int i = 0; i < this.dimension; ++i)
startingConfigurationAttributes.add(i);
this.startingConfiguration = new Configuration(startingConfigurationAttributes, "TSP starting configuration");
this.calculateNearestNeighbors(20);
this.metainfo = new TSPMetainfoStatic(this.coordinates, this.distances);
this.initOperations();
}
protected void initOperations () {
// prepare switch operation container
this.fastInvertSubtourOperations = new ArrayList<FastInvertSubtourOperation>(this.dimension * (this.dimension - 1) / 2);
for (int i = 0; i < this.dimension; ++i) {
for (int j = 0; j < this.dimension; ++j) {
if (i < this.dimension - 1 && j > i)
this.fastInvertSubtourOperations.add(new FastInvertSubtourOperation(i, j, distances, nearestNeighbors));
}
}
}
/**
* Returns absolute length of a path
*
* @param configuration path
* @return length
*/
public double pathLength (Configuration configuration) {
double distance = 0.0;
for (int i = 1; i < this.dimension; ++i) {
distance += distances[configuration.valueAt(i - 1)][configuration.valueAt(i)];
}
distance += distances[configuration.valueAt(configuration.getDimension() - 1)][configuration.valueAt(0)];
return distance;
}
/** Problem Interface */
public boolean isSolution(Configuration configuration) {
try {
boolean[] presenceMap = new boolean[this.dimension];
Arrays.fill(presenceMap, false);
for (int i = 0; i < this.dimension; ++i) {
if (presenceMap[configuration.valueAt(i)]) return false;
presenceMap[configuration.valueAt(i)] = true;
}
} catch (IndexOutOfBoundsException e) {
return false;
}
return true;
}
public OperationIterator getOperationIterator(Configuration configuration) {
return new FastTSPIterator(configuration, this);
}
public Fitness getDefaultFitness() {
return new FastTSPFitness(this);
}
/* StartingConfigurationProblem interface */
public Configuration getStartingConfiguration() {
return this.startingConfiguration;
}
/* GlobalSearchProblem interface */
public Integer getMaximum(int index) {
return this.distances.length - 1;
}
/* RandomConfigurationProblem interface */
public Configuration getRandomConfiguration() {
List<Integer> integerList = getStartingConfiguration().asList();
Collections.shuffle(integerList);
return new Configuration(integerList, "TSP random configuration");
}
/**
* Calculate nn nearest neighbors for each city (used by various speed up techniques)
*
* @param nn
*/
private void calculateNearestNeighbors(int nn) {
int n = this.dimension;
if ((nn > 0) && (nn < n)) {
nearestNeighbors = new int[n][nn];
Neighbor[] neighbors = new Neighbor[n];
for (int i = 0; i < n; i++) neighbors[i] = new Neighbor();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
neighbors[j].id = j;
neighbors[j].distance = this.distances[i][j];
}
neighbors[i].distance = Integer.MAX_VALUE;
Arrays.sort(neighbors);
for (int j = 0; j < nn; j++) nearestNeighbors[i][j] = neighbors[j].id;
}
}
}
}