Skip to content

Hill Climbing Search Algorithm in Python

I am going to implement a hill climbing search algorithm on the traveling salesman problem in this tutorial. Hill-climbing is a local search algorithm that starts with an initial solution, it then tries to improve that solution until no more improvement can be made. This algorithm works for large real-world problems in which the path to the goal is irrelevant.

Hill-climbing is a simple algorithm that can be used to find a satisfactory solution fast, without any need to use a lot of memory. Hill-climbing can be used on real-world problems with a lot of permutations or combinations. The algorithm is often referred to as greedy local search because it iteratively searchs for a better solution.

Hill climbing uses randomly generated solutions that can be more or less guided by what the person implementing it thinks is the best solution. Hill-climbing can be implemented in many variants: stochastic hill climbing, first-choice hill climbing, random-restart hill climbing and more custom variants. The algorithm can be used to find a satisfactory solution to a problem of finding a configuration when it is impossible to test all permutations or combinations.

Traveling Salesman Problem (TSP)

The traveling salesman problem is famous because it is difficult to give an optimal solution in an reasonable time as the number of cities in the problem increases. I have found distance data for 13 cities (Traveling Salesman Problem). The problem is to find the shortest route from a starting location and back to the starting location after visiting all the other cities. This problem has 479001600 ((13-1)!) permutations and if we added one more city it would have 6227020800 ((14-1)!) permutations.

It would take to long to test all permutations, we use hill-climbing to find a satisfactory solution. The initial solution can be random, random with distance weights or a guessed best solution based on the shortest distance between cities.

  1. # Import libraries
  2. import random
  3. import copy
  4. # This class represent a state
  5. class State:
  6. # Create a new state
  7. def __init__(self, route:[], distance:int=0):
  8. self.route = route
  9. self.distance = distance
  10. # Compare states
  11. def __eq__(self, other):
  12. for i in range(len(self.route)):
  13. if(self.route[i] != other.route[i]):
  14. return False
  15. return True
  16. # Sort states
  17. def __lt__(self, other):
  18. return self.distance < other.distance
  19. # Print a state
  20. def __repr__(self):
  21. return ('({0},{1})\n'.format(self.route, self.distance))
  22. # Create a shallow copy
  23. def copy(self):
  24. return State(self.route, self.distance)
  25. # Create a deep copy
  26. def deepcopy(self):
  27. return State(copy.deepcopy(self.route), copy.deepcopy(self.distance))
  28. # Update distance
  29. def update_distance(self, matrix, home):
  30. # Reset distance
  31. self.distance = 0
  32. # Keep track of departing city
  33. from_index = home
  34. # Loop all cities in the current route
  35. for i in range(len(self.route)):
  36. self.distance += matrix[from_index][self.route[i]]
  37. from_index = self.route[i]
  38. # Add the distance back to home
  39. self.distance += matrix[from_index][home]
  40. # This class represent a city (used when we need to delete cities)
  41. class City:
  42. # Create a new city
  43. def __init__(self, index:int, distance:int):
  44. self.index = index
  45. self.distance = distance
  46. # Sort cities
  47. def __lt__(self, other):
  48. return self.distance < other.distance
  49. # Get the best random solution from a population
  50. def get_random_solution(matrix:[], home:int, city_indexes:[], size:int, use_weights=False):
  51. # Create a list with city indexes
  52. cities = city_indexes.copy()
  53. # Remove the home city
  54. cities.pop(home)
  55. # Create a population
  56. population = []
  57. for i in range(size):
  58. if(use_weights == True):
  59. state = get_random_solution_with_weights(matrix, home)
  60. else:
  61. # Shuffle cities at random
  62. random.shuffle(cities)
  63. # Create a state
  64. state = State(cities[:])
  65. state.update_distance(matrix, home)
  66. # Add an individual to the population
  67. population.append(state)
  68. # Sort population
  69. population.sort()
  70. # Return the best solution
  71. return population[0]
  72. # Get best solution by distance
  73. def get_best_solution_by_distance(matrix:[], home:int):
  74. # Variables
  75. route = []
  76. from_index = home
  77. length = len(matrix) - 1
  78. # Loop until route is complete
  79. while len(route) < length:
  80. # Get a matrix row
  81. row = matrix[from_index]
  82. # Create a list with cities
  83. cities = {}
  84. for i in range(len(row)):
  85. cities[i] = City(i, row[i])
  86. # Remove cities that already is assigned to the route
  87. del cities[home]
  88. for i in route:
  89. del cities[i]
  90. # Sort cities
  91. sorted = list(cities.values())
  92. sorted.sort()
  93. # Add the city with the shortest distance
  94. from_index = sorted[0].index
  95. route.append(from_index)
  96. # Create a new state and update the distance
  97. state = State(route)
  98. state.update_distance(matrix, home)
  99. # Return a state
  100. return state
  101. # Get a random solution by using weights
  102. def get_random_solution_with_weights(matrix:[], home:int):
  103. # Variables
  104. route = []
  105. from_index = home
  106. length = len(matrix) - 1
  107. # Loop until route is complete
  108. while len(route) < length:
  109. # Get a matrix row
  110. row = matrix[from_index]
  111. # Create a list with cities
  112. cities = {}
  113. for i in range(len(row)):
  114. cities[i] = City(i, row[i])
  115. # Remove cities that already is assigned to the route
  116. del cities[home]
  117. for i in route:
  118. del cities[i]
  119. # Get the total weight
  120. total_weight = 0
  121. for key, city in cities.items():
  122. total_weight += city.distance
  123. # Add weights
  124. weights = []
  125. for key, city in cities.items():
  126. weights.append(total_weight / city.distance)
  127. # Add a city at random
  128. from_index = random.choices(list(cities.keys()), weights=weights)[0]
  129. route.append(from_index)
  130. # Create a new state and update the distance
  131. state = State(route)
  132. state.update_distance(matrix, home)
  133. # Return a state
  134. return state
  135. # Mutate a solution
  136. def mutate(matrix:[], home:int, state:State, mutation_rate:float=0.01):
  137. # Create a copy of the state
  138. mutated_state = state.deepcopy()
  139. # Loop all the states in a route
  140. for i in range(len(mutated_state.route)):
  141. # Check if we should do a mutation
  142. if(random.random() < mutation_rate):
  143. # Swap two cities
  144. j = int(random.random() * len(state.route))
  145. city_1 = mutated_state.route[i]
  146. city_2 = mutated_state.route[j]
  147. mutated_state.route[i] = city_2
  148. mutated_state.route[j] = city_1
  149. # Update the distance
  150. mutated_state.update_distance(matrix, home)
  151. # Return a mutated state
  152. return mutated_state
  153. # Hill climbing algorithm
  154. def hill_climbing(matrix:[], home:int, initial_state:State, max_iterations:int, mutation_rate:float=0.01):
  155. # Keep track of the best state
  156. best_state = initial_state
  157. # An iterator can be used to give the algorithm more time to find a solution
  158. iterator = 0
  159. # Create an infinite loop
  160. while True:
  161. # Mutate the best state
  162. neighbor = mutate(matrix, home, best_state, mutation_rate)
  163. # Check if the distance is less than in the best state
  164. if(neighbor.distance >= best_state.distance):
  165. iterator += 1
  166. if (iterator > max_iterations):
  167. break
  168. if(neighbor.distance < best_state.distance):
  169. best_state = neighbor
  170. # Return the best state
  171. return best_state
  172. # The main entry point for this module
  173. def main():
  174. # Cities to travel
  175. cities = ['New York', 'Los Angeles', 'Chicago', 'Minneapolis', 'Denver', 'Dallas', 'Seattle', 'Boston', 'San Francisco', 'St. Louis', 'Houston', 'Phoenix', 'Salt Lake City']
  176. city_indexes = [0,1,2,3,4,5,6,7,8,9,10,11,12]
  177. # Index of start location
  178. home = 2 # Chicago
  179. # Max iterations
  180. max_iterations = 1000
  181. # Distances in miles between cities, same indexes (i, j) as in the cities array
  182. matrix = [[0, 2451, 713, 1018, 1631, 1374, 2408, 213, 2571, 875, 1420, 2145, 1972],
  183. [2451, 0, 1745, 1524, 831, 1240, 959, 2596, 403, 1589, 1374, 357, 579],
  184. [713, 1745, 0, 355, 920, 803, 1737, 851, 1858, 262, 940, 1453, 1260],
  185. [1018, 1524, 355, 0, 700, 862, 1395, 1123, 1584, 466, 1056, 1280, 987],
  186. [1631, 831, 920, 700, 0, 663, 1021, 1769, 949, 796, 879, 586, 371],
  187. [1374, 1240, 803, 862, 663, 0, 1681, 1551, 1765, 547, 225, 887, 999],
  188. [2408, 959, 1737, 1395, 1021, 1681, 0, 2493, 678, 1724, 1891, 1114, 701],
  189. [213, 2596, 851, 1123, 1769, 1551, 2493, 0, 2699, 1038, 1605, 2300, 2099],
  190. [2571, 403, 1858, 1584, 949, 1765, 678, 2699, 0, 1744, 1645, 653, 600],
  191. [875, 1589, 262, 466, 796, 547, 1724, 1038, 1744, 0, 679, 1272, 1162],
  192. [1420, 1374, 940, 1056, 879, 225, 1891, 1605, 1645, 679, 0, 1017, 1200],
  193. [2145, 357, 1453, 1280, 586, 887, 1114, 2300, 653, 1272, 1017, 0, 504],
  194. [1972, 579, 1260, 987, 371, 999, 701, 2099, 600, 1162, 1200, 504, 0]]
  195. # Get the best route by distance
  196. state = get_best_solution_by_distance(matrix, home)
  197. print('-- Best solution by distance --')
  198. print(cities[home], end='')
  199. for i in range(0, len(state.route)):
  200. print(' -> ' + cities[state.route[i]], end='')
  201. print(' -> ' + cities[home], end='')
  202. print('\n\nTotal distance: {0} miles'.format(state.distance))
  203. print()
  204. # Get the best random route
  205. state = get_random_solution(matrix, home, city_indexes, 100)
  206. print('-- Best random solution --')
  207. print(cities[home], end='')
  208. for i in range(0, len(state.route)):
  209. print(' -> ' + cities[state.route[i]], end='')
  210. print(' -> ' + cities[home], end='')
  211. print('\n\nTotal distance: {0} miles'.format(state.distance))
  212. print()
  213. # Get a random solution with weights
  214. state = get_random_solution(matrix, home, city_indexes, 100, use_weights=True)
  215. print('-- Best random solution with weights --')
  216. print(cities[home], end='')
  217. for i in range(0, len(state.route)):
  218. print(' -> ' + cities[state.route[i]], end='')
  219. print(' -> ' + cities[home], end='')
  220. print('\n\nTotal distance: {0} miles'.format(state.distance))
  221. print()
  222. # Run hill climbing to find a better solution
  223. state = get_best_solution_by_distance(matrix, home)
  224. state = hill_climbing(matrix, home, state, 1000, 0.1)
  225. print('-- Hill climbing solution --')
  226. print(cities[home], end='')
  227. for i in range(0, len(state.route)):
  228. print(' -> ' + cities[state.route[i]], end='')
  229. print(' -> ' + cities[home], end='')
  230. print('\n\nTotal distance: {0} miles'.format(state.distance))
  231. print()
  232. # Tell python to run main method
  233. if __name__ == "__main__": main()

Output

I choosed to use the best solution by distance as an initial solution, the best solution is mutated in each iteration and a mutated solution will be the new best solution if the total distance is less than the distance for the current best solution. I am using extra iterations to give the algorithm more time to find a better solution. The best solution is 7293 miles.

  1. -- Best solution by distance --
  2. Chicago -> St. Louis -> Minneapolis -> Denver -> Salt Lake City -> Phoenix -> Los Angeles -> San Francisco -> Seattle -> Dallas -> Houston -> New York -> Boston -> Chicago
  3. Total distance: 8131 miles
  4. -- Best random solution --
  5. Chicago -> Boston -> Salt Lake City -> Los Angeles -> San Francisco -> Seattle -> Denver -> Houston -> Dallas -> Phoenix -> St. Louis -> Minneapolis -> New York -> Chicago
  6. Total distance: 11091 miles
  7. -- Best random solution with weights --
  8. Chicago -> Boston -> New York -> St. Louis -> Dallas -> Houston -> Phoenix -> Seattle -> Denver -> Salt Lake City -> Los Angeles -> San Francisco -> Minneapolis -> Chicago
  9. Total distance: 9155 miles
  10. -- Hill climbing solution --
  11. Chicago -> St. Louis -> Minneapolis -> Denver -> Salt Lake City -> Seattle -> San Francisco -> Los Angeles -> Phoenix -> Dallas -> Houston -> New York -> Boston -> Chicago
  12. Total distance: 7534 miles
Tags:

Leave a Reply

Your email address will not be published. Required fields are marked *