/* * Copyright © 2010 by Ondrej Skalicka. All Rights Reserved */ package cz.cvut.felk.cig.jcop.problem.tsp; 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. * <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 Ondrej Skalicka */ public class TSP extends BaseProblem implements StartingConfigurationProblem, GlobalSearchProblem, RandomConfigurationProblem { /** * List of all possible {@link SwitchCityOperation MoveCityOperations}. 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<SwitchCityOperation> switchCityOperations; /** * List of all cities in problem. */ protected List<City> cities; /** * Starting TSP configuration - cities visited in order in which they were created. */ protected Configuration startingConfiguration; /** * 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 TSP(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"); // prepare new cities this.cities = new ArrayList<City>(this.dimension); for (int i = 0; i < this.dimension; ++i) { this.cities.add(new City(i, this.dimension - 1)); } // parse distances for (int i = 0; i < this.dimension; ++i) { City city = this.cities.get(i); if (distances[i].length != this.dimension) { throw new ProblemFormatException(String.format( "Distance matrix' distances[%d].length is not %d, %d found.", i, this.dimension, distances[i].length)); } labelStringBuffer.append("{"); // add distance to target city for (int j = 0; j < this.dimension; ++j) { labelStringBuffer.append(j == 0 ? "" : ",").append(distances[i][j]); if (i != j) city.addDistance(this.cities.get(j), distances[i][j]); } labelStringBuffer.append("}"); } labelStringBuffer.append("}"); this.setLabel(labelStringBuffer.toString()); // 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(); } /** * 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 TSP(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; ArrayList<Double[]> 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 (%d) different from node or EOF found after NODE_COORD_SECTION: %s", lineCounter, line)); } 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 (%d) \"%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 %s", this.dimension, coordinates.size())); } this.cities = new ArrayList<City>(this.dimension); for (int i = 0; i < this.dimension; ++i) { this.cities.add(new City(i, this.dimension)); } for (int i = 0; i < this.dimension; ++i) { City city = this.cities.get(i); // add distance to target city for (int j = 0; j < this.dimension; ++j) { if (i != j) { int distance; // commonly used distance is integer, not double (see TSPLIB) // double distance; // if (edge.equals("CEIL_2D")) { // distance = Math.ceil(Math.sqrt(Math.pow(coordinates.get(i)[0] - coordinates.get(j)[0], 2) // + Math.pow(coordinates.get(i)[1] - coordinates.get(j)[1], 2))); // } else { // distance = Math.sqrt(Math.pow(coordinates.get(i)[0] - coordinates.get(j)[0], 2) // + Math.pow(coordinates.get(i)[1] - coordinates.get(j)[1], 2)); // } 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)); } city.addDistance(this.cities.get(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.initOperations(); } protected void initOperations () { // prepare switch operation container this.switchCityOperations = new ArrayList<SwitchCityOperation>(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.switchCityOperations.add(new SwitchCityOperation(i, j)); } } } /** * 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 += this.cities.get(configuration.valueAt(i - 1)).getDistance(this.cities.get(configuration.valueAt(i))); } distance += this.cities.get(configuration.valueAt(configuration.getDimension() - 1)) .getDistance(this.cities.get(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 TSPIterator(configuration, this); } public Fitness getDefaultFitness() { return new TSPFitness(this); } /* StartingConfigurationProblem interface */ public Configuration getStartingConfiguration() { return this.startingConfiguration; } /* GlobalSearchProblem interface */ public Integer getMaximum(int index) { return this.cities.size() - 1; } /* RandomConfigurationProblem interface */ public Configuration getRandomConfiguration() { List<Integer> integerList = getStartingConfiguration().asList(); Collections.shuffle(integerList); return new Configuration(integerList, "TSP random configuration"); } }