# Search Based Software Engineering¶

## Exercise 01 - Traveling Salesman Problem Introduction¶

### by André Karge - andre.karge[at]uni-weimar.de¶

The Traveling Salesman Problem (TSP) is given by the following question: “Given is a list of cities and distances between each pair of cities - what is the shortest route that visits each city and returns to the original city?”

The TSP is an NP-Hard-Problem which does not mean an instance of the problem will be hard to solve. It means, there does not exist an algorithm that produces the best solution in polynomial time. We can not make predictions about how long it might take to find the best solution.

But, we can find a good solution which might not be the best solution. It is ok to find a route amongst 1000 cities that is only few miles longer than the best route. Particularly, if it would take an inordinate amount amount of computing time to get from our good solution to the best solution. ## Representation of the Problem¶ A TSP can be modelled as an undirected weighted graph:

    - cities = vertices
- paths between cities = edges
- distance of a path = weight of an edge


This graph can be represented as an Adjacency matrix:

\ A B C D
A 0 20 42 35
B 20 0 30 34
C 42 30 0 12
D 35 34 12 0

But how do we get the distances between cities if we only got the koordinates for each city?

Each city is represented by a cartesian koordinate P

$P = (p_x, p_y)$

### Euclidean Distance¶

Euclidean distance between two points P1 = (x1, y1) and P2 = (x2, y2) is:

$d(P_{1},P_{2}) = \sqrt{(x_{1} - x_{2})^2 + (y_{1} - y_{2})^2}$

In :
from itertools import permutations

In :
def distance(p1, p2):
"""
Returns the Euclidean distance of two points in the Cartesian Plane.

>>> distance([3,4],[0,0])
5.0

"""
return ((p1 - p2)**2 + (p1 - p2)**2) ** 0.5

In :
print(distance([3,6],[7,6]))

4.0

In :
def total_distance(points):
"""
Returns the length of the path passing throught
all the points in the given order.

>>> total_distance([[1,2],[4,6]])
5.0
>>> total_distance([[3,6],[7,6],[12,6]])
9.0
"""
return sum([distance(point, points[index + 1]) for index, point in enumerate(points[:-1])])

• keep in mind that [:-1] means "all elements if the list without the last"
• enumerate is a function to enumerate all elements of a given sequence
In :
# enumerate example
seasons = ['spring', 'summer', 'fall', 'winter']
print(list(enumerate(seasons)))

[(0, 'spring'), (1, 'summer'), (2, 'fall'), (3, 'winter')]

In :
def traveling_salesman(points, start = None):
"""
Finds the shortest route to visit all the cities by bruteforce.
Time complexity is O(N!), so never use on long lists.

>>> travelling_salesman([[0,0],[10,0],[6,0]])
([0, 0], [6, 0], [10, 0])
>>> travelling_salesman([[0,0],[6,0],[2,3],[3,7],[0.5,9],[3,5],[9,1]])
([0, 0], [6, 0], [9, 1], [2, 3], [3, 5], [3, 7], [0.5, 9])
"""
if start is None:
start = points
return min([perm for perm in permutations(points) if perm == start], key = total_distance)


keep in mind that the permutation without repitition creates $\frac{n!}{(n-r)!}$ examples

where n is the amount of elements in the set and r is the number of elements we choose from the set

That means if we have a list of 4 elements we have 24 possible combinations:

$|perm([a,b,c,d])| = \frac{4!}{(4-4)!} = \frac{4!}{0!} = \frac{4!}{1} = 24$

In :
# Example for permutations
test_list = ["a", "b", "c", "d"]
test_permutations = permutations(test_list)
for test_perm in test_permutations:
print(test_perm)

('a', 'b', 'c', 'd')
('a', 'b', 'd', 'c')
('a', 'c', 'b', 'd')
('a', 'c', 'd', 'b')
('a', 'd', 'b', 'c')
('a', 'd', 'c', 'b')
('b', 'a', 'c', 'd')
('b', 'a', 'd', 'c')
('b', 'c', 'a', 'd')
('b', 'c', 'd', 'a')
('b', 'd', 'a', 'c')
('b', 'd', 'c', 'a')
('c', 'a', 'b', 'd')
('c', 'a', 'd', 'b')
('c', 'b', 'a', 'd')
('c', 'b', 'd', 'a')
('c', 'd', 'a', 'b')
('c', 'd', 'b', 'a')
('d', 'a', 'b', 'c')
('d', 'a', 'c', 'b')
('d', 'b', 'a', 'c')
('d', 'b', 'c', 'a')
('d', 'c', 'a', 'b')
('d', 'c', 'b', 'a')

• permutations returns tuples with all possible orderings without repeat
• function returns minimum of all possible tuples by the help of the function total_distance from above
In :
import datetime
def main():
points = [
[0, 0],
[1, 5.7],
[2, 3],
[3, 7],
[0.5, 9],
[3, 5],
[9, 1],
[10, 5],
[20, 5],
[12, 12],
[20, 19],
[25, 6],
[23, 7]
]

points = points[:-5]
#points = points[:-4]
#points = points[:-3]
#points = points[:-2]

then = datetime.datetime.now()
result = traveling_salesman(points)
distance_result = total_distance(result)
now = datetime.datetime.now()
print("calculation time", now - then)
print("""
The minimum distance to visit all
of the following points:\n
{0}

starting at
{1} is {2} and takes this
route:
{3}""".format(
points,
points,
distance_result,
result))

if __name__ == "__main__":
main()

calculation time 0:00:00.025904

The minimum distance to visit all
of the following points:

[[0, 0], [1, 5.7], [2, 3], [3, 7], [0.5, 9], [3, 5], [9, 1], [10, 5]]

starting at
[0, 0] is 25.90302275027582 and takes this
route:
([0, 0], [2, 3], [3, 5], [1, 5.7], [0.5, 9], [3, 7], [10, 5], [9, 1])


## Solving TSP with Hill Climbing¶

#### Recap: Hill Climbing¶

Idea:

• use only your local solution and evaluate your neighbors to find a better one
• repeat this step until no better neighbor exists

Pros:

• requires few resources (current state and neighbors)
• finds local optimum (global is possible)
• useful if the search space is huge (even unlimited)

Cons:

• is prone to get stuck at the top of local maximum and on plateaus
• strongly depends on “good” initialization

We will use standard Python lists to represent a route through our collection of cities. Each city will simply be assigned to a number from 0 to N-1 where N is the number of cities. Therefore, our list of cities will be a list of uniquie numbers between 0 and N-1 We also need to specify a "distance matrix" that we can use to keep track of distances between cities. To generate a distance matrix for a set of (x,y) coordinates we will use the following function:

In :
def cartesian_matrix(coordinates):
'''
Creates a distance matrix for the city coords using straight line distances
computed by the Euclidean distance of two points in the Cartesian Plane.
'''
matrix = {}
for i, p1 in enumerate(coordinates):
for j, p2 in enumerate(coordinates):
matrix[i,j] = distance(p1,p2)
return matrix


This function takes a list of (x,y) tuples and outputs a dictionary that contains the distance between any pair of cities:

In :
m = cartesian_matrix([(0,0), (1,0), (1,1)])
for k, v in m.items():
print(k, v)
print()
print(m[2,0])

(0, 0) 0.0
(0, 1) 1.0
(0, 2) 1.4142135623730951
(1, 0) 1.0
(1, 1) 0.0
(1, 2) 1.0
(2, 0) 1.4142135623730951
(2, 1) 1.0
(2, 2) 0.0

1.4142135623730951


[2,0] gives the distance between the city with number 2 and the city with number 0. In our case the result of [2,0] is the same for [0,2], but for other TSPs this may not be the case (for example if a street between two cities is only one way - we have to take another route)

#### Read City Coordinates from File¶

In :
def read_coords(file_handle):
coords = []
for line in file_handle:
x,y = line.strip().split(',')
coords.append((float(x), float(y)))
return coords

with open('city100.txt', 'r') as coord_file:
matrix = cartesian_matrix(coords)


On real world problems it may be more complicated to generate a distance matrix - you might need to take a map and calculate the real distances between cities.

#### Compute the Total Distance¶

In :
def tour_length(matrix, tour):
"""Sum up the total length of the tour based on the distance matrix"""
result = 0
num_cities = len(list(tour))
for i in range(num_cities):
j = (i+1) % num_cities
city_i = tour[i]
city_j = tour[j]
result += matrix[city_i, city_j]
return result


#### Implementing Tweak Operators¶

We will implement the two tweak operators as generator functions that will return all possible versions of a route that can be made in one step of the generator (in a random order).

Generators are iterators which can be only iterated once. They generate values on the fly and do not store them in memory. By using a generator function, we can get each possiblility and perhaps decide to not generate any more variations. This saves the overhead of generating all combinations at once.

In :
import random

def all_pairs(size, shuffle = random.shuffle):
r1 = list(range(size))
r2 = list(range(size))
if shuffle:
shuffle(r1)
shuffle(r2)
for i in r1:
for j in r2:
yield(i,j) # yield is an iterator function
# for each call of the generator it returns the next value in yield

In :
from copy import deepcopy

# Tweak 1
def swapped_cities(tour):
"""
Generator to create all possible variations where two
cities have been swapped
"""
ap = all_pairs(len(tour))
for i,j in ap:
if i < j:
copy = deepcopy(tour)
copy[i], copy[j] = tour[j], tour[i]
yield copy

# Tweak 2
def reversed_sections(tour):
"""
Generator to return all possible variations where the
section between two cities are swapped.
It preserves entire sections of a route,
yet still affects the ordering of multiple cities in one go.
"""
ap = all_pairs(len(tour))
for i,j in ap:
if i != j:
#print("indices from:",i, "to", j)
copy = deepcopy(tour)
if i < j:
copy[i:j+1] = reversed(tour[i:j+1])
else:
copy[i+1:] = reversed(tour[:j])
copy[:j] = reversed(tour[i+1:])
if copy != tour: # not returning same tour
yield copy
# usage
print("start tour swap:",[1,2,3,4])
for tour in swapped_cities([1,2,3,4]):
print(tour)
print()
print("start tour reverse section:",[1,2,3,4])
for tour in reversed_sections([1,2,3,4]):
print(tour)

start tour swap: [1, 2, 3, 4]
[1, 2, 4, 3]
[1, 3, 2, 4]
[1, 4, 3, 2]
[2, 1, 3, 4]
[3, 2, 1, 4]
[4, 2, 3, 1]

start tour reverse section: [1, 2, 3, 4]
[4, 3, 1, 2]
[1, 4, 3, 2]
[1, 3, 2, 4]
[4, 3, 2, 1]
[3, 2, 1, 4]
[2, 1, 3, 4]
[3, 4, 2, 1]
[2, 3, 4, 1]
[4, 1, 2, 3]
[1, 2, 4, 3]
[4, 2, 3, 1]


#### Getting Started with Hill Climbing¶

To start with Hill Climbing, we need two functions:

• init function that returns a random solution
• objective function that tells us how "good" a solution is

For the TSP, an init function will just return a tour of correct length that has cities aranged in random order.

The objective function will return the length of a tour.

We need to ensure that init function takes no arguments and returns a tour of the correct length and the objective function takes one argument (the solution tour) and returns its length.

Assume we have the city coordinates in a variable coords and our distance matrix in matrix, we can define the objective function and init function by using init_random_tour:

In :
def init_random_tour(tour_length):
tour = list(range(tour_length))
random.shuffle(list(tour))

init_function = lambda: init_random_tour(len(coords))
objective_function = lambda tour: tour_length(matrix, tour)


#### Short Explanation of Lambda Functions¶

is the creation of an anonymous function

• lambda definition does not include a return statement
• it always contains an expression which is returned
• you can put a lambda definition anywhere a function is expected
• you don't have to assign it to a variable
In :
# normal function definition
def f(x): return x**2

# lambda function definition
g = lambda x: x**2
print(f(5))
print(g(5))

25
25


## Basic Hill Climbing¶

In :
def hc(init_function, move_operator, objective_function, max_evaluations):
'''
Hillclimb until either max_evaluations is
reached or we are at a local optima.
'''
best = init_function()
best_score = objective_function(best)

num_evaluations = 1

while num_evaluations < max_evaluations:
# move around the current position
for next in move_operator(best):
if num_evaluations >= max_evaluations:
break

next_score = objective_function(next)
num_evaluations += 1
if next_score < best_score:
best = next
best_score = next_score
break # depth first search
break # couldn't find better move - must be a local max
return (num_evaluations, best_score, best)

In :
from PIL import Image, ImageDraw, ImageFont

def write_tour_to_img(coords, tour, title, img_file):
# shift all coords in a bit
coords = [(x+padding,y+padding) for (x,y) in coords]
maxx, maxy = 0,0
for x,y in coords:
maxx = max(x,maxx)
maxy = max(y,maxy)
img = Image.new("RGB",(int(maxx), int(maxy)), color=(255,255,255))

d=ImageDraw.Draw(img);
num_cities = len(tour)
for i in range(num_cities):
j = (i+1) % num_cities
city_i = tour[i]
city_j = tour[j]
x1,y1 = coords[city_i]
x2,y2 = coords[city_j]
d.line((int(x1), int(y1), int(x2), int(y2)), fill=(0,0,0))
d.text((int(x1)+7, int(y1)-5), str(i), font=font, fill=(32,32,32))

for x,y in coords:
x,y = int(x), int(y)
d.ellipse((x-5, y-5, x+5, y+5), outline=(0,0,0), fill=(196,196,196))

d.text((1,1), title, font=font, fill=(0,0,0))

del d
img.save(img_file, "PNG")

In :
def reload_image_for_jupyter(filename):
# pick a random integer with 1 in 2 billion chance of getting the same
# integer twice
import random
__counter__ = random.randint(0,2e9)

# now use IPython's rich display to display the html image with the
# new argument
from IPython.display import HTML, display
display(HTML('<img src="./'+filename+'?%d" alt="Schema of adaptive filter" height="100">' % __counter__))

In :
def do_hc_evaluations(evaluations , move_operator = swapped_cities):
max_evaluations = evaluations
then = datetime.datetime.now()
num_evaluations, best_score, best = hc(init_function, move_operator, objective_function, max_evaluations)
now = datetime.datetime.now()

print("computation time ", now - then)
print("best score:", best_score)
print("best route:", best)
filename = "test"+str(max_evaluations)+".PNG"
write_tour_to_img(coords, best, filename, open(filename, "ab"))

In :
move_operator = swapped_cities
#move_operator = reversed_sections
max_evaluations = 500
do_hc_evaluations(max_evaluations,move_operator)

computation time  0:00:00.033558
18280.516188122754
[56, 1, 67, 3, 79, 5, 66, 19, 20, 99, 23, 16, 74, 13, 6, 15, 11, 45, 18, 26, 61, 41, 88, 10, 24, 25, 14, 27, 95, 29, 35, 49, 4, 33, 37, 30, 50, 43, 8, 39, 34, 78, 42, 46, 44, 7, 93, 28, 17, 31, 38, 51, 52, 48, 65, 72, 64, 89, 58, 76, 55, 73, 96, 54, 85, 80, 62, 40, 68, 60, 70, 75, 53, 71, 12, 83, 59, 0, 21, 36, 22, 81, 87, 82, 77, 69, 91, 98, 2, 84, 90, 86, 92, 57, 94, 32, 47, 97, 63, 9] In :
move_operator = swapped_cities
#move_operator = reversed_sections
max_evaluations = 5000
do_hc_evaluations(max_evaluations,move_operator)

computation time  0:00:00.254526
11019.742218194911
[83, 46, 7, 98, 60, 77, 31, 79, 38, 41, 61, 23, 47, 15, 6, 80, 14, 56, 0, 68, 5, 35, 39, 99, 4, 40, 24, 89, 17, 27, 92, 50, 20, 8, 33, 70, 25, 85, 22, 18, 34, 19, 74, 52, 55, 76, 94, 53, 32, 13, 12, 11, 49, 43, 90, 63, 51, 75, 62, 28, 64, 48, 3, 84, 54, 86, 73, 66, 44, 2, 93, 45, 72, 95, 58, 37, 71, 65, 16, 36, 67, 88, 21, 10, 96, 97, 42, 81, 87, 82, 69, 9, 30, 78, 26, 1, 59, 57, 29, 91] In :
#move_operator = swapped_cities
move_operator = reversed_sections
max_evaluations = 50000
do_hc_evaluations(max_evaluations,move_operator)

computation time  0:00:02.476184
4184.116151227048
[86, 73, 9, 30, 19, 96, 10, 21, 34, 88, 67, 23, 22, 18, 36, 16, 61, 41, 63, 1, 26, 78, 90, 97, 3, 54, 84, 51, 48, 65, 47, 52, 11, 53, 71, 15, 32, 94, 55, 76, 95, 72, 58, 37, 6, 45, 13, 64, 62, 80, 83, 49, 43, 38, 14, 8, 74, 85, 28, 42, 4, 75, 12, 93, 2, 40, 25, 98, 46, 70, 27, 91, 17, 29, 89, 24, 33, 87, 81, 7, 57, 59, 82, 60, 69, 20, 79, 31, 50, 99, 77, 44, 66, 92, 5, 68, 0, 35, 39, 56] 