Tutorial
⏱️ ~50 minutes 🎓 Intermediate 🛠️ JavaScript · Canvas 2D · Algorithms

Genetic Algorithm — Solve the TSP

The Travelling Salesman Problem is NP-hard — exhaustive search is impossible even for 30 cities. Genetic Algorithms find near-optimal solutions by mimicking evolution: populations of routes compete, reproduce (ordered crossover), and occasionally mutate. This tutorial builds it from scratch, animated on a canvas.

Prerequisites

Represent the Problem

A TSP tour is a permutation of city indices. Number of possible tours for N cities = (N-1)! / 2. For N=20 that's ~60 billion — way too many to check exhaustively.

// Generate random cities
const N_CITIES = 24;
const cities = Array.from({ length: N_CITIES }, () => ({
  x: 30 + Math.random() * 740,
  y: 30 + Math.random() * 440,
}));

// A tour = array of city indices in visit order
// e.g. [0, 5, 3, 11, ...] — visit city 0, then 5, then 3, etc.
// Always return to city 0 (the start) to complete the loop

function tourDistance(tour) {
  let dist = 0;
  for (let i = 0; i < tour.length; i++) {
    const a = cities[tour[i]];
    const b = cities[tour[(i + 1) % tour.length]];
    dist += Math.hypot(b.x - a.x, b.y - a.y);
  }
  return dist;
}

Fitness Function

We minimise distance. For selection we need a fitness that is higher is better — invert the distance:

function fitness(tour) {
  return 1 / tourDistance(tour);
}
// Shorter tour → smaller distance → larger fitness → more likely to reproduce

Initialise Random Population

const POP_SIZE = 200;

function randomTour() {
  const tour = Array.from({ length: N_CITIES }, (_, i) => i);
  // Fisher-Yates shuffle
  for (let i = tour.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [tour[i], tour[j]] = [tour[j], tour[i]];
  }
  return tour;
}

let population = Array.from({ length: POP_SIZE }, randomTour);
let best = [...population[0]]; // track all-time best

Tournament Selection

Pick k random individuals from the population and return the fittest one. Higher k = more selection pressure (converges faster but may lose diversity):

function tournament(pop, k = 5) {
  let best = null;
  let bestFit = -Infinity;
  for (let i = 0; i < k; i++) {
    const candidate = pop[Math.floor(Math.random() * pop.length)];
    const f = fitness(candidate);
    if (f > bestFit) { bestFit = f; best = candidate; }
  }
  return best;
}

Ordered Crossover (OX)

Regular single-point crossover doesn't work for permutations — it creates duplicate cities. OX preserves relative order:

function orderedCrossover(parent1, parent2) {
  const n = parent1.length;
  // Pick a random segment from parent1
  const start = Math.floor(Math.random() * n);
  const end   = start + Math.floor(Math.random() * (n - start));

  const child = new Array(n).fill(-1);
  const segSet = new Set();

  // Copy segment from parent1
  for (let i = start; i <= end; i++) {
    child[i] = parent1[i];
    segSet.add(parent1[i]);
  }

  // Fill remaining positions from parent2 in order
  let pos = (end + 1) % n;
  for (let i = 0; i < n; i++) {
    const city = parent2[(end + 1 + i) % n];
    if (!segSet.has(city)) {
      child[pos] = city;
      pos = (pos + 1) % n;
    }
  }
  return child;
}

Swap Mutation

Mutation prevents premature convergence by randomly exploring nearby tours. Swap mutation picks two random cities and exchanges them:

const MUTATION_RATE = 0.02;

function mutate(tour) {
  const child = [...tour];
  for (let i = 0; i < child.length; i++) {
    if (Math.random() < MUTATION_RATE) {
      const j = Math.floor(Math.random() * child.length);
      [child[i], child[j]] = [child[j], child[i]];
    }
  }
  return child;
}

// 2-opt local search (optional, very effective for TSP)
function twoOpt(tour) {
  let improved = true;
  while (improved) {
    improved = false;
    for (let i = 1; i < tour.length - 1; i++) {
      for (let j = i + 1; j < tour.length; j++) {
        // Reverse segment [i..j] and check if shorter
        const d1 = Math.hypot(cities[tour[i-1]].x - cities[tour[i]].x,
                              cities[tour[i-1]].y - cities[tour[i]].y)
                 + Math.hypot(cities[tour[j]].x   - cities[tour[j%tour.length+1===tour.length?0:j+1]].x,
                              cities[tour[j]].y   - cities[tour[j%tour.length+1===tour.length?0:j+1]].y);
        // (simplified — see full demo for complete 2-opt)
      }
    }
  }
  return tour;
}

Generation Loop + Animation

const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
let generation = 0;

function nextGeneration() {
  const newPop = [];
  // Elitism: keep top 2 individuals
  const sorted = [...population].sort((a, b) => fitness(b) - fitness(a));
  newPop.push([...sorted[0]], [...sorted[1]]);

  while (newPop.length < POP_SIZE) {
    const p1 = tournament(population);
    const p2 = tournament(population);
    const child = mutate(orderedCrossover(p1, p2));
    newPop.push(child);
  }
  population = newPop;

  // Update best
  const fittest = population.reduce((a, b) => fitness(a) > fitness(b) ? a : b);
  if (tourDistance(fittest) < tourDistance(best)) best = [...fittest];

  generation++;
}

function draw() {
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw best tour
  ctx.strokeStyle = '#22c55e';
  ctx.lineWidth = 2;
  ctx.beginPath();
  for (let i = 0; i <= best.length; i++) {
    const c = cities[best[i % best.length]];
    i === 0 ? ctx.moveTo(c.x, c.y) : ctx.lineTo(c.x, c.y);
  }
  ctx.stroke();

  // Draw cities
  ctx.fillStyle = '#f8fafc';
  for (const city of cities) {
    ctx.beginPath();
    ctx.arc(city.x, city.y, 5, 0, Math.PI*2);
    ctx.fill();
  }

  ctx.fillStyle = '#94a3b8';
  ctx.font = '14px monospace';
  ctx.fillText(`Gen: ${generation}  Best: ${tourDistance(best).toFixed(1)}px`, 10, 20);
}

let running = true;
(function loop() {
  if (!running) return;
  nextGeneration();
  draw();
  requestAnimationFrame(loop);
})();

Typical convergence for 24 cities: near-optimal in 200–500 generations with pop=200. Try increasing MUTATION_RATE to 0.05 if it stagnates, or decrease to 0.005 once close to optimal.

Continue Learning

🛠

Experiment in Playground

Try a different GA variant — write and run code directly in the browser.

Open Playground → View Simulation ↗