The intended purpose for posting here is to have more experienced software developers review my code, test it, and improve it. Should it to hold up, my ultimate goal is to construct a proof that reinforces it to prove that it works across all cases.
The Theory
After some research, my theory most closely resembles the Nearest Neighbor Algorithm by Rosenkratz, Stearns and Lewis, detailed on page 242 of this paper. The concept I use is entirely original, but the performance is very similar to their take.
My first step was to think about the problem differently. The classic TSP (Traveling Salesman Problem) is stated along these lines:
Find the shortest possible route that visits every city exactly once and returns to the starting point.
The problem is defined as the shortest route that starts and ends at the same point, which is essentially the shortest circuit for the whole graph, making the start aribtrary. In other words, no matter where you start on the graph, there will only be one "shortest path." The question then becomes:
Find the shortest possible circuit that visits every city exactly once.
The easiest way to make this understandable for a computer is to make the whole graph into one line.
Thinking abstractly, we take all of the points in the graph and arrange them into one straight line along the y-axis, creating a path with the shortest distance between each point.
This would give us the shortest Hamiltonian, but we can't say that the shortest Hamiltonian always results in the shortest possible circuit. The length of the overall path must be considered when choosing each subpath.
Now the only time we run into problems when using my method is if any point shares the same y-value with another point.
I call these points collisions. To find the best place to put them into the graph, all we have to do is test the distance at each point and choose which index gives us the shortest distance.
This is the part of the algorithm that performs the worst, and has the most room for improvement.
The pseudocode for the whole algorithm is as follows:
solution = new list
collisions = new list
// init solution
// sort solution
// be sure we have the shortest path, and remove any collisions for future calculation
if we have collisions
// find the best places to put them
Results
Currently my implementation works for the following cases:
- a single point (length 0)
- simple to complex polygons
- lines (simple to complex functions implied)
- scatter plots (implied from polygons)
Every possible path would have to be either one or a mixture of these.
Reference.java
package T145.salesman;
import java.util.Random;
public class Reference {
private Reference() {}
public static final double[][] TRICKY_TRAPEZOID = { { 2, 4 }, { 4, 4 }, { 6, 4 }, { 3, 1 }, { 5, 1 } };
public static final double[][] SIMPLE_GRAPH = { { 1, 1 }, { 2, 3 }, { 3, 5 }, { 4, 3 }, { 5, 5 }, { 6, 1 }, { 7, 6 } };
public static final double[][] LINE = { { 1, 1 }, { 2, 2 }, { 3, 3 }, { 4, 4 }, { 5, 5 }, { 6, 6 }, { 7, 7 } };
public static final double[][] FLAT_LINE = { { 1, 1 }, { 2, 1 }, { 3, 1 }, { 4, 1 }, { 5, 1 }, { 6, 1 }, { 7, 1 }, { 8, 1 } };
public static final double[][] SQUARE = { { 1, 1 }, { 5, 5 }, { 1, 5 }, { 5, 1 } };
public static final double[][] SQUARE_WITH_CENTER = { { 1, 1 }, { 5, 5 }, { 1, 5 }, { 5, 1 }, { 3, 3 } };
public static final double[][] RHOMBUS = { { 2, 2 }, { 3, 5 }, { 4, 3 }, { 5, 6 } };
// user-created problems
public static final double[][] OSCAR_DILEMA = { { 0, 0 }, { 1, 1000 }, { 2, 0 }, { 3, 1000 } };
public static final double[][] GREYBEARD = { { 0, 0 }, { 1, 99 }, { 2, 98 }, { 3, 3 }, { 4, 4 }, { 5, 95 } };
public static final double[][] getRandomIntegerGraph(int maxSize) {
double[][] graph = new double[maxSize][maxSize];
Random rand = new Random();
for (int t = 0; t < maxSize; ++t) {
for (int s = 0; s < maxSize; ++s) {
graph[t][s] = rand.nextInt(maxSize);
}
}
return graph;
}
public static final double[][] getRandomDoubleGraph(int maxSize) {
double[][] graph = new double[maxSize][maxSize];
Random rand = new Random();
for (int t = 0; t < maxSize; ++t) {
for (int s = 0; s < maxSize; ++s) {
graph[t][s] = rand.nextDouble();
}
}
return graph;
}
}
Main.java
package T145.salesman;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
public class Main {
private static class Point implements Comparable<Point> {
private final double x;
private final double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public double getX() {
return x;
}
public double getY() {
return y;
}
public double getDistance(Point dest) {
double distX = dest.getX() - x;
double distY = dest.getY() - y;
distX *= distX;
distY *= distY;
return Math.sqrt(distX + distY);
}
@Override
public String toString() {
return "{ X: " + x + "; Y: " + y + " }";
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Point) {
Point point = (Point) obj;
return point.x == x && point.y == y;
}
return false;
}
@Override
public int compareTo(Point other) {
int result = Double.compare(x, other.getX());
if (result == 0) {
result = Double.compare(y, other.getY());
}
return result;
}
}
private static Point getNextPoint(List<Point> points, int start) {
return points.get(start == points.size() - 1 ? 0 : start + 1);
}
private static double getTotalDistance(List<Point> points) {
double shortestDist = 0;
for (int t = 0; t < points.size(); ++t) {
shortestDist += points.get(t).getDistance(getNextPoint(points, t));
}
return shortestDist;
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
double[][] graph = Reference.GREYBEARD;
if (graph.length <= 1) {
System.out.println("SOLUTION: 0");
return;
}
LinkedList<Point> points = new LinkedList<>();
ArrayDeque<Point> collisions = new ArrayDeque<>(graph.length);
// O(n)
System.out.println("Input Graph: ");
for (int t = 0; t < graph.length; ++t) {
Point point = new Point(graph[t][0], graph[t][1]);
points.add(point);
System.out.println(point);
}
// O(nlog(n))
Collections.sort(points);
List<Point> virtualSolution;
// O(n^2)
for (int t = 0; t < points.size(); ++t) {
Point point = points.get(t);
for (int s = t + 1; s < points.size(); ++s) {
Point other = points.get(s);
if (point.getY() == other.getY()) {
collisions.add(other);
points.remove(s);
}
}
virtualSolution = new ArrayList<>(points);
Collections.swap(virtualSolution, t, t == 0 ? points.size() - 1 : t - 1);
double solutionDist = 0;
double virtualDist = 0;
for (int s = 0; s < points.size(); ++s) {
solutionDist += points.get(s).getDistance(getNextPoint(points, s));
virtualDist += virtualSolution.get(s).getDistance(getNextPoint(virtualSolution, s));
}
if (virtualDist < solutionDist) {
points = new LinkedList<>(virtualSolution);
}
}
if (!collisions.isEmpty()) {
Map<Double, Integer> distances = new HashMap<>(points.size(), 1F);
// O(n^3)
while (!collisions.isEmpty()) {
Point c = collisions.remove();
for (int t = 0; t < points.size(); ++t) {
virtualSolution = new ArrayList<>(points);
virtualSolution.add(t, c);
distances.put(getTotalDistance(virtualSolution), t);
}
points.add(distances.get(Collections.min(distances.keySet())), c);
distances.clear();
}
}
System.out.println('\n' + " --- RESULT ---");
for (Point point : points) {
System.out.println(point);
}
System.out.println('\n' + " --- VERIFICATION ---");
System.out.println("Graph Length:\t" + graph.length);
System.out.println("Solution Size:\t" + points.size());
System.out.println("VERIFIED: " + (points.size() == graph.length));
System.out.println('\n' + " --- FINAL PHASE ---");
System.out.println("SOLUTION: " + getTotalDistance(points));
System.out.println("Runtime: " + (System.currentTimeMillis() - start) + " ms");
}
}
[(0,0), (1,1000), (2,0)]
, it will give that as the path which is nearly twice as long as optimal. If you are only trying to produce a solution with no bounds on how big it is, the traveling salesman problem is trivial. \$\endgroup\$