Currently working on a TSP problem, and the idea is to optimize it for the use of restaurants, thus making food delivery easier. One of the criteria is that each path the courier takes has to have cumulative weight less than 60 (minutes) so that the food would not get too cold by the time the courier delivers it to the last client, taking into account time needed to actually hand over the food, the limit is actually closer to 40.
Now, I have implemented a tsp solving algorithm using Nearest Neighbor approach. As such I can accurately get the best(or close) path a courier can take to visit all clients.
The restriction mentioned above, however is giving me quite a bit of trouble. The best I could come up with at this moment is to divide the result of tsp, into smaller paths of length less than 60.This would provide an answer that does fir the criteria, yet is definitely not the best solution, then I used permutations to change the order of the places to be visited in each respective smaller cycle, and presented as a result the ones with smallest total weight, thus increasing the accuracy.
I am wondering if someone has any idea how to improve the solution, without actually brute-forcing all possible paths and then seeing which one is optimal, although the result is technically better, it takes way longer, and not suitable for use in a real situation.
My Idea of improving it is to somehow generate a tsp of length less than 60, then remove these nodes from the initial matrix, and have the algorithm run over it again, using recursion, until the nodes matrix is empty.
Here is the code for those interested in the implementation, any ideas are appreciated!
import itertools
def tsp_nn(nodes):
"""
This function takes a 2D array of distances between nodes, finds the nearest
neighbor for each node to form a tour using the nearest neighbor heuristic,
and then splits the tour into segments of length no more than 60. It returns the path segments and the segment
distances.
"""
# Set initial variables
if len(nodes) == 1:
return 0
unvisited = set(range(len(nodes)))
solution = [0]
current_node = 0
total_distance = 0
# Find nearest neighbor for each node
while unvisited:
nearest_neighbor = min(unvisited, key=lambda node: nodes[current_node][node])
solution.append(nearest_neighbor)
unvisited.remove(nearest_neighbor)
total_distance += nodes[current_node][nearest_neighbor]
current_node = nearest_neighbor
# Split the solution into segments of maximum length 60
path_segments, segment_distances = split_into_segments(solution, nodes)
print(path_segments)
# Print the results
print(f"Total distance: {total_distance}")
for i in range(len(path_segments)):
print(f"Segment {i}: {path_segments[i]}, distance: {segment_distances[i]}")
# Return the path segments and segment distances
return path_segments, segment_distances
def calculate_distance(nodes, solution):
"""
Given a set of nodes and a solution, this function calculates the total
distance of the solution path.
"""
distance = 0
for i in range(len(solution) - 1):
distance += nodes[solution[i]][solution[i+1]]
if len(solution)==1:
distance=nodes[0][solution[0]]
return distance
def split_into_segments(solution, nodes):
"""
Given a solution path and the distances between nodes, this function splits
the solution into segments of maximum length 60 and returns the segments and segment distances.
"""
path_segments = []
segment_distances = []
unvisited = list(range(len(nodes)))
while solution:
# Find the longest path less than or equal to 60
max_distance = 0
max_path = None
for i in range(len(solution)):
for j in range(i+1, len(solution)):
distance = calculate_distance(nodes, solution[i:j+1])
if distance <= 40 and distance > max_distance:
max_distance = distance
max_path = (i, j)
if max_path is None:
break
# Remove nodes in the longest path from the solution
i, j = max_path
path_segments.append(solution[i:j+1])
for k in range(i,j+1):
try:
unvisited.remove(solution[k])
except:
pass
segment_distances.append(max_distance)
solution = solution[:i] + solution[j+1:]
if len(unvisited)>0:
path_segments.append(unvisited)
segment_distances.append(calculate_distance(nodes,unvisited))
return path_segments, segment_distances
def valid(candidate):
"""
Given a candidate solution as a list of segments, this function generates
all possible permutations of the nodes in each segment and returns the segment
with the lowest total distance.
"""
final = []
for k in candidate:
possible=[]
# print(k)
while 0 in k:
k.remove(0)
li=list(itertools.permutations(k))
for i in li:
# print(i)
weight=calculate_distance(nodes,i)
# print(weight)
possible.append((weight,i))
final.append(sorted(possible)[0])
# print(final)
for i in final:
print(f"Segment {i[1]}: distance: {i[0]}")
# Example usage
nodes = [
[0, 17, 8, 7, 24, 21, 17, 31, 2, 9, 8, 15, 18, 26, 24, 14],
[17, 0, 11, 22, 19, 22, 27, 30, 17, 14, 18, 17, 1, 16, 17, 11],
[8, 11, 0, 11, 21, 19, 21, 26, 6, 5, 7, 12, 13, 21, 19, 9],
[7, 22, 11, 0, 26, 22, 11, 26, 6, 14, 10, 13, 24, 31, 28, 20],
[24, 19, 21, 26, 0, 15, 28, 19, 25, 26, 30, 19, 21, 26, 25, 25],
[21, 22, 19, 22, 15, 0, 22, 26, 18, 19, 21, 12, 19, 32, 28, 18],
[17, 27, 21, 11, 28, 22, 0, 30, 16, 24, 17, 17, 28, 32, 30, 24],
[31, 30, 26, 26, 19, 26, 30, 0, 29, 31, 34, 18, 34, 39, 38, 31],
[2, 17, 6, 6, 25, 18, 16, 29, 0, 9, 7, 14, 17, 26, 24, 14],
[9, 14, 5, 14, 26, 19, 24, 31, 9, 0, 11, 14, 14, 18, 17, 10],
[8, 18, 7, 10, 30, 21, 17, 34, 7, 11, 0, 19, 20, 21, 19, 13],
[15, 17, 12, 13, 19, 12, 17, 18, 14, 14, 19, 0, 19, 28, 26, 16],
[18, 1, 13, 24, 21, 19, 28, 34, 17, 14, 20, 19, 0, 16, 17, 11],
[26, 16, 21, 31, 26, 32, 32, 39, 26, 18, 21, 28, 16, 0, 6, 10],
[24, 17, 19, 28, 25, 28, 30, 38, 24, 17, 19, 26, 17, 6, 0, 11],
[14, 11, 9, 20, 25, 18, 24, 31, 14, 10, 13, 16, 11, 10, 11, 0]]
path_segments, segment_distances = tsp_nn(nodes)
print("===============================================")
valid(path_segments)
This is the current output:
Total distance: 175
Segment 0: [11, 3, 10, 6], distance: 40
Segment 1: [0, 0, 8, 2, 9, 15, 13, 14], distance: 39
Segment 2: [1, 12, 5, 4], distance: 35
Segment 3: [7], distance: 31
===============================================
Segment (10, 3, 6, 11): distance: 38
Segment (8, 2, 9, 15, 13, 14): distance: 37
Segment (1, 12, 5, 4): distance: 35
Segment (7,): distance: 31