0

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
huntrese
  • 25
  • 5

1 Answers1

1

remove these nodes from the initial matrix

Tricky. You are liable to disconnect the graph, leaving some destinations unreachable. Better just to remove the destinations from the request list

- Construct list of requested destinations
- WHILE destinations remain in list
    - Run TSP
    - Remove destinations from the END of the route, until route is shorter than the maximum
    - Remove destinations that remain from request list
    - Add remaining route to solution list
- Output solution list of routes
ravenspoint
  • 19,093
  • 6
  • 57
  • 103
  • Thanks for the idea, I have managed to develop a working algorithm based on the structure you have provided, It can definitely be further optimised, but it is definitely better than what I had before haha! – huntrese Feb 27 '23 at 08:36