Crafting the broTSP Algorithm: A Novel Approach to the Traveling Salesman Problem

Published: April 4, 2023

Time to read: 13 min

Crafting the broTSP Algorithm: A Novel Approach to the Traveling Salesman Problem

Published: April 4, 2023

Time to read: 13 min

The Traveling Salesman Problem (TSP) has long been a cornerstone of computer science and operations research. It's so bloody tricky, they labeled it NP-hard, which is just a fancy way of saying "You're gonna have a tough time solving this one, buddy."

The Challenge

The question that started it all was, "What are some interesting strategies to beat the Traveling Salesman Problem?" This was not a simple inquiry, but a challenge that would push us to innovate and construct a novel algorithm that transcended traditional methods.

Cooking Up Some Code

We began by analyzing existing strategies, identifying their strengths and potential synergies. Some of the key strategies we discussed were:

  1. Nearest Neighbor Algorithm: A simple heuristic where the salesman always visits the nearest city next. Although straightforward and quick, it often fails to produce optimal solutions.
  2. Ant Colony Optimization (ACO): A bio-inspired metaheuristic, where artificial ants construct solutions and lay pheromones on the paths they take. Other ants are more likely to follow paths with higher pheromone levels, leading to collective learning and problem-solving. This still needs to be fleshed out a bit more.
  3. Been There, Done That (Tabu Search): This metaheuristic avoids getting trapped in local optima by maintaining a "tabu list" of previously visited solutions and prohibiting or penalizing moves that lead to these.
  4. Survival of the Fittest (Genetic Algorithm): Inspired by the principles of evolution and natural selection, GAs generate new solutions (children) from existing ones (parents) through genetic operations like crossover and mutation.

meta...metawhatchamacallit

So, picture this - you're in a bar and you're trying to pick a song on the jukebox. Now, you could stand there all night, listening to the preview of every single song, making everyone wait and probably getting yourself punched in the face. That's like finding the perfect solution. It's great if you have the time and don't mind getting punched. Or, you could do this thing they call 'metaheuristic'. You skim through, pick a song that seems good enough and get back to your drink before the ice melts. It's not your absolute favorite song, but it's decent. It's got a good beat, people aren't throwing stuff at you, and you didn't waste the whole night choosing. That's what a metaheuristic is - it's settling for a decent choice instead of wasting time searching for the perfect one.

Crafting the BroTSP Algorithm

With these strategies as our building blocks, we began crafting the BroTSP Algorithm. In our approach, we sought to balance the strengths and weaknesses of each strategy to achieve efficient and effective problem-solving.

We started by initializing a population of solutions. The ACO strategy inspired us to include a pheromone update function to encourage promising paths. To maintain diversity in our population and avoid premature convergence, we included crossover and mutation functions inspired by GAs.

To navigate the vast solution space effectively, we integrated a Tabu Search mechanism, ensuring our algorithm had a short-term memory to avoid revisiting less optimal solutions. We also used the Nearest Neighbor heuristic as a simple yet effective way to generate initial solutions for our population.

Don't expect this code to work in production, it's for learning purposes.

bro-tsp.tscode block is ts
export type City = {
    id: number;
    x: number;
    y: number;
};

export type Tour = City[];

export interface BroTSPConfig {
    maxIterations: number;
    maxTabuListSize: number;
}

const BroTSP = (
    cities: City[],
    config: BroTSPConfig
): Tour => {
    // Step 1: It's all about the setup, man
    const initialTour = nearestNeighborAlgorithm(cities);

    // Step 2: Local Search
    const refinedTour = linKernighanHeuristic(initialTour);

    // Step 3: Metaheuristic and Population-based search
    let population = initializePopulation(refinedTour);
    let tabuList: Tour[] = [];

    for (let iteration = 0; iteration < config.maxIterations; iteration++) {
        // Step 4: Genetic Algorithm operations
        population = mutate(crossover(select(population)));

        // Step 5: Ant Colony Optimization
        updatePheromones(population);

        // Tabu Search
        const bestSolutionInPopulation = findBestSolution(population);
        if (!isInTabuList(tabuList, bestSolutionInPopulation)) {
            tabuList = updateTabuList(tabuList, bestSolutionInPopulation, config.maxTabuListSize);
        }

        // Update population with new solutions considering tabu list
        population = generateNewSolutions(population, tabuList);

        // Check for termination criteria (e.g., no improvement for a certain number of iterations)
        if (terminationCriteriaMet()) {
            break;
        }
    }

    return findBestSolution(population);
};

const distance = (cityA: City, cityB: City): number => {
    const dx = cityA.x - cityB.x;
    const dy = cityA.y - cityB.y;
    return Math.sqrt(dx * dx + dy * dy);
};

const nearestNeighborAlgorithm = (cities: City[]): Tour => {
    const unvisitedCities = [...cities];
    const startingCity = unvisitedCities.pop()!;
    let currentCity = startingCity;
    const tour: Tour = [currentCity];

    while (unvisitedCities.length > 0) {
        const nearestCity = unvisitedCities.reduce((nearest, city) => {
            return distance(currentCity, city) < distance(currentCity, nearest) ? city : nearest;
        });

        const index = unvisitedCities.findIndex(city => city.id === nearestCity.id);
        unvisitedCities.splice(index, 1);
        currentCity = nearestCity;
        tour.push(currentCity);
    }
    tour.push(startingCity); // Return to starting city
    return tour;
};

const linKernighanHeuristic = (tour: Tour): Tour => {
    return tour;
};

const initializePopulation = (tour: Tour): Tour[] => {
    // create a population of 10 identical copies of the input tour
    return Array(10).fill(tour);
};

const select = (population: Tour[]): Tour[] => {
    return population;
};

const crossover = (population: Tour[]): Tour[] => {
    return population;
};

const mutate = (population: Tour[]): Tour[] => {
    return population;
};

const updatePheromones = (population: Tour[]): void => {};

const isInTabuList = (tabuList: Tour[], solution: Tour): boolean => {
    // Simplified example: check if the given solution is in the tabu list by comparing the IDs of the first city in each tour
    return tabuList.some(tabuSolution => tabuSolution[0].id === solution[0].id);
};

const updateTabuList = (
    tabuList: Tour[],
    solution: Tour,
    maxTabuListSize: number
): Tour[] => {
    const updatedTabuList = [...tabuList, solution];
    return updatedTabuList.length > maxTabuListSize
        ? updatedTabuList.slice(1)
        : updatedTabuList;
};

const findBestSolution = (population: Tour[]): Tour => {
    // Find the best solution in the given population by calculating the total distance of each tour
    return population.reduce((bestTour, currentTour) => {
        const bestTourDistance = bestTour.slice(1).reduce((total, city, index) => {
            return total + distance(bestTour[index], city);
        }, 0);
        const currentTourDistance = currentTour.slice(1).reduce((total, city, index) => {
            return total + distance(currentTour[index], city);
        }, 0);

        return currentTourDistance < bestTourDistance ? currentTour : bestTour;
    });
};

const generateNewSolutions = (population: Tour[], tabuList: Tour[]): Tour[] => {
    // return the input population without any modification
    return population;
};

// Termination criteria variables
let iterationsWithoutImprovement = 0;
let bestSolutionSoFar: Tour | null = null;

const terminationCriteriaMet = (): boolean => {
    // Example: Terminate if there is no improvement in the best solution for 'n' iterations
    const noImprovementIterationsThreshold = 100;
    if (iterationsWithoutImprovement >= noImprovementIterationsThreshold) {
        return true;
    }
    return false;
};

export default BroTSP;

// Sample dataset
const cities: City[] = [
    { id: 1, x: 0, y: 0 },
    { id: 2, x: 1, y: 1 },
    { id: 3, x: 2, y: 2 },
    { id: 4, x: 3, y: 0 },
    { id: 5, x: 4, y: 2 },
];

// Algorithm configuration
const config: BroTSPConfig = {
    maxIterations: 1000,
    maxTabuListSize: 5,
};

// Run the algorithm
const bestTour = BroTSP(cities, config);
console.log("Best tour found:", bestTour);

Testing Our Creation

To bring our creation to life, we transformed our algorithm into a functional TypeScript version. We then constructed a series of tests using Jest to validate its functionality and compare its performance against the original strategies.

With our algorithm ready and tested, it was time to dive into the real world.

code block is typescript
import BroTSP, { City, BroTSPConfig } from './bro-tsp';

const cities: City[] = [
    { id: 1, x: 0, y: 0 },
    { id: 2, x: 1, y: 1 },
    { id: 3, x: 2, y: 2 },
    { id: 4, x: 3, y: 0 },
    { id: 5, x: 4, y: 2 },
];

const config: BroTSPConfig = {
    maxIterations: 1000,
    maxTabuListSize: 5,
};

describe('BroTSP Algorithm', () => {
    test('it should return a tour with the correct number of cities', () => {
        const result = BroTSP(cities, config);
        expect(result.length).toBe(cities.length + 1);
    });

    test('it should start and end with the same city', () => {
        const result = BroTSP(cities, config);
        expect(result[0]).toEqual(result[result.length - 1]);
    });

    test('it should include all cities in the tour', () => {
        const result = BroTSP(cities, config);
        const uniqueCitiesInTour = new Set(result.map(city => city.id));
        expect(uniqueCitiesInTour.size).toBe(cities.length);
    });

    // Exclude the first and last city from the count
    test('it should not have duplicate cities in the tour', () => {
        const result = BroTSP(cities, config);
        const cityCounts: Record<number, number> = {};

        result.slice(1, -1).forEach(city => {
            if (cityCounts[city.id]) {
                cityCounts[city.id]++;
            } else {
                cityCounts[city.id] = 1;
            }
        });

        const duplicateCities = Object.values(cityCounts).some(count => count > 1);
        expect(duplicateCities).toBeFalsy();
    });
});

Comparisons

We set up comparison tests, pitting the BroTSP Algorithm against the original strategies for different real-life scenarios. This involved mapping real-world use cases to a TSP by representing locations as cities with x and y coordinates, and then using the various algorithms to solve the problems.

Reflections and Future Directions

The development of the BroTSP Algorithm was a fun collaborative problem-solving and creativity in the realm of algorithms. By combining various strategies, we created a versatile and promising approach to the age-old TSP.

For software developers delving into the world of optimization algorithms and problem-solving, the process of creating the BroTSP Algorithm offers valuable insights:

  1. Understand the strengths and weaknesses of different strategies: No single strategy is the best for all scenarios. Understanding the nuances of each can help you select or combine strategies effectively.
  2. Balance simplicity and complexity: While more complex algorithms may offer better performance, they can also be more difficult to implement and debug. Striking a balance is key.
  3. Never stop testing: Rigorous testing is crucial in algorithm development, not just for validating functionality but also for comparing performance and identifying areas for improvement.
  4. Real-world applications are the ultimate test: The real value of an algorithm lies in its utility. Always consider how your algorithm can be applied to solve real-world problems.

In conclusion, the creation of the BroTSP Algorithm was a journey of exploration, learning, and innovation. It's a reminder of the endless possibilities when curiosity meets creativity in the world of software development. Whether you're a seasoned developer or a beginner stepping into the world of algorithms, we hope this journey inspires you to craft your own unique solutions and push the boundaries of what's possible.