I am writing a program that can implement algorithms to solve the TSP. My goals:
- The solver can record every algorithm step, so that the whole solving process can be later visualised by charts or animations
- It's easy to add new algorithm implementation
- End user has full control over the simulation parameters
I tried to do this before a year ago and even asked. Thought I will try this again with more knowledge.
Point - a basic structure used across the application to represent cities.
public class Point {
private final double x;
private final double y;
public double getX() {
return x;
}
public double getY() {
return y;
}
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public Point(Random r, int rangeX, int rangeY) {
this.x = r.nextInt(rangeX);
this.y = r.nextInt(rangeY);
}
public double calculateDistanceToPoint(Point p) {
return Math.sqrt((this.x - p.x) * (this.x - p.x) + (this.y - p.y) * (this.y - p.y));
}
}
General solver
public abstract class TSPSolver {
protected ArrayList<Point> initialSetOfPoints = new ArrayList<>();
public abstract void solve();
public TSPSolver(int noOfPoints, Random r, int rangeX, int rangeY) {
for (int i = 0; i < noOfPoints; i++) {
addPoint(new Point(r, rangeX, rangeY));
}
}
public TSPSolver(ArrayList<Point> points) {
initialSetOfPoints = points;
}
public void addPoint(Point p) {
initialSetOfPoints.add(p);
}
}
Solver that can record and store history
public abstract class RecordableTSPSolver extends TSPSolver {
protected SolutionHistory solutionHistory = new SolutionHistory();
public RecordableTSPSolver(int noOfPoints, Random r, int rangeX, int rangeY) {
super(noOfPoints, r, rangeX, rangeY);
}
public RecordableTSPSolver(ArrayList<Point> points) {
super(points);
}
protected void recordStep(ArrayList<Point> currentSolution){
solutionHistory.addStep(currentSolution);
}
public SolutionHistory getSolutionHistory() {
return solutionHistory;
}
}
Actual implementation of simulated annealing algorithm:
public class AnnealingSolver extends RecordableTSPSolver {
private ArrayList<Point> currentSolution;
private ArrayList<Point> newSolution;
private ArrayList<Point> finalSolution;
private double initialTemperature;
private double minimalTemperature;
private int maximumNumberOfTrials;
private double coolingCoefficient;
private int iterationsWithoutImprovement;
public AnnealingSolver(int noOfPoints, Random r, int rangeX, int rangeY, double initialTemperature, double minimalTemperature, int maximumNumberOfTrials, double coolingCoefficient) {
super(noOfPoints, r, rangeX, rangeY);
initializeFields(initialTemperature, minimalTemperature, maximumNumberOfTrials, coolingCoefficient);
}
public AnnealingSolver(ArrayList<Point> points, double initialTemperature, double minimalTemperature, int maximumNumberOfTrials, double coolingCoefficient){
super(points);
initializeFields(initialTemperature, minimalTemperature, maximumNumberOfTrials, coolingCoefficient);
}
private void initializeFields(double initialTemperature, double minimalTemperature, int maximumNumberOfTrials, double coolingCoefficient) {
this.initialTemperature = initialTemperature;
this.minimalTemperature = minimalTemperature;
this.maximumNumberOfTrials = maximumNumberOfTrials;
this.coolingCoefficient = coolingCoefficient;
}
@Override
public void solve() {
currentSolution = initialSetOfPoints;
while (solutionCanBeImproved()) {
algorithmStep();
}
finalSolution = currentSolution;
}
public void algorithmStep() {
newSolution = TSPUtils.swapTwoRandomEdges(currentSolution);
if (isABetterCandidate()) {
recordStep(newSolution);
iterationsWithoutImprovement = 0;
currentSolution = newSolution;
} else {
iterationsWithoutImprovement++;
}
lowerTemperature();
}
private boolean solutionCanBeImproved() {
return initialTemperature > minimalTemperature && iterationsWithoutImprovement < maximumNumberOfTrials;
}
private void lowerTemperature() {
initialTemperature = coolingCoefficient * initialTemperature;
}
private boolean isABetterCandidate() {
double travelCostDifference = getTravelCostDifference();
return travelCostDifference < 0 || (travelCostDifference > 0 && Math.exp(-travelCostDifference / initialTemperature) > Math.random());
}
private double getTravelCostDifference() {
return TSPUtils.getTotalTourCost(newSolution) - TSPUtils.getTotalTourCost(currentSolution);
}
public ArrayList<Point> getFinalSolution() {
return finalSolution;
}
}
Utils class
public class TSPUtils {
private TSPUtils(){}
public static double getTotalTourCost(ArrayList<Point> points) {
double cost = 0;
if (points.size() > 2) {
for (int i = 0; i < points.size() - 1; i++) {
cost += points.get(i).calculateDistanceToPoint(points.get(i + 1));
}
cost += points.get(points.size()-1).calculateDistanceToPoint(points.get(0));
}
return cost;
}
public static ArrayList<Point> swapTwoRandomEdges(ArrayList<Point> points) {
int range1 = 0, range2 = 0, min, max;
int noOfPoints = points.size();
ArrayList<Point> unchangedBeginning;
ArrayList<Point> toBeReversed;
ArrayList<Point> unchangedEnd;
Random random = new Random();
while (Math.abs(range1 - range2) < 2) {
range1 = random.nextInt(noOfPoints);
range2 = random.nextInt(noOfPoints);
}
min = Math.min(range1, range2);
max = Math.max(range1, range2);
unchangedBeginning = new ArrayList<>(points.subList(0, min));
toBeReversed = new ArrayList<>(points.subList(min, max));
Collections.reverse(toBeReversed);
unchangedEnd = new ArrayList<>(points.subList(max, noOfPoints));
ArrayList<Point> swapped = new ArrayList<>();
swapped.addAll(unchangedBeginning);
swapped.addAll(toBeReversed);
swapped.addAll(unchangedEnd);
return swapped;
}
}
My thoughts:
- Architecture - the shown
AnnealingSolver
solver implementation has 8 parameters when executing on random cities, 5 when supplying own cities. That's a lot of parameters. I was tempted to introduce a builder pattern for this at some point, but I don't know if it's a good idea.Also, the constructors are repeating a lot of code - maybe I should extract the common code into a private method? - Architecture - the
RecordableTSPSolver
class seems... weak. I mean, there is nothing forcing its child class to record history. At one point I had an interfaceRecordable
which had one methodrecordStep
, but of course it had to be public, and it just forced behaviour, which isn't good as far as I know. So I implemented what you can see above. Or maybe leave therecordStep
method as protected and abstract to force implementation in child class and prevent end user from adding history? - Architecture - I also feel like the implementation of recording steps is weird, because if I wanted to have a class that just extends
TSPSolver
, it will be the same algorithm as inRecordableTSPSolver
, but just without the one line of code that records the step. I'm sure there is some neat solution for this and I will try to find it. - Performance - storing history is expensive, so I'm thinking of rebuilding the history mechanism so that it stores initial points and a list of changes introduced by an iteration, instead of the full list of points every time.
This is a long post, so thank you for reading and attention. Your input will be most appreciated.
Point2D.Double
useful. \$\endgroup\$