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):
try:
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
if not DEBUG: 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)):
if j in [i, i + 1, i + 2]:
continue continue
new_len = length(points[i - 1], points[i])
if new_len > max_len:
max_len = new_len
p_i = i - 1
p1 = points[p_i]
p2 = points[p_i + 1]
#print("Found max_len for ", edge(p1, p2))
current_length = total_distance(points)
for p_j in range(len(points)):
if p_j in [p_i, p_i + 1, p_i + 2]:
continue
q1 = points[p_j - 1]
q2 = points[p_j]
new_points = list(points) new_points = list(points)
swap_edges(p_i, p_j - 1, new_points) swap_edges(i, j - 1, new_points)
new_length = total_distance(new_points) new_total = total_distance(new_points)
if new_length < current_length: if new_total < best_new_total:
#print("Swaping", edge(points[p_i], points[p_i + 1]), "and", edge(points[p_j - 1], points[p_j])) swap = (points[i], points[j - 1])
#print("Better new_points", new_length, "smaller", current_length) best_new_total = new_total
ignore_list.clear() best_points = new_points
return new_points
#print("Did not find an intersection that provides better results.") if best_new_total < current_total:
ignore_list.append(p1) current_total = best_new_total
points = best_points
ignore_set = set()
return points return points
@ -172,23 +176,29 @@ 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]
def k_opt(p1_index, points, steps):
ignore_set = set()
for _ in range(10):
p2_index = p1_index + 1
p1, p2 = points[p1_index], points[p2_index]
dist_p1p2 = distance(p1, p2) dist_p1p2 = distance(p1, p2)
ignore_list.append(p2) ignore_set.add(p2)
p4_index = None p4_index = None
for p3_index in range(len(points)): #for p3_index in range(len(points)):
for p3_index in get_indices(p2_index, points):
p3 = points[p3_index] p3 = points[p3_index]
p4 = points[p3_index - 1] p4 = points[p3_index - 1]
if p4 in ignore_list or p4 is p1: if p4 in ignore_set or p4 is p1:
continue continue
dist_p2p3 = distance(p2, p3) dist_p2p3 = distance(p2, p3)
if dist_p2p3 < dist_p1p2: if dist_p2p3 < dist_p1p2:
@ -196,69 +206,81 @@ def k_opt(p1_index, points, ignore_list, swaps):
dist_p1p2 = dist_p2p3 dist_p1p2 = dist_p2p3
if not p4_index: if not p4_index:
return [] return steps
print_swap(p1_index, p4_index, points) # Get previous total as current_total
plot_graph(points) current_total = steps[-1][0]
swap_edges(p1_index, p4_index, points) new_total = swap_edges(p1_index, p4_index, points, current_total)
swaps.append([p1_index, p4_index]) steps.append((new_total, (p1_index, p4_index)))
new_total = total_distance(points) return steps
print("Current distance", new_total)
r = k_opt(p1_index, points, ignore_list, list(swaps))
r.append((new_total, swaps))
return r
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:
r = k_opt(index, list(points), [], [])
print("k-opt", len(r))
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:
print("Improvment. Apply steps.")
for step in steps:
swap_edges(*step, points)
assert(total_distance(points) == new_total)
current_total = new_total
ignore_list = []
else:
print("No changes.")
plot_graph(points)
return points return points
steps = k_opt(index, list(points), [(current_total, None)])
new_total = min(steps, key=lambda t: t[0])[0]
if new_total < current_total:
# Skip first step as it is the original order.
for total, step in steps[1:]:
current_total = swap_edges(*step, points, current_total)
if total == new_total:
break
# assert(float_is_equal(total_distance(points), current_total))
ignore_set = set()
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))