Refactor for better TSP algorithm.

This commit is contained in:
2020-01-14 20:01:23 -05:00
parent 65fc139e65
commit efb0f611cf
5 changed files with 258 additions and 371 deletions

View File

@@ -2,11 +2,44 @@ import math
from functools import lru_cache
from collections import namedtuple
from random import shuffle
from map import Map
import time
@lru_cache(maxsize=1000000)
def distance(p1, p2):
""" Returns the distance between two points. """
return math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
def float_is_equal(a, b):
if (a - b) < 0.001:
return True
return False
def parse_input_data(input_data):
lines = input_data.split('\n')
node_count = int(lines[0])
return [Point(i, *map(float, lines[i + 1].split()))
for i in range(0, node_count)]
def prepare_output_data(points):
# Basic plausibility checks
assert(len(set(points)) == len(points))
assert(len(points) > 4)
for i, p in enumerate(points):
assert(i == p.index)
r = Route(points)
obj = r.total_distance
output_data = '%.2f' % obj + ' ' + str(0) + '\n'
output_data += ' '.join(map(lambda p: str(p.id), points))
return output_data
class Point(object):
def __init__(self, index, x, y):
self.id = index
self.index = index
self.x = x
self.y = y
@@ -23,48 +56,93 @@ class Point(object):
def __str__(self):
# m = "P_{}({}, {})".format(self.index, self.x, self.y)
# m = "P_{}({}, {})".format(self.index, self.cluster_x, self.cluster_y)
m = "P({})".format(self.index)
id = self.id
index = self.index
m = f"P({id=}, {index=})"
return m
def __repr__(self):
return self.__str__()
def parse_input_data(input_data):
lines = input_data.split('\n')
node_count = int(lines[0])
return [Point(str(i), *map(float, lines[i + 1].split()))
for i in range(0, node_count)]
class Route(object):
def __init__(self, points):
self.points = points
self.len_points = len(points)
self.total_distance = self.get_total_distance(self.points)
def get_total_distance(self, points):
""" Calculate the total distance of the point sequence. """
# Use negative indexing to get the distance from last to first point
return sum([distance(points[i - 1], points[i])
for i in range(self.len_points)])
def float_is_equal(a, b):
if (a - b) < 0.001:
return True
return False
def swap(self, p1, p2):
"""
Swaps two edges. p1 is the first point of the first
edge and p2 is the first point of the second edge.
The first point of edge 1 (p1) points to the first point
of edge two (p2) after the swap, while the second point
of edge 1 (p12) points to the second point of edge two (p22).
This means we swap p12 and p2 and update their indices.
Before: p1 -> p12 and p2 -> p22
After: p1 -> p2 and p12 -> p22
def prepare_output_data(points):
# Basic plausibility checks
assert(len(set(points)) == len(points))
assert(len(points) > 4)
obj = total_distance(points)
output_data = '%.2f' % obj + ' ' + str(0) + '\n'
output_data += ' '.join(map(lambda p: str(p.index), points))
return output_data
Afterwards we have to reverse the order of the points between
p2 and p12 while those points themselves are no longer touched.
"""
p12 = self.points[(p1.index + 1) % self.len_points]
p22 = self.points[(p2.index + 1) % self.len_points]
@lru_cache(maxsize=1000000)
def distance(point_1, point_2):
""" Calculate the distance between two points. """
p1, p2 = point_1, point_2
return math.sqrt((p1.x - p2.x)**2 + (p1.y - p2.y)**2)
# Swap positions in route.
self.points[p12.index] = p2
self.points[p2.index] = p12
# Swap indices.
p2.index, p12.index = p12.index, p2.index
# TODO(felixm): Update self.total_distance.
# TODO(felixm): Reverse order between p2 and p12.
def total_distance(points):
""" Calculate the total distance of the point sequence. """
# Use negative indexing to get the distance from last to first point
return sum([distance(points[i - 1], points[i])
for i in range(len(points))])
return points
def reorder_points_greedy(self):
best_distance = float("inf")
best_solution = None
points = self.points
for i in range(1000):
shuffle(points)
current_point, points = points[0], points[1:]
solution = [current_point]
while points:
next_point = None
# Select the closest point as the following one.
for neighbor, _ in current_point.neighbors:
if neighbor in points:
next_point = neighbor
points.remove(next_point)
break
# If none of the neighbors could be selected use any point.
if next_point is None:
next_point = points.pop()
solution.append(next_point)
current_point = next_point
total_distance = self.get_total_distance(solution)
points = solution
if total_distance < best_distance:
best_distance = total_distance
best_solution = solution.copy()
self.points = best_solution
for i, p in enumerate(self.points):
p.index = i
return self.points
def longest_distance(points, ignore_set):
@@ -117,40 +195,6 @@ def swap_edges(i, j, points, current_distance=0):
return current_distance
def reorder_points_greedy(points):
best_length = float("inf")
best_solution = None
for i in range(1000):
shuffle(points)
current_point, points = points[0], points[1:]
solution = [current_point]
while points:
next_point = None
# Select the closest point as the following one.
for neighbor, _ in current_point.neighbors:
if neighbor in points:
next_point = neighbor
points.remove(next_point)
break
# If none of the neighbors could be selected use any point.
if next_point is None:
next_point = points.pop()
solution.append(next_point)
current_point = next_point
total_length = total_distance(solution)
points = solution
if total_length < best_length:
best_length = total_length
best_solution = solution.copy()
return best_solution
def k_opt(p1_index, points, steps):
ignore_set = set()
@@ -213,154 +257,17 @@ def local_search_k_opt(points):
return points
class Map(object):
# Create Map. Cluster points into regions. Calculate distances only to own
# and neighbor regions. We can actually cluster in O(n) when we know how
# high and wide the clusters are. Once we have that working we go from
# there
CLUSTER_SIZE = 3 # How many points we want per cluster.
def __init__(self, points):
self.points = points
self.num_points = len(points)
self.calc_corners()
self.calc_cluster_dim()
self.sort_points_into_clusters()
self.add_neighbors_to_points()
def calc_cluster_dim(self):
clusters = self.num_points // self.CLUSTER_SIZE
# Calculate number of clusters to have a square
self.clusters_x = math.ceil(math.sqrt(clusters))
self.clusters_y = self.clusters_x
self.clusters_total = self.clusters_x ** 2
self.cluster_x_dim = (self.x_max - self.x_min) / self.clusters_x
self.cluster_y_dim = (self.y_max - self.y_min) / self.clusters_y
def add_neighbors_to_points(self):
""" Add all points from the surrounding clusters to each point. """
for p in self.points:
clusters_x = [p.cluster_x]
clusters_y = [p.cluster_y]
if p.cluster_x - 1 >= 0:
clusters_x.append(p.cluster_x - 1)
if p.cluster_x + 1 < self.clusters_x:
clusters_x.append(p.cluster_x + 1)
if p.cluster_y - 1 >= 0:
clusters_y.append(p.cluster_y - 1)
if p.cluster_y + 1 < self.clusters_y:
clusters_y.append(p.cluster_y + 1)
clusters = [(x, y)
for x in clusters_x
for y in clusters_y]
neighbors = []
for x, y in clusters:
for p2 in self.clusters[x][y]:
if p is not p2:
neighbors.append(p2)
p.add_neighbors(neighbors)
def sort_points_into_clusters(self):
self.clusters = [[[]
for x in range(self.clusters_y)]
for y in range(self.clusters_y)]
for p in self.points:
cluster_x = int((p.x - self.x_min) // self.cluster_x_dim)
cluster_y = int((p.y - self.y_min) // self.cluster_y_dim)
# If the point is on the outer edge of the highest cluster
# the index will be outside the correct range. We put it
# into the closes cluster.
if cluster_x == self.clusters_x:
cluster_x -= 1
if cluster_y == self.clusters_y:
cluster_y -= 1
self.clusters[cluster_x][cluster_y].append(p)
p.cluster_x = cluster_x
p.cluster_y = cluster_y
def calc_corners(self):
x_min, x_max = float("inf"), float("-inf")
y_min, y_max = float("inf"), float("-inf")
for p in self.points:
if p.x < x_min:
x_min = p.x
if p.x > x_max:
x_max = p.x
if p.y < y_min:
y_min = p.y
if p.y > y_max:
y_max = p.y
self.x_min = x_min
self.x_max = x_max
self.y_min = y_min
self.y_max = y_max
def plot(self):
try:
import matplotlib.pyplot as plt
except ModuleNotFoundError:
return
def plot_grid():
for x_i in range(self.clusters_x + 1):
x_1 = self.x_min + x_i * self.cluster_x_dim
x_2 = x_1
y_1 = self.y_min
y_2 = self.y_max
plt.plot([x_1, x_2], [y_1, y_2], 'b:')
for y_i in range(self.clusters_y + 1):
x_1 = self.x_min
x_2 = self.x_max
y_1 = self.y_min + y_i * self.cluster_y_dim
y_2 = y_1
plt.plot([x_1, x_2], [y_1, y_2], 'b:')
def plot_arrows():
for i in range(self.num_points):
p1 = self.points[i - 1]
p2 = self.points[i]
plot_arrow(p1, p2)
def plot_arrow(p1, p2):
x = p1.x
y = p1.y
dx = p2.x - x
dy = p2.y - y
opt = {'head_width': 0.4, 'head_length': 0.4, 'width': 0.05,
'length_includes_head': True}
plt.arrow(x, y, dx, dy, **opt)
def plot_points():
for i, p in enumerate(self.points):
plt.plot(p.x, p.y, '')
plt.text(p.x, p.y, ' ' + str(p))
for nb, _ in p.neighbors:
# plt.plot([p.x, nb.x], [p.y, nb.y], 'r--')
pass
plot_points()
plot_grid()
plot_arrows()
plt.show()
def solve_it(input_data):
points = parse_input_data(input_data)
# Initialiaze map before algorithm because it clusters the points
# and adds the neighbors to each point.
m = Map(points)
m.points = reorder_points_greedy(points)
# FIXME(felixm): Don't do this here.
m.points = local_search_k_opt(m.points)
m.plot()
r = Route(parse_input_data(input_data))
m = Map()
m.cluster(r.points)
r.reorder_points_greedy()
# m.plot(r.points)
return prepare_output_data(r.points)
return prepare_output_data(m.points)
if __name__ == "__main__":
@@ -369,32 +276,3 @@ if __name__ == "__main__":
input_data = input_data_file.read()
print(solve_it(input_data))
def local_search_2_opt(points):
current_total = total_distance(points)
ignore_set = set()
while True:
pi, i = longest_distance(points, ignore_set)
ignore_set.add(pi)
if not pi:
break
best_new_total = current_total
best_points = None
swap = None
for j in range(len(points)):
if j in [i, i + 1, i + 2]:
continue
new_points = list(points)
swap_edges(i, j - 1, new_points)
new_total = total_distance(new_points)
if new_total < best_new_total:
swap = (points[i], points[j - 1])
best_new_total = new_total
best_points = new_points
if best_new_total < current_total:
current_total = best_new_total
points = best_points
ignore_set = set()
return points