Skip to content

Solving the Travelling Salesman Problem and Using Ready Made Components

Zvi Rosenfeld edited this page Apr 13, 2020 · 3 revisions

In this tutorial, we'll see how a genetic algorithm can be used to solve the Travelling Salesman Problem. This tutorial will be using ready-made-components - so it will also show how to use those.

The Travelling Salesman Problem

In the Travelling Salesman Problem (TSP), we have a list of cities. A salesman must visit each city exactly once. The TSP tries to find the shortest route that will accomplish this. You can find the Wikipedia page here.

Creating a Genetic Algorithm to Solve the Problem

For our example we'll use 10 cities. Each city will be marked by a letter between "a" to "j". A chromosome is a list of 10 cities. The chromosome defines a route by visiting each city in the order it appears. For instance, the chromosome "abcdefghij" would define the route: a -> b -> c -> d -> e -> f -> g -> h -> i -> j. Since the salesman must visit each city exactly once, for a chromosome to be a legal route, it must contain each city exactly once.

DistanceCalclator

Let's start by creating a class that will calculate the distance traveled for a chromosome (route). The class will receive a dictionary with the coordinates of each city, and it will use the Math.Sqrt(Math.Pow(X1 - X2, 2) + Math.Pow(Y1 - Y2, 2)) equation to calculate the distance.

    class DistanceCalclator
    {
        private readonly IDictionary<string, Tuple<int, int>> locations;

        public DistanceCalclator(IDictionary<string, Tuple<int, int>> locations)
        {
            this.locations = locations;
        }

        public double GetDistance(IChromosome chromosome)
        {
            var totalDistance = 0.0;
            var cityVector = ((VectorChromosome<string>)chromosome).GetVector();
            for (int i = 0; i < cityVector.Length - 1; i++)
                totalDistance += GetDistance(cityVector[i], cityVector[i + 1]);

            return totalDistance;
        }

        private double GetDistance(string city1, string city2) =>
            Math.Sqrt(Math.Pow(locations[city1].Item1 - locations[city2].Item1, 2) + Math.Pow(locations[city1].Item2 - locations[city2].Item2, 2));
    }

Creating an IEvaluator

Let's use our DistanceCalclator to implement the IEvaluator class. One thing to note here is that a chromosome with a short route should give a high evaluation. This is why we subtract the route length from a number that is greater than the longest possible path to create the evaluation.

    class DistanceEvaluator : IEvaluator
    {
        private readonly DistanceCalclator distanceCalclator;
        private readonly double maxDistance;

        public DistanceEvaluator(IDictionary<string, Tuple<int, int>> locations)
        {
            distanceCalclator = new DistanceCalclator(locations);
            maxDistance = 1500 * locations.Count;
        }

        // Note that a shorter route should give a better evaluation
        public double Evaluate(IChromosome chromosome) => 
            maxDistance - distanceCalclator.GetDistance(chromosome);
    }

Creating an IMutationManager

For a mutation manager, we'll use the ready-made ExchangeMutationManager<T>. This mutation manager swaps two genomes in the chromosome. This will ensure that after the mutation the chromosome still contains each city exactly once.

IMutationManager<string> mutationManager = new ExchangeMutationManager<string>();

ICrossoverManager

For our ICrossoverManager, we'll again use a ready-made component. We need a manager that will ensure that the children will contain each genome (city) exactly once. There are a number of ready-made crossover manages that met this constraint. For our example we'll use the OrderCrossover<string>. You can read about how it works here.

ICrossoverManager crossoverManager = new OrderCrossover<string>(mutationManager, evaluator);

Creating an IPopulationGenerator

We need each chromosome to contain each city exactly once. For that, we can use the read-made AllElementsVectorChromosomePopulationGenerator<T>. This PopulationGenerator receives a collection in its constructor, and creates chromosomes that each contain every element from the collection exactly once.

string[] cities = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
IPopulationGenerator populationGenerator =
      new AllElementsVectorChromosomePopulationGenerator<string>(cities, mutationManager, evaluator);

Running the search.

Let's put everything together and run an actual search.

    class Program
    {
        private const int POPULATION_SIZE = 100;
        private const int GENERATIONS = 100;
        private static string[] cities = {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j"};
        private static Dictionary<string, Tuple<int, int>> locations = new Dictionary<string, Tuple<int, int>>
        {
            {"a", new Tuple<int, int>(10, 10)},
            {"b", new Tuple<int, int>(10, 200)},
            {"c", new Tuple<int, int>(1000, 10)},
            {"d", new Tuple<int, int>(500, 476)},
            {"e", new Tuple<int, int>(200, 800)},
            {"f", new Tuple<int, int>(80, 199)},
            {"g", new Tuple<int, int>(455, 78)},
            {"h", new Tuple<int, int>(511, 907)},
            {"i", new Tuple<int, int>(230, 400)},
            {"j", new Tuple<int, int>(611, 801)},
        };
        private static DistanceCalclator distanceCalclator = new DistanceCalclator(locations);

        static void Main(string[] args)
        {
            Console.WriteLine("Started!");

            IMutationManager<string> mutationManager = new ExchangeMutationManager<string>();
            IEvaluator evaluator = new DistanceEvaluator(locations);
            ICrossoverManager crossoverManager = new OrderCrossover<string>(mutationManager, evaluator);
            IPopulationGenerator populationGenerator =
                new AllElementsVectorChromosomePopulationGenerator<string>(cities, mutationManager, evaluator);

            GeneticSearchEngine engine =
                new GeneticSearchEngineBuilder(POPULATION_SIZE, GENERATIONS, crossoverManager, populationGenerator)
                    .SetMutationProbability(0.1).SetElitePercentage(0.02).Build();
            engine.OnNewGeneration += (Population p, IEnvironment e) => PrintBestChromosome(p);

            GeneticSearchResult result = engine.Run();

            Console.WriteLine("Finished!");
            Console.WriteLine(result.BestChromosome + ": " + distanceCalclator.GetDistance(result.BestChromosome));
            
            Console.ReadLine();
        }

        private static void PrintBestChromosome(Population population)
        {
            var bestEvaluation = 0.0;
            ChromosomeEvaluationPair bestPair = null;
            foreach (ChromosomeEvaluationPair chromosomeEvaluationPair in population)
                if (chromosomeEvaluationPair.Evaluation > bestEvaluation)
                {
                    bestEvaluation = chromosomeEvaluationPair.Evaluation;
                    bestPair = chromosomeEvaluationPair;
                }

            Console.WriteLine(bestPair.Chromosome + ": " + distanceCalclator.GetDistance(bestPair.Chromosome));
        }
    }