Branch and bound took us to the optimum of Rutalia's TSP by expanding a thousand nodes instead of a million — but we closed that lesson admitting that the combinatorial explosion was only delayed: with the 60-120 stops of a real van, no exact method finishes in time. This lesson crosses the frontier deliberately: genetic algorithms (GA) give up the guarantee of optimality and, in exchange, produce very good solutions in controlled time, for almost any problem you know how to evaluate. We will cover the full evolutionary scheme — encoding, population, fitness, selection, crossover, mutation, elitism — and implement it end to end in Python on the same 10-point instance from 02-02/02-03, so we can compare against the known optimum of 35.22 km. We'll finish with a hyperparameter guide and a brief look at the "cousin" metaheuristics (simulated annealing, tabu search).
Contents
- Exact vs heuristic: what we sacrifice and what we gain
- The evolutionary metaphor
- Representation: binary chromosomes and permutations
- The fitness function
- Selection: tournament and roulette
- Crossover: one-point and order crossover (OX)
- Mutation, elitism, and stopping criteria
- Complete implementation: a GA for Rutalia's TSP
- Hyperparameters: what to tune and what to expect
- The neighborhood cousins: simulated annealing and tabu search
Exact vs heuristic: what we sacrifice and what we gain
| Exact (02-03) | Metaheuristics (02-04, 02-05) | |
|---|---|---|
| Result | Certified optimum | A good solution, no certificate |
| Time | Unpredictable (exponential in the worst case) | Controlled: you decide how many iterations |
| Scale | TSP: dozens of nodes (hundreds with fine bounds) | Thousands of nodes and "messy" problems |
| Requirements | Exploitable structure (bounds, relaxations) | Only knowing how to evaluate a solution |
| Reproducibility | Deterministic | Stochastic: each run may differ |
The key word is certificate. A GA can hand you the optimal route — in fact, on our small instance it will — but it cannot tell you so: it produces no bounds proving that nothing better exists. For Rutalia the deal usually pays off: between "the optimal route in three weeks" and "a route 2% worse in one minute", operations picks the latter every single day. Professional discipline consists of knowing when you are making that deal and measuring quality against bounds or instances with a known optimum — exactly what we will do here.
The evolutionary metaphor
A GA maintains a population of candidate solutions and evolves it by imitating natural selection:
flowchart LR
A["Initial population<br/>(random)"] --> B["Evaluate the fitness<br/>of each individual"]
B --> C["Select parents<br/>(the best have an edge)"]
C --> D["Crossover:<br/>combine two parents"]
D --> E["Mutation:<br/>random alteration"]
E --> F["New generation<br/>(+ elitism)"]
F --> B
B -->|"stopping criterion"| G["Best individual<br/>found"]
The intuition for why it works: selection concentrates the population in good areas of the solution space (exploitation), crossover combines good fragments from different parents — perhaps one parent solves the north of the city well and the other the south —, and mutation injects variety to avoid getting stuck (exploration). None of this guarantees the optimum; what it produces is a sustained statistical pressure toward better solutions.
Vocabulary we will use: individual/chromosome (an encoded solution), gene (one position of the chromosome), fitness (the individual's quality), generation (one iteration of the loop).
Representation: binary chromosomes and permutations
The first design decision — and the most important — is how to encode a solution:
- Binary encoding: a list of 0/1. Natural for subset problems, like the knapsack from 02-02:
[0, 1, 0, 1, 0]means "load E2 and E4". The classic operators (one-point crossover, bit-by-bit mutation) work directly. - Permutation encoding: an ordering of elements. Natural for sequence problems like the TSP:
[3, 1, 5, ...]is the visiting order of the stops. Careful: the classic binary operators break the solution here — crossing two permutations at one point usually duplicates some stops and omits others, i.e. it produces children that aren't even routes. Permutations demand specialized operators (OX, which we'll see shortly).
| Rutalia problem | Encoding | Example individual | Watch out for |
|---|---|---|---|
| What to load in the van (knapsack) | Binary | [1, 0, 0, 1, 1] |
Infeasible individuals (overweight): penalize or repair |
| Route order (TSP) | Permutation | [4, 1, 6, 9, 3, 7, 5, 2, 8] |
Crossover must preserve permutation-ness |
The fitness function
Fitness translates "quality" into a number that selection can compare. For the TSP, shorter distance = better individual. Two common options: use the route length directly (and select the smallest), or convert to "bigger is better" with fitness = 1 / length. We'll use the former for clarity, with one engineering precaution: fitness evaluation is 90% of the cost of a typical GA (it runs population × generations times), so it must be cheap — and with our matrix D precomputed in 02-02, it is.
For constrained problems (an overweight knapsack), the fitness must additionally decide what to do with infeasible individuals: penalize (subtract a lot of value per kilo of excess) or repair (unload items until it fits). Badly calibrated penalties are a classic source of GAs that "work" while full of unusable solutions.
Selection: tournament and roulette
Selection chooses which individuals reproduce. It must favor the good ones without wiping out diversity (if only the best one reproduces, within three generations the population is clones and the GA degenerates into an expensive greedy).
- Tournament (k participants): pick k individuals at random and the best one wins. Simple, robust, and with adjustable pressure: k = 2 is gentle, k = 7 is aggressive. It's the default choice in practice.
- Roulette: each individual gets a probability proportional to its fitness (a "roulette wheel" with unequal sectors). Elegant in theory, delicate in practice: it requires positive, "bigger = better" fitness, and if one individual dominates heavily, it monopolizes the wheel (premature convergence); if all are similar, selection becomes almost random.
import random
def tournament_selection(population, k=3):
"""Return the best of k randomly chosen individuals (shortest length wins)."""
candidates = random.sample(population, k)
return min(candidates, key=individual_length)
def roulette_selection(population):
"""Probability proportional to the fitness 1/length."""
weights = [1.0 / individual_length(ind) for ind in population]
return random.choices(population, weights=weights, k=1)[0]Crossover: one-point and order crossover (OX)
One-point crossover (binary encoding): cut both parents at the same random position and swap the halves. For the knapsack: [1,0|0,1,1] × [0,1|1,0,0] → children [1,0,1,0,0] and [0,1,0,1,1]. Cheap and effective when contiguous genes form meaningful "blocks".
Order crossover (OX) (permutations): the star operator for routes. It copies a segment from parent 1 verbatim, and fills the gaps with the remaining stops in the order they appear in parent 2. The child inherits a literal sub-route from one parent and the relative ordering of the other — and it is always a valid permutation:
Parent 1: 4 1 | 6 9 3 | 7 5 2 8 chosen segment: positions 2-4
Parent 2: 9 3 5 2 6 8 1 4 7
Child: _ _ | 6 9 3 | _ _ _ _ 1) copy the segment from parent 1
Remaining in parent 2's order (skipping 6, 9, 3): 5 2 8 1 4 7
Child: 5 2 | 6 9 3 | 8 1 4 7 2) fill the gaps in that orderdef ox_crossover(parent1, parent2):
n = len(parent1)
a, b = sorted(random.sample(range(n), 2)) # segment endpoints
child = [None] * n
child[a:b + 1] = parent1[a:b + 1] # 1) literal segment from parent 1
used = set(child[a:b + 1])
remaining = [g for g in parent2 if g not in used] # 2) parent 2's order
gaps = [i for i in range(n) if child[i] is None]
for i, gene in zip(gaps, remaining):
child[i] = gene
return childMutation, elitism, and stopping criteria
- Mutation: with small probability, alter the individual. Binary: flip a bit. Permutations: swap two positions or reverse a segment. Without mutation, the population only recombines its initial genetic material: if no initial permutation puts H at the end, crossover will never invent it; mutation will.
- Elitism: copy the 1-2 best individuals intact into the next generation. It guarantees the best result never gets worse between generations (a monotonicity that stochastic selection alone does not provide). With too much elite, the population homogenizes.
- Stopping criteria: a fixed number of generations (predictable, our default), stagnation (X generations without improvement — the most used in production), a time budget, or reaching a known bound.
Complete implementation: a GA for Rutalia's TSP
All together, on the instance from 02-02 (requires NAMES, D, and route_length from that lesson). The depot (index 0) stays out of the chromosome: it is always the start and the end, so the individual is a permutation of [1..9]:
import random
def individual_length(individual):
"""Individual = permutation of 1..9. The full route prepends the depot."""
return route_length(tuple([0] + individual))
def create_individual():
genes = list(range(1, len(NAMES)))
random.shuffle(genes)
return genes
def swap_mutation(individual, prob=0.2):
if random.random() < prob:
i, j = random.sample(range(len(individual)), 2)
individual[i], individual[j] = individual[j], individual[i]
return individual
def genetic_algorithm(pop_size=100, generations=200,
tournament_k=3, mutation_prob=0.2, elite=2, seed=42):
random.seed(seed) # reproducibility of the run
population = [create_individual() for _ in range(pop_size)]
history = [] # best length per generation
for g in range(generations):
population.sort(key=individual_length) # best first
history.append(individual_length(population[0]))
new_gen = [ind[:] for ind in population[:elite]] # ELITISM: copies, not references
while len(new_gen) < pop_size:
parent1 = tournament_selection(population, tournament_k)
parent2 = tournament_selection(population, tournament_k)
child = ox_crossover(parent1, parent2)
new_gen.append(swap_mutation(child, mutation_prob))
population = new_gen
best = min(population, key=individual_length)
return best, individual_length(best), history
best, km, hist = genetic_algorithm()
print([NAMES[i] for i in [0] + best], round(km, 2))
# ['DEP', 'D', 'A', 'F', 'I', 'C', 'G', 'E', 'B', 'H'] 35.22
print("Generation it was reached:", hist.index(min(hist)))Points that deserve a magnifying glass:
- Total evaluations: 100 individuals × 200 generations = 20,000 route evaluations — the same order of useful work as B&B's ~1,200 nodes multiplied by their bound cost, and far below brute force's 362,880 routes. In our run, the GA reaches the known optimum of 35.22 km (the same route as B&B), usually within the first few dozen generations. That this almost always happens on a 10-node instance is to be expected: the space is small for the method's power. The difference appears at scale: with 100 stops, B&B doesn't finish and the GA keeps delivering good routes with the same code.
- The elite is copied (
ind[:]), not referenced: if mutation touched a shared reference, it would silently corrupt the best individual. Bugs like that are the nightmare of debugging GAs, because the algorithm keeps working, just worse. - The history is your diagnostic instrument: a curve that drops fast and flattens early suggests convergence (premature?); one that keeps stumbling downward until the end is asking for more generations.
- The seed pins the random sequence. In production you run several seeds and report the mean and best case — a single run of a stochastic algorithm is an anecdote, not a measurement.
Hyperparameters: what to tune and what to expect
GAs aren't programmed once: they are tuned. A guide to the effects:
| Hyperparameter | Typical value | If too low | If too high |
|---|---|---|---|
| Population size | 50–200 | Little diversity: premature convergence | High cost per generation without proportional gains |
| Generations | 100–1000 | Stops before converging | Wasted time after stagnation |
| Tournament k | 2–5 | Weak pressure: near-random drift | Brutal pressure: clones within a few generations |
| Mutation prob. | 0.05–0.3 per individual | The population homogenizes and stalls | Search degenerates into a random walk |
| Elite | 1–2 (≈ 1–2%) | The best may be lost between generations | The elite dominates and freezes evolution |
The underlying tension is always the same: exploitation (refining the good: more selection pressure, more elite) versus exploration (seeking the new: more mutation, larger population). There is no universal configuration; there is the habit of measuring with the history and adjusting one parameter at a time.
The neighborhood cousins: simulated annealing and tabu search
GAs are not the only way to search without guarantees. Two families worth knowing by name — they work with a single individual that moves through its neighborhood (solutions one small change away), instead of with a population:
- Simulated annealing: sometimes accepts moves that make things worse, with a probability that decays over time (the "temperature" cools). Early on it explores freely; at the end it only refines. Inspiration: the slow cooling of metals.
- Tabu search: local search that keeps a tabu list of recently made, now-forbidden moves, so as not to undo its steps or cycle around a local optimum.
| Genetic algorithm | Simulated annealing | Tabu search | |
|---|---|---|---|
| State | Population | One individual | One individual + memory |
| Improvement engine | Crossover + selection | Random neighbor + probabilistic acceptance | Best non-tabu neighbor |
| Escapes local optima via… | Population diversity and mutation | Accepting worsening moves (temperature) | Forbidding going back |
| Critical hyperparameter | Selection pressure / mutation | Cooling schedule | Tabu list size |
In practice they compete head-to-head with GAs (and are often hybridized: a GA whose best individual is refined with local search). We won't develop them further: it's enough to know they exist and what sets them apart, because the next lesson presents a metaheuristic of a very different spirit — based on cooperation through shared trails — and we'll close by comparing the module's three approaches on the same TSP.
Common Mistakes and Tips
- Classic crossover on permutations. One-point crossover applied to routes produces children with duplicated and omitted stops — they aren't even solutions. With permutations, use OX (or its relatives PMX/CX). It's the number-one mistake when adapting knapsack code to the TSP.
- Forgetting to copy the elite.
new_gen = population[:2]stores references; a later mutation corrupts the best individuals. Copy withind[:]orlist(ind). - Expensive fitness. If evaluating an individual costs 10 ms, 100 × 200 evaluations is 3.3 minutes of fitness alone. Precompute (our matrix
D), cache, vectorize. The cost analysis of 01-02 applies inside the metaheuristic. - Undiagnosed premature convergence. If by generation 20 all individuals are nearly identical, the rest of the budget is thrown away. Monitor diversity (e.g. distinct lengths in the population) in addition to the best fitness.
- Concluding from a single run. Stochastic means seed 42 might be lucky. Run 10-30 seeds and look at the distribution, not the anecdote.
- Using a GA where a viable exact method exists. For the 5-shipment knapsack or the 10-point TSP, DP and B&B deliver the certified optimum in less time than it takes to tune hyperparameters. Metaheuristics are the frontier tool, not the first option.
- Tip: always keep the global best individual of the run (not just the last generation's) and, if one exists, compare it against a bound or a known optimum. Without a reference, "the GA improved" means nothing.
Exercises
-
OX by hand. With parents
P1 = [2, 5, 1, 4, 3, 6]andP2 = [4, 1, 6, 2, 5, 3]and a segment at positions 1–3 (0-indexed, both inclusive), compute theox_crossoverchild step by step. Verify that it is a valid permutation. -
A GA for the knapsack. Adapt the full scheme to the 0/1 knapsack from 02-02 (capacity 15, weights
[12, 7, 11, 8, 9], values[40, 24, 35, 26, 30]): binary encoding, one-point crossover, bit-flip mutation, and fitness with a penalty for excess weight (value − 10 × excess). Does it reach the €50 optimum? What happens if the penalty is 1 per unit of excess instead of 10? -
Mutation study. Run
genetic_algorithmwithmutation_probin{0.0, 0.05, 0.2, 0.8}and 10 seeds each (leave the other parameters at their defaults). For each value, record how many seeds reach 35.22 km and the mean of the best result. Interpret the extremes.
Solutions
Exercise 1. Segment of P1, positions 1–3: 5, 1, 4 → partial child [_, 5, 1, 4, _, _]. Remaining from P2 in its order, skipping 5, 1, 4: P2 = [4, 1, 6, 2, 5, 3] → 6, 2, 3 remain. Gaps: positions 0, 4, 5 → child = [6, 5, 1, 4, 2, 3]. It contains each gene 1..6 exactly once: a valid permutation. Note the inheritance: the block 5-1-4 comes verbatim from P1; the relative order 6 → 2 → 3 comes from P2.
Exercise 2. Skeleton of the changes:
def create_individual():
return [random.randint(0, 1) for _ in range(5)]
def fitness(ind, penalty=10):
weight = sum(w * x for w, x in zip([12, 7, 11, 8, 9], ind))
value = sum(v * x for v, x in zip([40, 24, 35, 26, 30], ind))
excess = max(0, weight - 15)
return value - penalty * excess
def one_point_crossover(p1, p2):
c = random.randint(1, len(p1) - 1)
return p1[:c] + p2[c:]
def bit_flip_mutation(ind, prob=0.2):
if random.random() < prob:
i = random.randrange(len(ind))
ind[i] = 1 - ind[i]
return indWith penalty 10, excess weight is exorbitantly expensive and the population converges to feasible individuals; the optimum [0,1,0,1,0] (€50) shows up within a few generations — the space has only 2⁵ = 32 points, so it's less a challenge than a test bench. With penalty 1, the GA discovers an unwanted "trick": [1,1,1,1,1] weighs 47 (excess 32) and scores 155 − 32 = 123 > 50 — the best individual is infeasible and the GA enthusiastically optimizes the wrong problem. That's the important lesson of the exercise: the penalty must ensure that no infeasible individual beats a feasible one, or the fitness lies.
Exercise 3. Typical results (yours will vary — that's what the exercise is about): with 0.0, several seeds get stuck above the optimum — without mutation, the population exhausts its initial genetic material and cannot manufacture new orderings; with 0.05 and 0.2, the vast majority of seeds reach 35.22 km, with 0.2 somewhat more robust on this instance; with 0.8, children are destroyed almost as soon as they're born and the mean worsens — the search approaches random sampling with an elite. The quality-vs-mutation curve is an inverted U: the sweet spot is where there's enough variety without destroying inheritance.
Conclusion
We have made the heuristic deal with our eyes open: in exchange for the certificate of optimality that DP and B&B provided, genetic algorithms give us scale and generality — all they ask for is a sensible encoding (binary for subsets, permutations with OX for routes), a fitness that is cheap and honest about constraints, and the eternal balance between selection pressure and mutation. On Rutalia's TSP, our GA of 100 individuals and 200 generations reached the same 35.22 km that B&B certified in 02-03 — unable to certify them, but with code that would keep working just the same with 100 stops, where B&B is no longer in the game. In the next lesson we'll meet a metaheuristic with a different biological inspiration: instead of inheritance and selection, indirect cooperation — ants leaving chemical signals on the good routes. We will implement ant colony optimization on this same instance and close the module with the experimental comparison of the three approaches: exact, evolutionary, and stigmergic.
Advanced Algorithms
Module 1: Introduction to Advanced Algorithms
- Basic Concepts and Notation
- Complexity Analysis
- Recursion and Dynamic Programming
- Advanced Data Structures
Module 2: Optimization Algorithms
- Linear Programming
- Combinatorial Optimization Algorithms
- Backtracking and Branch and Bound
- Genetic Algorithms
- Ant Colony Optimization
Module 3: Graph Algorithms
- Graph Representation
- Graph Search: BFS and DFS
- Shortest Path Algorithms
- Minimum Spanning Trees
- Maximum Flow Algorithms
- Graph Matching Algorithms
Module 4: Search and Sorting Algorithms
Module 5: Machine Learning Algorithms
- Introduction to Machine Learning
- Classification Algorithms
- Regression Algorithms
- Neural Networks and Deep Learning
- Clustering Algorithms
Module 6: Case Studies and Applications
- Optimization in Industry
- Graph Applications in Social Networks
- Search and Sorting on Large Data Volumes
- Machine Learning Applications in Real Life
