How to Implement the Hill Climbing Algorithm in Python
By: Hein De Haan
Tags: Python, Optimization, Programming, Coding, Hill Climbing

How to Implement the Hill Climbing Algorithm in Python

Hill climbing is a mathematical optimization algorithm, which means its purpose is to find the best solution to a problem which has a (large) number of possible solutions. Explaining the algorithm (and optimization in general) is best done using an example. In the Travelling salesman problem, we have a salesman who needs to visit a number of cities exactly once, after which he returns to the first city. The distances between each pair of cities are known, and we need to find the shortest route. As you can imagine, there is (often) a large number of possible solutions (routes) to a specific Travelling salesman problem; the goal is to find the best (i.e. the shortest) solution.

Photo by Joseph Liu on Unsplash

Hill climbing tries to find the best solution to this problem by starting out with a random solution, and then generate neighbours: solutions that only slightly differ from the current one. If the best of those neighbours is better (i.e. shorter) than the current one, it replaces the current solution with this better solution. It then repeats the pattern by again creating neighbours. If at some point no neighbour is better than the current solution, it returns the then current solution. That’s it! The algorithm is quite simple, but it needs to be said that it doesn’t always find the best solution. It can get stuck in a local maximum: a place where the current solution isn’t the best solution to the problem, but where none of the direct neighbours of the current solution are better than the current solution. As described, the algorithm will stop at such a point, unfortunately without returning the best solution. More complicated algorithms exist that have a higher chance of finding the best solution, but they often take more computational resources.

Anyway, let’s start coding the Travelling salesman problem and Hill climbing in Python!

Create a Travelling salesman problem

First, let’s code an instantiation of the Travelling salesman problem. If you think about it, such an instantiation should be a list of cities, where each one has information about the distances from there to the other cities. Of course, the distance from each city to itself is zero, and the distance from city A to city B is the same as the distance from city B to city A. That gives us a list, containing n lists of size n (where in this case n equals 4):

As we can see, the first city’s distance to itself is, of course, 0, and, for example, its distance to the third city is 500. One can see that the third city’s distance to the first is also 500 (the first entry of the third array), as it should.

Create a random solution generator

For Hill climbing to work, it has to start with a random solution to our Travelling salesman problem. From there, it can generate neighbouring solutions and start the optimization process. How should we design such a solution? Well, a solution to the Travelling salesman problem could simply be a list of identifiers of all cities, in the order the salesman should visit them. Each city has to be visited exactly once. Let’s therefore first create a list of identifiers of all cities (called cities in the code below), and from there on iteratively pick a city from that list at random and add it to our solution. Python’s range function is excellent for creating cities, as it creates a range of all numbers from 0 to the argument given, which in this case is the length of the problem itself, as the problem itself contains one entry for each city.

Since every city can only be visited one, after its identifier is added to our solution we then remove that city’s identifier from the city identifier list. Our function needs the Travelling salesman problem itself for information about the distances between cities. See the code below (note that you will need to import random for this to work):

Create a function calculating the length of a route

Since we want our Hill climber to find the shortest solution, we need a function calculating the length of a specific solution. This function needs the Travelling salesman problem itself (for information about the distances between cities) and of course the solution of which we need the route length. Since a solution is a list of all cities in a specific order, we can just iterate over a solution and use the tsp argument to add the distance to each new city to our total route length. The iterator i “visits” each city, so i-1 is “at” the previous city or the last city when i equals 0 (which is exactly what we want, since we want to end up at the first city again). solution[i] thus gives us the current city, and solution[i-1] gives us the previous one. Then we simply use the tsp to get the distance between these cities, which we add to the total length of the route (routeLength).

Create a function generating all neighbours of a solution

As explained before, Hill climbing works in part by generating all neighbouring solutions to the current solution. Let’s create a function doing exactly that. A neighbouring solution is a solution that’s only slightly different from the current solution. Note also that a neighbour still needs to be a correct solution: every city still needs to be visited exactly once. We can accomplish both by creating a neighbour as follows: copy the current solution, and then make two cities swap places! This way, we create a slightly different solution that’s still correct. Since we want to create all neighbours to a solution, and need to make cities swap places, we need to create two for loops, one nested in the other, both iterating the current solution. Since swapping city A with city B is the same as swapping city B with city A, our second loop needs to only loop over those cities the first loop hasn’t looped over yet. Inside the second loop, we create our neighbour with the swapped cities and add it to our neighbours list.

Create a function finding the best neighbour

Now that we have a function generating all neighbours to a solution, it’s time we create one finding the best of these neighbours. This is a pretty straightforward function: it first sets the bestNeighbour to the first neighbour in the list of neighbours (and bestRouteLength to its route length), and then iterates over all neighbours; when a neighbour has a shorter route length, both bestNeighbour and bestRouteLength are updated. This way, the best neighbour is found:

Create the Hill climbing algorithm

It’s time for the core function! After creating the previous functions, this step has become quite easy:

First, we make a random solution and calculate its route length. We then create the neighbouring solutions, and find the best one. From there on, as long as the best neighbour is better than the current solution, we repeat the same pattern with the current solution each time being updated with the best neighbour. When this process stops, we return the current solution (and its route length).

Full code

Here’s the complete Python file, with a main function defining a Travelling salesman problem and calling the Hill climber.

Let’s try it!

Now that we have written the full code, it’s time to try our algorithm! The example Travelling salesman problem I gave in lines 55–60 is “rectangular”: we have four cities, each located in the corner of a rectangular shape. The long side of the rectangle is 400 kilometers (or whatever unit you like) long, while the short side is 300. That makes the diagonal 500 kilometers long. It seems obvious that the shortest routes actually travel the sides of this rectangle, which would make the length of the shortest route 2 x 400 + 2 x 300 = 1400 kilometers. Let’s see if our Hill climber finds one of the shortest routes! There are multiple, since our salesman can start at any city.

After running the code multiple times (results can differ because the initial solution is random), I consistently see a best route length of 1400. That’s great! Our algorithm finds the best route. Of course, this was a very simple problem, consisting of only 4 cities. Let’s expand on our code a bit, and add a problem generator that can generate larger problems for our Hill climber to solve and see how well it does that!

Create a problem generator

As it turns out, it’s quite straightforward to make a problem generator for our situation. Let’s define a function taking one argument: nCities, which specifies the number of cities we want for our Travelling salesman problem. The problem generator can then simply loop nCities times, each time creating a list of distances (distances) for a new city. Such a distance list can be filled with random values, except where the value is already known: when the loop is adding the distance of the city to itself (which should be zero), and when it is adding a value it has already calculated. This last one is the case when the algorithm is adding the distance from city B to city A when it has already added the distance from city A to city B, since these distance should be the same. This requirement is fulfilled in lines 8 and 9.

Each distances can be simply added to the initially empty tsp list, which in the end is returned.

Use the problem generator to create a big problem

Now that we have a problem generator, let’s find out how our Hill climber performs at bigger problems. I modified the main function a bit:

As you can see, the tsp is now assigned a problem from the problem generator: one with ten cities. The main function calls the Hill climber ten times with this problem, to see whether it gives the same quality solution each time. If it doesn’t, we know the Hill climber doesn’t perform optimally each time! I get these results (remember, yours might be different due to the randomness of the initial solutions):

([0, 4, 3, 7, 8, 5, 6, 2, 9, 1], 2124)

([6, 2, 3, 7, 8, 5, 1, 4, 0, 9], 1975)

([5, 1, 4, 0, 9, 6, 2, 3, 7, 8], 1975)

([1, 4, 0, 9, 6, 2, 3, 7, 8, 5], 1975)

([1, 4, 0, 9, 6, 2, 3, 7, 8, 5], 1975)

([1, 5, 8, 7, 3, 2, 6, 9, 0, 4], 1975)

([7, 3, 2, 6, 9, 0, 4, 1, 5, 8], 1975)

([5, 1, 4, 0, 8, 7, 3, 2, 9, 6], 1938)

([4, 0, 8, 5, 6, 9, 2, 3, 7, 1], 1908)

([9, 6, 5, 8, 7, 1, 0, 4, 3, 2], 1981)

As we can see, the score is not the same for each solution. This means our Hill climber isn’t perfect, but I warned about this in the beginning. It would be interesting to compare Hill climbing to more sophisticated algorithms. They may perform better, but take more time to do so. However, this is something for a future post! For now, thanks for reading, and I hope you enjoyed the tutorial!