Travelling Salesman Problem
r
A
"""
Use a genetic algorithm to solve the travelling salesman problem (TSP)
which asks the following question:
"Given a list of cities and the distances between each pair of cities, what is the
shortest possible route that visits each city exactly once and returns to the origin
city?"
https://en.wikipedia.org/wiki/Genetic_algorithm
https://en.wikipedia.org/wiki/Travelling_salesman_problem
Author: Clark
"""
import copy
import random
cities = {
0: [0, 0],
1: [0, 5],
2: [3, 8],
3: [8, 10],
4: [12, 8],
5: [12, 4],
6: [8, 0],
7: [6, 2],
}
def main(
cities: dict[int, list[int]],
population_size: int,
iterations_num: int,
crossover_probability: float,
mutation_probability: float,
) -> tuple[list[int], float]:
"""
Genetic algorithm main function
The algorithm is stochastic, so seed ``random`` and assert invariants of the
returned tour rather than one exact ordering (not reproducible across
platforms / Python versions).
>>> import random
>>> random.seed(0)
>>> path, best = main(cities=cities, population_size=100, iterations_num=100,
... crossover_probability=0.6, mutation_probability=0.2)
>>> path[0] == 0 and path[-1] == 0 # starts and ends at the origin city
True
>>> sorted(path[:-1]) == sorted(cities) # every city visited exactly once
True
>>> 37 <= best < 45 # converges close to the optimal round-trip (~37.9)
True
>>> main(cities={0: [0, 0], 1: [2, 2]}, population_size=10, iterations_num=10,
... crossover_probability=0.6, mutation_probability=0.2)
([0, 1, 0], 5.656854249492381)
>>> main(cities={},population_size=10,iterations_num=10,
... crossover_probability=0.6,mutation_probability=0.2)
Traceback (most recent call last):
...
IndexError: list assignment index out of range
"""
best_path: list[int] = []
best_distance = float("inf")
chromosomes, cities_list = init(cities, population_size)
fitness_matrix, best_path, best_distance = fitness(
cities, chromosomes, best_path, best_distance
)
for _ in range(iterations_num):
"""
Uncomment to choose another selection operator
Only one of the two selection operators can be uncommented at the same time.
"""
# chromosomes = chose_ts(fitness_matrix, chromosomes, population_size)
chromosomes = chose_rws(fitness_matrix, chromosomes, population_size)
for x in range(int(population_size / 2)): # Population crossover
chromosomes[x], chromosomes[x + int(population_size / 2)] = crossing(
chromosomes[x],
chromosomes[x + int(population_size / 2)],
crossover_probability,
cities_list,
)
for x in range(population_size): # Population variation
chromosomes[x] = mutate(chromosomes[x], mutation_probability)
fitness_matrix, best_path, best_distance = fitness(
cities, chromosomes, best_path, best_distance
)
return best_path, best_distance
def distance(city1: list[int], city2: list[int]) -> float:
"""
Calculate the distance between two coordinate points
>>> distance([0, 0], [3, 4] )
5.0
>>> distance([0, 0], [-3, 4] )
5.0
>>> distance([0, 0], [-3, -4] )
5.0
"""
return (((city1[0] - city2[0]) ** 2) + ((city1[1] - city2[1]) ** 2)) ** 0.5
def init(
cities: dict[int, list[int]], population_size: int
) -> tuple[list[list[int]], list[int]]:
"""
Initialization generates initial population
>>> init(cities={0: [0, 0], 1: [2, 2]}, population_size=2)
([[0, 1, 0], [0, 1, 0]], [1])
>>> init(cities={0: [0, 0], 1: [2, 2]}, population_size=0)
([], [1])
>>> init(cities={},population_size=2)
Traceback (most recent call last):
...
IndexError: list assignment index out of range
"""
chromosomes = []
cities_list = list(cities.keys())
del cities_list[0]
for _ in range(population_size):
chromosome = []
chromosome.append(0) # Add starting point
chromosome.extend(random.sample(cities_list, len(cities_list)))
chromosome.append(0) # Add end point
chromosomes.append(chromosome)
return chromosomes, cities_list
def fitness(
cities: dict[int, list[int]],
chromosomes: list[list[int]],
best_path: list[int],
best_distance: float,
) -> tuple[list[float], list[int], float]:
"""
Calculate population fitness
Generate a fitness matrix and obtain the optimal value in the current population
>>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[[0,1,0]],
... best_path=[], best_distance=float("inf"))
([0.17677669529663687], [0, 1, 0], 5.656854249492381)
>>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[[0,1,0],[0,1,0]],
... best_path=[], best_distance=float("inf"))
([0.17677669529663687, 0.17677669529663687], [0, 1, 0], 5.656854249492381)
>>> fitness(cities={}, chromosomes=[[0,1,0]],
... best_path=[], best_distance=float("inf"))
Traceback (most recent call last):
...
KeyError: 0
>>> fitness(cities={0: [0, 0], 1: [2, 2]},chromosomes=[],
... best_path=[], best_distance=float("inf"))
([], [], inf)
"""
fitness_matrix = []
new_best_path = best_path
new_best_distance = best_distance
for chromosome in chromosomes:
total_distance = 0.0
for i in range(len(chromosome) - 1): # Calculate total distance
total_distance += distance(cities[chromosome[i]], cities[chromosome[i + 1]])
fitness_matrix.append(1 / total_distance)
if total_distance < new_best_distance:
new_best_path = chromosome
new_best_distance = total_distance
return fitness_matrix, new_best_path, new_best_distance
def chose_ts(
fitness_matrix: list[float], chromosomes: list[list[int]], population_size: int
) -> list[list[int]]:
"""
A type of selection operator
Tournament Selection
>>> chose_ts(fitness_matrix=[1], chromosomes=[[0,1,0]], population_size=1)
[[0, 1, 0]]
>>> chose_ts(fitness_matrix=[1], chromosomes=[0,1,0], population_size=0)
[]
>>> chose_ts(fitness_matrix=[], chromosomes=[[0,1,0]], population_size=1)
Traceback (most recent call last):
...
IndexError: list index out of range
>>> chose_ts(fitness_matrix=[1], chromosomes=[], population_size=1)
Traceback (most recent call last):
...
IndexError: list index out of range
>>> import random
>>> random.seed(0)
>>> chose_ts(fitness_matrix=[1], chromosomes=[0, 1, 0], population_size=2)
Traceback (most recent call last):
...
IndexError: list index out of range
"""
chromosomes_new = []
for _ in range(population_size):
x1 = random.randint(0, population_size - 1)
x2 = random.randint(0, population_size - 1)
if fitness_matrix[x1] >= fitness_matrix[x2]:
chromosomes_new.append(chromosomes[x1])
else:
chromosomes_new.append(chromosomes[x2])
return chromosomes_new
def chose_rws(
fitness_matrix: list[float], chromosomes: list[list[int]], population_size: int
) -> list[list[int]]:
"""
A type of selection operator
Roulette Wheel Selection
>>> chose_rws(fitness_matrix=[1], chromosomes=[[0,1,0]], population_size=1)
[[0, 1, 0]]
>>> chose_rws(fitness_matrix=[1], chromosomes=[0,1,0], population_size=0)
[0, 1, 0]
>>> chose_rws(fitness_matrix=[], chromosomes=[[0,1,0]], population_size=1)
Traceback (most recent call last):
...
IndexError: list index out of range
>>> chose_rws(fitness_matrix=[1], chromosomes=[], population_size=1)
Traceback (most recent call last):
...
IndexError: list index out of range
>>> chose_rws(fitness_matrix=[1], chromosomes=[0,1,0], population_size=2)
[0, 0, 0]
"""
probabilitys = [0.0] * len(fitness_matrix)
total_probability = 0.0
for i in fitness_matrix:
total_probability += i
for i in range(len(fitness_matrix)):
probabilitys[i] = fitness_matrix[i] / total_probability
chromosomes_new = copy.deepcopy(chromosomes)
for i in range(population_size):
k = 0.0
r = random.uniform(0, 1)
for j in range(population_size):
k = k + probabilitys[j]
if r <= k:
chromosomes_new[i] = chromosomes[j]
break
return chromosomes_new
def crossing(
chromosome_a: list[int],
chromosome_b: list[int],
crossover_probability: float,
cities_list: list[int],
) -> tuple[list[int], list[int]]:
"""
Population crossover
>>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0],
... crossover_probability=0,cities_list=[1])
([0, 1, 0], [0, 1, 0])
>>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0],
... crossover_probability=1,cities_list=[1])
([0, 1, 0], [0, 1, 0])
>>> crossing(chromosome_a=[0,1,0], chromosome_b=[],
... crossover_probability=1,cities_list=[1])
Traceback (most recent call last):
...
IndexError: list index out of range
>>> crossing(chromosome_a=[0,1,0], chromosome_b=[0,1,0],
... crossover_probability=1,cities_list=[])
([0, 1, 0], [0, 1, 0])
"""
new_chromosome_a = copy.deepcopy(chromosome_a)
new_chromosome_b = copy.deepcopy(chromosome_b)
if random.random() <= crossover_probability:
crossover_segment = sorted(
[
random.randint(0, len(new_chromosome_a) - 1),
random.randint(0, len(new_chromosome_a) - 1),
]
)
for k in range((crossover_segment[1] - crossover_segment[0]) + 1):
(
new_chromosome_a[crossover_segment[0] + k],
new_chromosome_b[crossover_segment[0] + k],
) = (
new_chromosome_b[crossover_segment[0] + k],
new_chromosome_a[crossover_segment[0] + k],
)
for chromosome in [new_chromosome_a, new_chromosome_b]:
unique_elements_set = set(chromosome)
if len(unique_elements_set) != len(
chromosome
): # Determine whether the chromosome segment has duplication
for segment_index in range(
(crossover_segment[1] - crossover_segment[0]) + 1
):
target_index = 0
for chrom_index in range(
1, len(chromosome) - 1
): # Exclude start and end points 0 when searching
if chrom_index == (crossover_segment[0] + segment_index):
continue
if (
chromosome[chrom_index]
== chromosome[crossover_segment[0] + segment_index]
):
target_index = chrom_index
break
if target_index != 0:
cities_list_copy = copy.deepcopy(cities_list)
for t in chromosome:
try:
cities_list_copy.remove(t)
except ValueError:
continue
chromosome[target_index] = random.sample(cities_list_copy, 1)[0]
return new_chromosome_a, new_chromosome_b
def mutate(chromosome: list[int], mutation_probability: float) -> list[int]:
"""
Population variation: swap two interior cities (endpoints stay at 0).
>>> mutate([0, 1, 0], mutation_probability=0) # no mutation -> unchanged
[0, 1, 0]
>>> import random
>>> random.seed(1)
>>> mutate([0, 1, 2, 3, 0], mutation_probability=1) # swaps two interior cities
[0, 2, 1, 3, 0]
An empty chromosome has no interior cities to swap; match only the exception
type since the exact stdlib message changes across Python versions.
>>> mutate([], mutation_probability=1) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError
"""
new_chromosome = copy.deepcopy(chromosome)
if random.random() <= mutation_probability:
mutate_location = [
random.randint(1, len(new_chromosome) - 2),
random.randint(1, len(new_chromosome) - 2),
] # Exclude start and end points 0
new_chromosome[mutate_location[0]], new_chromosome[mutate_location[1]] = (
new_chromosome[mutate_location[1]],
new_chromosome[mutate_location[0]],
)
return new_chromosome
if __name__ == "__main__":
best_path, best_distance = main(
cities=cities,
population_size=100,
iterations_num=100,
crossover_probability=0.6,
mutation_probability=0.2,
)
print(f"{best_path = }")
print(f"{best_distance = }")