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.
- JavaScript arrays and functions
- Canvas 2D — drawing lines and circles
- No prior GA or optimisation knowledge needed
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.
Frequently Asked Questions
What will I learn in this tutorial?
Build a genetic algorithm to approximate the Travelling Salesman Problem: population initialisation, ordered crossover (OX), swap mutation, tournament selection and fitness animation.
What topics are covered in this tutorial?
This tutorial covers: Represent the problem, Fitness function, Initialise random population, Tournament selection, Ordered Crossover (OX), Swap mutation, Generation loop + animation.
What tools and technologies does this tutorial use?
This tutorial uses JavaScript, Canvas 2D, Algorithms.
How long does this tutorial take?
This tutorial takes approximately 50 minutes to complete.
What prerequisites do I need before starting?
Before starting, you should have: JavaScript arrays and functions; Canvas 2D — drawing lines and circles; No prior GA or optimisation knowledge needed.