This commit is contained in:
2020-01-15 07:13:00 -05:00
parent efb0f611cf
commit 274f8e10db
2 changed files with 169 additions and 116 deletions

7
tsp/data/tsp_6_1 Normal file
View File

@@ -0,0 +1,7 @@
6
0 2
5 0
8 3
8 4
6 5
2 5

View File

@@ -1,9 +1,9 @@
import math import math
import time
from functools import lru_cache from functools import lru_cache
from collections import namedtuple
from random import shuffle from random import shuffle
from map import Map from map import Map
import time
@lru_cache(maxsize=1000000) @lru_cache(maxsize=1000000)
def distance(p1, p2): def distance(p1, p2):
@@ -53,6 +53,12 @@ class Point(object):
neighbors = [(n, distance(self, n)) for n in neighbors] neighbors = [(n, distance(self, n)) for n in neighbors]
self.neighbors = sorted(neighbors, key=lambda t: t[1]) self.neighbors = sorted(neighbors, key=lambda t: t[1])
def copy(self):
p = Point(self.id, self.x, self.y)
p.index = self.index
p.neighbors = self.neighbors
return p
def __str__(self): def __str__(self):
# m = "P_{}({}, {})".format(self.index, self.x, self.y) # 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, self.cluster_x, self.cluster_y)
@@ -65,92 +71,11 @@ class Point(object):
return self.__str__() return self.__str__()
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 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
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]
# 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.
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): def longest_distance(points, ignore_set):
""" Returns the point and index of the """ Returns the point and index of the
point with the longest distance to the next point. """ point with the longest distance to the next point. """
longest_distance = 0 longest_distance = 0
longest_dist_point = None longest_dist_point = None
longest_dist_index = None
for i in range(len(points)): for i in range(len(points)):
p1, p2 = points[i - 1], points[i] p1, p2 = points[i - 1], points[i]
if p1 in ignore_set: if p1 in ignore_set:
@@ -159,8 +84,7 @@ def longest_distance(points, ignore_set):
if current_distance > longest_distance: if current_distance > longest_distance:
longest_distance = current_distance longest_distance = current_distance
longest_dist_point = p1 longest_dist_point = p1
longest_dist_index = i - 1 return longest_dist_point
return longest_dist_point, longest_dist_index
def swap_edges(i, j, points, current_distance=0): def swap_edges(i, j, points, current_distance=0):
@@ -195,66 +119,187 @@ def swap_edges(i, j, points, current_distance=0):
return current_distance return current_distance
def k_opt(p1_index, points, steps): def k_opt(p1, route, steps):
ignore_set = set() ignore_set = set()
for _ in range(10): for _ in range(10):
p2_index = p1_index + 1 p2 = route.points[(p1.index + 1) % route.len_points]
p1, p2 = points[p1_index], points[p2_index]
dist_p1p2 = distance(p1, p2) dist_p1p2 = distance(p1, p2)
ignore_set.add(p2) ignore_set.add(p2)
p4_index = None p4 = None
# TODO(felixm): Keep track of current indices and then make this more efficient. # TODO(felixm): Keep track of current indices and then make this more efficient.
for p3_index in range(len(points)): for p3 in route.points:
p3 = points[p3_index] if p3 is p2 or p3 is p1 or p3 in ignore_set:
p4 = points[p3_index - 1] continue
if p4 in ignore_set or p4 is p1: p4_ = route.points[(p3.index - 1) % route.len_points]
if p4_ in ignore_set or p4_ is p1:
continue continue
dist_p2p3 = distance(p2, p3)
if dist_p2p3 < dist_p1p2:
p4_index = p3_index - 1
dist_p1p2 = dist_p2p3
if not p4_index: dist_p2p3 = distance(p2, p3)
if dist_p2p3 < dist_p1p2:
dist_p1p2 = dist_p2p3
p4 = p4_
if p4 is None:
return steps return steps
# Get previous total as current_total step = (p1.index, p4.index)
current_total = steps[-1][0] new_total = route.swap(p1, p4)
new_total = swap_edges(p1_index, p4_index, points, current_total) steps.append((new_total, step))
steps.append((new_total, (p1_index, p4_index)))
return steps return steps
def local_search_k_opt(points): def local_search_k_opt(route):
current_total = total_distance(points) current_total = route.total_distance
ignore_set = set() ignore_set = set()
start_time = time.perf_counter() start_time = time.perf_counter()
while True: while True:
point, index = longest_distance(points, ignore_set) # TODO(felixm): Get longest distance from heap in route.
point = longest_distance(route.points, ignore_set)
ignore_set.add(point) ignore_set.add(point)
if not point: if not point:
break break
current_time = time.perf_counter() if time.perf_counter() - start_time > 10:
if current_time - start_time > 180: return
return points
steps = k_opt(index, list(points), [(current_total, None)]) copy_route = route.copy()
steps = k_opt(point, copy_route, [(current_total, None)])
new_total = min(steps, key=lambda t: t[0])[0] new_total = min(steps, key=lambda t: t[0])[0]
if new_total < current_total: if new_total < current_total:
# Skip first step as it is the original order. # Skip first step as it is the original order.
for total, step in steps[1:]: for total, step in steps[1:]:
current_total = swap_edges(*step, points, current_total) p1, p4 = step
current_total = route.swap(p1, p4)
if total == new_total: if total == new_total:
break break
# assert(float_is_equal(total_distance(points), current_total)) assert(float_is_equal(route.total_distance, current_total))
ignore_set = set() ignore_set = set()
return points
class Route(object):
def __init__(self, points):
self.points = points
self.len_points = len(points)
self.total_distance = self.get_total_distance(points)
self.point_id_to_point = {p.id: p for p in self.points}
def copy(self):
route = Route([])
route.points = [p.copy() for p in self.points]
route.len_points = self.len_points
route.total_distance = self.total_distance
route.point_id_to_point = {p.id: p for p in route.points}
return route
def get_point(self, point):
return self.point_id_to_point[point.id]
def verify_total_distance(self):
a = self.total_distance
b = self.get_total_distance(self.points)
assert(float_is_equal(a, b))
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 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
Afterwards we have to reverse the order of the points [p', p'', p''']
between p2 and p12 while those points themselves are no longer touched.
Before swap: [p1, p12, p', p'', p''', p2, p21]
After swap: [p1, p2, p', p'', p''', p12, p21]
"""
if type(p1) is int:
p1 = self.points[p1]
if type(p2) is int:
p2 = self.points[p2]
# Handle case when edge goes over the end of the list.
p12 = self.points[(p1.index + 1) % self.len_points]
p21 = self.points[(p2.index + 1) % self.len_points]
# Update self.total_distance.
self.total_distance -= (distance(p1, p12) + distance(p2, p21))
self.total_distance += (distance(p1, p2) + distance(p12, p21))
# Swap positions and indices.
self.points[p12.index] = p2
self.points[p2.index] = p12
p2.index, p12.index = p12.index, p2.index
# Handle case when p2 was before p1 initially.
if p12.index > p2.index:
len_revers = p12.index - p2.index
else:
len_revers = (p12.index + self.len_points) - p2.index
# Reverse order between p2 and p12.
for i in range(1, len_revers // 2 + 1):
pa = self.points[(p2.index + i) % self.len_points]
pb = self.points[(p12.index - i) % self.len_points]
self.points[pa.index] = pb
self.points[pb.index] = pa
pa.index, pb.index = pb.index, pa.index
return self.total_distance
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
self.total_distance = best_distance
for i, p in enumerate(self.points):
p.index = i
return self.points
def solve_it(input_data): def solve_it(input_data):
@@ -263,15 +308,16 @@ def solve_it(input_data):
m.cluster(r.points) m.cluster(r.points)
r.reorder_points_greedy() r.reorder_points_greedy()
# m.plot(r.points) local_search_k_opt(r)
m.plot(r.points)
r.verify_total_distance()
return prepare_output_data(r.points) return prepare_output_data(r.points)
if __name__ == "__main__": if __name__ == "__main__":
file_location = "tsp/data/tsp_51_1" file_location = "tsp/data/tsp_51_1"
# file_location = "tsp/data/tsp_6_1"
with open(file_location, 'r') as input_data_file: with open(file_location, 'r') as input_data_file:
input_data = input_data_file.read() input_data = input_data_file.read()
print(solve_it(input_data)) print(solve_it(input_data))