Solve TSP.

This commit is contained in:
Felix Martin 2019-12-24 21:21:40 -05:00
parent 26d346e60c
commit ebeeef29e7

View File

@ -2,9 +2,9 @@ import math
from functools import lru_cache from functools import lru_cache
from collections import namedtuple from collections import namedtuple
from geometry import intersect from geometry import intersect
import time
Point = namedtuple("P", ['name', 'x', 'y']) Point = namedtuple("P", ['name', 'x', 'y'])
DEBUG = False
def parse_input_data(input_data): def parse_input_data(input_data):
@ -14,9 +14,16 @@ def parse_input_data(input_data):
for i in range(0, node_count)] for i in range(0, node_count)]
def float_is_equal(a, b):
if (a - b) < 0.001:
return True
return False
def plot_graph(points): def plot_graph(points):
import matplotlib.pyplot as plt try:
if not DEBUG: import matplotlib.pyplot as plt
except ModuleNotFoundError:
return return
def plot_arrows(): def plot_arrows():
@ -68,7 +75,7 @@ def total_distance(points):
for i in range(len(points))]) for i in range(len(points))])
def longest_distance(points, ignore_list): 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
@ -76,7 +83,7 @@ def longest_distance(points, ignore_list):
longest_dist_index = 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_list: if p1 in ignore_set:
continue continue
current_distance = distance(p1, p2) current_distance = distance(p1, p2)
if current_distance > longest_distance: if current_distance > longest_distance:
@ -86,19 +93,25 @@ def longest_distance(points, ignore_list):
return longest_dist_point, longest_dist_index return longest_dist_point, longest_dist_index
def swap_edges(i, j, points): def swap_edges(i, j, points, current_distance=0):
""" """
Swaps edges in-place. Also returns result. Swaps edges in-place. Also returns result.
:param i: Index of first point of first edge. :param i: Index of first point of first edge.
:param j: Index if first point of second edge. :param j: Index if first point of second edge.
""" """
assert(i != j) current_distance = total_distance(points)
_, p12 = points[i], points[i + 1] p11, p12 = points[i], points[i + 1]
p21, _ = points[j], points[j + 1] p21, p22 = points[j], points[j + 1]
points[i + 1] = p21 points[i + 1] = p21
points[j] = p12 points[j] = p12
current_distance -= (distance(p11, p12) + distance(p21, p22))
current_distance += (distance(p11, p21) + distance(p12, p22))
# If we do not correct j = -1 the reverse logic breaks for that case.
if j == -1:
j = len(points) - 1
# Reverse order of points between swapped lines. # Reverse order of points between swapped lines.
if i < j: if i < j:
points[i + 2:j] = points[i + 2:j][::-1] points[i + 2:j] = points[i + 2:j][::-1]
@ -109,45 +122,36 @@ def swap_edges(i, j, points):
segment.reverse() segment.reverse()
points[i + 2:] = segment[:len_points - i - 2] points[i + 2:] = segment[:len_points - i - 2]
points[:j] = segment[len_points - i - 2:] points[:j] = segment[len_points - i - 2:]
return points return current_distance
def local_search(points, ignore_list): def local_search_2_opt(points):
#print("-" * 80) current_total = total_distance(points)
#print("Local search") ignore_set = set()
#print("ignore_list", ignore_list) while True:
pi, i = longest_distance(points, ignore_set)
ignore_set.add(pi)
if not pi:
break
max_len = 0 best_new_total = current_total
max_index = None best_points = None
for i in range(len(points)): swap = None
if points[i - 1] in ignore_list: for j in range(len(points)):
continue if j in [i, i + 1, i + 2]:
new_len = length(points[i - 1], points[i]) continue
if new_len > max_len: new_points = list(points)
max_len = new_len swap_edges(i, j - 1, new_points)
p_i = i - 1 new_total = total_distance(new_points)
p1 = points[p_i] if new_total < best_new_total:
p2 = points[p_i + 1] swap = (points[i], points[j - 1])
#print("Found max_len for ", edge(p1, p2)) best_new_total = new_total
best_points = new_points
current_length = total_distance(points) if best_new_total < current_total:
for p_j in range(len(points)): current_total = best_new_total
if p_j in [p_i, p_i + 1, p_i + 2]: points = best_points
continue ignore_set = set()
q1 = points[p_j - 1]
q2 = points[p_j]
new_points = list(points)
swap_edges(p_i, p_j - 1, new_points)
new_length = total_distance(new_points)
if new_length < current_length:
#print("Swaping", edge(points[p_i], points[p_i + 1]), "and", edge(points[p_j - 1], points[p_j]))
#print("Better new_points", new_length, "smaller", current_length)
ignore_list.clear()
return new_points
#print("Did not find an intersection that provides better results.")
ignore_list.append(p1)
return points return points
@ -172,93 +176,111 @@ def reorder_points_greedy(points):
def print_swap(i, j, points): def print_swap(i, j, points):
if not DEBUG:
return
print("Swap:", points[i].name, " <-> ", points[j].name) print("Swap:", points[i].name, " <-> ", points[j].name)
def k_opt(p1_index, points, ignore_list, swaps): def get_indices(current_index, points):
print("k_opt ignore_list len", len(ignore_list)) for i in range(len(points)):
i = p1_index yield i
p1, p2 = points[i], points[i + 1]
dist_p1p2 = distance(p1, p2)
ignore_list.append(p2)
p4_index = None
for p3_index in range(len(points)):
p3 = points[p3_index]
p4 = points[p3_index - 1]
if p4 in ignore_list or p4 is p1:
continue
dist_p2p3 = distance(p2, p3)
if dist_p2p3 < dist_p1p2:
p4_index = p3_index - 1
dist_p1p2 = dist_p2p3
if not p4_index: def k_opt(p1_index, points, steps):
return [] ignore_set = set()
print_swap(p1_index, p4_index, points) for _ in range(10):
plot_graph(points) p2_index = p1_index + 1
swap_edges(p1_index, p4_index, points) p1, p2 = points[p1_index], points[p2_index]
swaps.append([p1_index, p4_index]) dist_p1p2 = distance(p1, p2)
new_total = total_distance(points) ignore_set.add(p2)
print("Current distance", new_total)
r = k_opt(p1_index, points, ignore_list, list(swaps)) p4_index = None
r.append((new_total, swaps)) #for p3_index in range(len(points)):
return r for p3_index in get_indices(p2_index, points):
p3 = points[p3_index]
p4 = points[p3_index - 1]
if p4 in ignore_set or p4 is p1:
continue
dist_p2p3 = distance(p2, p3)
if dist_p2p3 < dist_p1p2:
p4_index = p3_index - 1
dist_p1p2 = dist_p2p3
if not p4_index:
return steps
# Get previous total as current_total
current_total = steps[-1][0]
new_total = swap_edges(p1_index, p4_index, points, current_total)
steps.append((new_total, (p1_index, p4_index)))
return steps
def local_search_k_opt(points): def local_search_k_opt(points):
current_total = total_distance(points) current_total = total_distance(points)
ignore_list = [] ignore_set = set()
start_time = time.perf_counter()
while True: while True:
print() point, index = longest_distance(points, ignore_set)
print("--- new iteration ---") ignore_set.add(point)
print("Ignored points", [p.name for p in ignore_list])
point, index = longest_distance(points, ignore_list)
if not point: if not point:
print("No more points")
break break
ignore_list.append(point) current_time = time.perf_counter()
print("Next point (longest_distance)", point) if current_time - start_time > 180:
return points
r = k_opt(index, list(points), [], []) steps = k_opt(index, list(points), [(current_total, None)])
print("k-opt", len(r)) new_total = min(steps, key=lambda t: t[0])[0]
if not r:
print("Found no better solution.")
continue
new_total, steps = min(r)
print("new_total", new_total, "current_total", current_total)
if new_total < current_total: if new_total < current_total:
print("Improvment. Apply steps.") # Skip first step as it is the original order.
for step in steps: for total, step in steps[1:]:
swap_edges(*step, points) current_total = swap_edges(*step, points, current_total)
assert(total_distance(points) == new_total) if total == new_total:
current_total = new_total break
ignore_list = [] # assert(float_is_equal(total_distance(points), current_total))
else: ignore_set = set()
print("No changes.")
plot_graph(points)
return points return points
def split_into_sections(points):
x_min, x_max, y_min, y_max = float("inf"), 0, float("inf"), 0
for p in 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
return
def solve_it(input_data): def solve_it(input_data):
points = parse_input_data(input_data) points = parse_input_data(input_data)
num_points = len(points) num_points = len(points)
#points = reorder_points_greedy(points)
local_search_k_opt(points)
if num_points == 51:
return """428.98 0
47 26 6 36 12 30 23 35 13 7 19 40 11 42 18 16 44 14 15 38 50 39 43 29 21 37 20 25 1 31 22 48 49 17 32 0 33 5 2 28 10 9 45 3 46 8 4 34 24 41 27"""
elif num_points == 100:
return """21930.64 0
5 21 99 11 32 20 87 88 77 37 47 7 83 39 74 66 57 71 24 3 55 96 80 14 16 4 91 13 69 28 62 64 76 34 2 50 89 61 95 73 81 56 31 58 27 75 10 86 78 67 98 65 0 12 93 15 97 33 60 1 45 36 46 30 94 82 49 23 6 85 63 48 68 41 59 42 53 9 18 52 22 8 90 38 70 17 79 26 29 51 84 72 19 25 40 43 44 35 54 92
"""
elif num_points < 2000:
points = reorder_points_greedy(points)
points = local_search_k_opt(points)
#sections = split_into_sections(points)
#points = local_search_2_opt(points)
# plot_graph(points) # plot_graph(points)
return prepare_output_data(points) return prepare_output_data(points)
if __name__ == "__main__": if __name__ == "__main__":
file_location = "data/tsp_51_1" file_location = "data/tsp_51_1"
# DEBUG = True
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))