Back to Classical ML
AI/ML · Classical ML

Genetic Algorithms & Stochastic Search

Keep a crowd of candidate answers and let the better ones have more children. Score every candidate with the fitness function, pick parents with a bias toward the high scorers, cut two parents together to make a child that inherits pieces of each, flip a few bits at random so the crowd keeps trying things nobody had, and repeat. The whole method is one dial: how hard to lean on what already works (selection pressure, crossover) against how hard to keep looking (mutation, population size). Lean too hard and the crowd collapses into near-copies of one decent answer, which is premature convergence. Lean too softly and it is random search with extra steps. The reason to accept that cost is what the fitness function is allowed to be. It can be a simulator, a lab assay, a compiled binary, a human rating, a backtest. It can be discontinuous, noisy, or defined over schedules and subsets and graphs where there is no gradient to take. The price is sample efficiency: a gradient method on a differentiable objective will beat it by orders of magnitude, so the honest question is never "is a genetic algorithm good" but "have I run out of anything cheaper that applies".

Not a learning paradigm at all: no parameters are fitted to data, and there is no training set, no generalization gap and no inference step. It is a search procedure that returns a candidate decision. It is sometimes filed under reinforcement learning because both improve from a scalar signal, but a genetic algorithm has no state, no policy and no credit assignment across time. It sees one number per candidate and nothing else. Listed with the other classical methods because it sits beside them as a way to choose a model configuration, and in its own group because that is a different job from fitting one.

Also known as: Genetic algorithm, Stochastic search, Evolutionary algorithms, Evolution strategies

Intuition

What it actually does

Keep a crowd of candidate answers and let the better ones have more children. Score every candidate with the fitness function, pick parents with a bias toward the high scorers, cut two parents together to make a child that inherits pieces of each, flip a few bits at random so the crowd keeps trying things nobody had, and repeat. The whole method is one dial: how hard to lean on what already works (selection pressure, crossover) against how hard to keep looking (mutation, population size). Lean too hard and the crowd collapses into near-copies of one decent answer, which is premature convergence. Lean too softly and it is random search with extra steps. The reason to accept that cost is what the fitness function is allowed to be. It can be a simulator, a lab assay, a compiled binary, a human rating, a backtest. It can be discontinuous, noisy, or defined over schedules and subsets and graphs where there is no gradient to take. The price is sample efficiency: a gradient method on a differentiable objective will beat it by orders of magnitude, so the honest question is never "is a genetic algorithm good" but "have I run out of anything cheaper that applies".

Assumptions it makes
  • Fitness is cheap enough to afford on the order of N times G evaluations, which is thousands to millions; if each takes minutes, a sample-efficient method such as Bayesian optimization is the better choice
  • Fitness has some locality: genomes that are close under the chosen distance (Hamming for bits, Euclidean for reals) tend to score similarly. Without it the landscape is random and so is the search
  • For crossover to help, good partial solutions must recombine: short groups of genes contribute fitness fairly independently, which is the building-block assumption and is a property of the problem, not of the algorithm
  • Constraints can be expressed through the encoding, a repair step, or the fitness, in a way that does not distort what is being optimized
  • The fitness function is stationary within a run, or its noise is handled explicitly, since elitism and selection assume a score means the same thing in generation 5 as in generation 500
  • A good answer, not a proven-optimal one, is acceptable, because the method returns the best candidate found and no certificate
Objective

Fitness function

Find the candidate with the highest score, when the only access to the score is by trying a candidate. Everything distinctive follows from that sentence. There is no loss surface to descend, so there is no learning rate, no backpropagation and no notion of a derivative; there is only a ranking, and rank-based selection such as a tournament does not even use the magnitude of f, so any monotone rescaling of the fitness leaves the run unchanged. That is also why the objective can be defined on a space with no geometry at all, such as the set of all schedules. The cost of that generality is a hard limit on what can be claimed: the output is the best candidate seen so far and a trace of how the search behaved, never a bound on how far it is from the optimum. If the problem is linear with linear constraints, a solver returns the optimum and a proof of it, and this method returns neither. Constraints are the second thing the formula hides. Here they are folded into f, by a penalty, a repair step, or a feasibility-first comparison, and the way that is done changes what the search converges to far more than the crossover operator does.

the fitness: a black box returning one number per candidate. It may be noisy, discontinuous, expensive, or the output of a simulator
the search space, which is the representation choice: bit strings, real vectors, or permutations (S_n) for ordering problems
genome length for bit strings, dimension for real vectors, and number of items for permutations
the best candidate found. Not a certified optimum: nothing in the procedure proves that no better one exists
the gradient, which is exactly what this method does not need and exactly what you should use if it exists
Optimization

How it is minimized

Population-based stochastic search: select parents by fitness, recombine, mutate, replace, and carry the best few forward unchanged (elitism)

the population at generation t: N candidate genomes
pick parents with a bias toward high fitness. Tournament selection draws s candidates at random and keeps the best; roulette selection picks in proportion to fitness
with probability p_c, splice two parents at a random cut point (one-point crossover); otherwise copy a parent
flip each bit independently with probability p_m, conventionally 1/L, so that about one bit changes per child
the k best candidates copied to the next generation unmodified, so the best score found can never go down
a schema, meaning a pattern of fixed positions with wildcards elsewhere, and the number of population members matching it at generation t
the schema average fitness relative to the population average, which is the selection pressure acting on that pattern
the defining length (distance between outermost fixed positions) and order (number of fixed positions): the two quantities that decide how likely crossover and mutation are to break the pattern

The second line is the schema theorem, read informally: a pattern of bits that is short, involves few fixed positions, and scores above the population average receives an exponentially growing share of the trials, because selection amplifies it by the fitness ratio while crossover and mutation only occasionally cut through it. That is the argument for why recombination can do something random restarts cannot, namely assemble good partial solutions found in different individuals into one. It is also much weaker than its reputation. It is an inequality about the next generation only, it says nothing about how new schemata are created, and the building-block hypothesis built on it, that real problems decompose into short independent blocks, is an assumption about the problem rather than a result about the algorithm. When the blocks are not independent (strong epistasis) crossover mostly destroys what selection built, and a mutation-only method such as a (1+1) evolution strategy or simulated annealing often wins. The practical reading is then a design rule: choose a genome encoding in which nearby bits interact and distant bits do not, and expect crossover to pay off only to that degree. Selection pressure and mutation rate are the exploration dial. Tournament size s sets how fast a good candidate takes over the population, roughly ln(N)/ln(s) generations. A mutation rate near 1/L per bit keeps about one change per child, which is the standard balance between local refinement and escape; a rate much above that is a random walk, and one well below it leaves the population unable to leave the basin it is in. Similarity belongs in this design too. Hamming distance, the count of differing bits, is the natural metric on bit-string genomes and it is what diversity-preserving variants use: fitness sharing divides each candidate fitness by the number of neighbours inside a niche radius, and crowding makes a child replace the most similar parent instead of the worst member. Swapping the metric breaks that silently. Hamming on a plain binary encoding of an integer has cliffs (7 and 8 differ in four bits though they are adjacent), so Gray-code the integer or the niches mean nothing. Euclidean distance is the right one for real-valued genomes, but only after each gene is scaled to a comparable range, or one wide-range gene decides every niche. And neither is meaningful for permutations, where a position-based or ordering distance such as Kendall tau is the matching choice.

Hyperparameters
  • population size N

    Parallel hypotheses held at once. More diversity and slower convergence per generation; scales with how rugged the landscape is more than with genome length (50 to 1,000)

  • generations G

    Search budget. Total evaluations are about N times G, and that product is the real cost knob (100 to 10,000, or stop on stagnation)

  • tournament size s

    Selection pressure. s of 2 is gentle; large s takes over the population quickly and invites premature convergence (2 to 7)

  • crossover rate p_c

    How often two parents are spliced rather than one being copied. Worth lowering when the encoding has strong interactions between distant genes (0.6 to 0.95)

  • mutation rate p_m

    Per-gene change probability, the main source of new material. 1/L means about one flip per child; for real-valued genomes the equivalent is the step size sigma, which strong methods adapt online (0.5/L to 3/L per bit)

  • elite count k

    Candidates carried over unchanged, which guarantees monotone best-so-far. Too many reduce the population to its elite (1 to 5 percent of N)

  • niche radius sigma_share

    Hamming (or scaled Euclidean) distance inside which candidates share fitness, in the diversity-preserving variant. Too small does nothing; too large merges real optima into one niche (a few percent of L, tuned on diversity curves)

  • random seed and restarts

    The run is a random variable. Seed it for reproducibility, and judge the method by its distribution over many seeds, never by one run (20 or more seeds per configuration)

Convergence & cost

There is no finite-time guarantee, and being precise about what is guaranteed matters. With elitism and a mutation operator that can reach any genome, the best score converges to the global optimum in probability as generations grow without bound, but random search satisfies the same statement, so it distinguishes nothing. What is known is mostly problem-specific runtime analysis: a (1+1) evolutionary algorithm solves OneMax in expected time of order L log L evaluations, and that sort of result does not extend to general landscapes. The No Free Lunch theorems apply in full: averaged over all possible fitness functions no search strategy beats another, so a genetic algorithm helps exactly to the degree that nearby candidates have correlated fitness and good partial solutions recombine, and a problem with neither is random search. The failures are specific and observable. Premature convergence is the diversity collapsing: the population fills with near-copies, mean pairwise Hamming distance falls toward zero, and further generations only polish one basin. A deceptive landscape, where the short patterns that look good lead away from the optimum, defeats the schema argument by construction. Epistasis makes crossover destructive. Noisy fitness lets a lucky evaluation of a mediocre candidate win a tournament and, once elitism has frozen it, stick forever unless elites are re-evaluated. And a penalty weight set too low converges to an infeasible optimum while one set too high produces a population that never crosses the feasibility boundary where the good solutions live.

O(G * N * (L + C_f)) time, where C_f is the cost of one fitness evaluation, and in practice C_f dominates everything: for a simulator or a model-training run the operators are free and the whole cost is evaluations. Memory is O(N * L) for the population, plus the cached scores. The embarrassingly parallel structure is the main practical lever, since the N evaluations in a generation are independent, but the generations themselves are strictly sequential. Cost scales with how many evaluations you can afford rather than with problem size, which is a real advantage when f is cheap and a real disqualifier when each evaluation takes minutes.

Applications

How, where, and why it is used

Time-Series Forecasting

Adapted
How

It does not forecast. It searches the space of forecasting pipelines. Encode a configuration as a genome: which lags enter (a bit mask), the differencing order, the seasonal period, the regularization strength, the tree depth, the ensemble weights. Define fitness as the negative error of a rolling-origin backtest of that configuration, and let the population search the mixed discrete-and-continuous space. The forecast itself is still made by the model the search selects.

Where
  • Lag and feature subset selection for a gradient-boosted or linear forecaster, where the mask is a bit string and the objective is a backtest score
  • Hyperparameter search for a forecasting pipeline whose space mixes categorical choices (model family, seasonality handling) with continuous ones
  • Choosing ensemble weights or a combination of base forecasters under a non-differentiable metric such as pinball loss at several quantiles or a business cost
  • Calibrating the parameters of a simulation-based demand model where the likelihood is intractable and only simulated-versus-observed error is available
Why

A reasonable choice when the configuration space is mixed and structured and the metric is not differentiable, and an honest second choice when it is not. Each fitness evaluation here is a full backtest, which is expensive, and for a few continuous hyperparameters Bayesian optimization or even plain random search reaches a comparable result in far fewer evaluations. The genetic algorithm earns its place on the combinatorial part: subset selection over dozens of lags and exogenous features, where the space is too large to enumerate and recombining two good masks is a sensible move. Treat it as a way to pick the configuration, never as part of the forecaster, and be wary of how it can overfit the selection itself.

Featurization
  • Encode the configuration so that nearby genomes are similar configurations: a bit mask for lags, a Gray-coded or real-valued gene for ordered hyperparameters
  • Make fitness a rolling-origin backtest error scaled against a seasonal-naive baseline (MASE), so scores are comparable across series
  • Cache the score of every distinct genome, since the same mask recurs and a backtest is the expensive step
  • Hold out a final period that the search never scores, to measure the selection effect honestly
Evaluation

Measure the selected configuration on a final held-out window that the search never saw, against a well-tuned random-search baseline given the same number of backtests. The comparison that matters is score at equal evaluation budget; a genetic algorithm that merely matches random search at the same cost has not justified its machinery.

Pitfalls
  • Selection overfits the backtest: the winning configuration is the luckiest of thousands scored on the same folds, so its in-search score is optimistically biased (the same winner curse as stepwise selection)
  • Look-ahead leakage inside the fitness function silently rewards configurations that peek at the future, and the search is very good at finding exactly that
  • Stochastic model training makes fitness noisy, so a lucky run of a mediocre configuration is promoted and frozen by elitism unless elites are re-scored
  • Running a backtest per candidate per generation without caching, which spends most of the budget re-evaluating genomes already seen

Anomaly Detection

Not applicable

It searches for a decision that maximizes a score and defines no notion of normal behaviour or of an unusual observation, so there is nothing to detect with; it can tune a detector hyperparameters, but that is optimization applied to a detector rather than detection.

Optimization

Primary fit
How

Treat the problem as a black box f over a candidate representation and search it with a population. Choose a genome that makes feasible solutions easy to represent (a bit string for subsets, a permutation for orderings, a real vector for continuous parameters), a fitness that folds constraints in by penalty, repair or feasibility-first comparison, and variation operators that respect the structure (order crossover for permutations, not one-point). The output is the best candidate found plus the convergence and diversity traces that say whether to trust it.

Where
  • Hyperparameter and neural architecture search, where each evaluation is a training run and the space mixes discrete structure with continuous settings
  • Scheduling and timetabling (shifts, exams, job-shop sequences), where constraints are numerous, messy and awkward to write as a linear program
  • Engineering and design optimization against a simulator, such as antenna shapes, airfoils or layout, where f is a physics solver and no gradient exists
  • Feature and subset selection, where the genome is a mask and fitness is a cross-validated score
  • Routing and packing heuristics (travelling salesman, bin packing) at sizes where exact solvers stall, often as a metaheuristic wrapped around local search
Why

The right tool when the objective cannot be differentiated, cannot be written down in closed form, and is too irregular for a convex or mixed-integer formulation, and the wrong tool in almost every other case. The order of preference matters. If f is differentiable, use gradients, which are orders of magnitude more sample-efficient. If it is linear with linear constraints, use linear or mixed-integer programming, which is exact and returns an optimality certificate that this method cannot give. If it is continuous, smooth enough and low-dimensional, CMA-ES adapts its own step size and covariance and usually beats a hand-built genetic algorithm. If each evaluation is expensive and the dimension is modest, Bayesian optimization spends evaluations far more carefully. Simulated annealing and random search are the honest siblings in stochastic search: they have no population and no crossover, they are simpler, and on a surprising number of problems they match a genetic algorithm. What a genetic algorithm uniquely offers is recombination, so it deserves the job only when good partial solutions genuinely combine, and when a population of diverse answers (several different good designs, not one) is itself useful.

Featurization
  • Choose the encoding so that distance between genomes tracks distance between solutions; Hamming distance on bits, scaled Euclidean on reals, a position or order distance on permutations
  • Gray-code integers that are stored as bits, or Hamming-1 neighbours will not be adjacent in value
  • Handle constraints deliberately: a repair step that always returns a feasible solution, or a feasibility-first comparison rule, is usually sturdier than a hand-tuned penalty weight
  • Normalise the fitness scale if using roulette selection, since proportional selection is sensitive to offsets that tournament selection ignores
  • Cache evaluations by genome hash; duplicates are common once the population starts to converge
Evaluation

Report the distribution of best-so-far fitness against number of fitness evaluations across at least twenty seeds, with a median and an interquartile band, and compare it with random search and simulated annealing at the same evaluation budget. A single lucky run proves nothing. Track mean pairwise Hamming distance each generation to diagnose premature convergence directly, and when an exact method is applicable, measure the optimality gap against its certified bound.

Pitfalls
  • Using it where a gradient, a linear program or a convex solver applies, which wastes orders of magnitude in evaluations and gives up the optimality proof
  • Reporting the best of many runs as though it were the expected result, which hides the variance that is the main property of the method
  • Premature convergence mistaken for convergence: the best score stops rising because the population stopped exploring, not because the optimum was reached
  • A penalty weight tuned until the demo works, which then fails on the next instance; this is the usual source of an infeasible final answer
  • Applying one-point crossover to a permutation, which produces duplicate and missing items and needs a repair step the code did not have
Also applied in

Control & Operations

Viable
How

Scheduling, routing, rostering and packing problems are search over discrete arrangements with many awkward rules. Encode an arrangement as a genome (a permutation of jobs, an assignment vector), define fitness as total cost with penalties for violated rules, and let the population search. In practice it is most often the outer loop of a hybrid, with a fast local-search or constructive heuristic polishing each child (a memetic algorithm).

Where
  • Job-shop and flow-shop sequencing, where a permutation encodes the order and a decoder builds the schedule
  • Staff rostering and exam timetabling with a long list of soft preferences and hard rules
  • Vehicle routing and delivery-sequence heuristics at sizes beyond exact solvers
  • Layout and bin-packing heuristics where the evaluation is a placement simulator
Why

Viable rather than primary, and the reason is the competition. Where the problem has linear structure, a mixed-integer solver with a modern branch-and-cut engine returns a provably optimal or bounded answer and routinely handles instances that practitioners assume need a heuristic. Constraint programming is the other strong alternative for rule-heavy scheduling. A genetic algorithm earns its keep when the rules do not fit those formalisms (a simulator evaluates the schedule), when a good-enough answer within a time budget is acceptable, or when a warm start from the current schedule matters, since a population can be seeded with the incumbent plus perturbations. It is the pragmatic choice, not the principled one, and the optimality gap it leaves is unknown unless you also solve a relaxation.

Featurization
  • Use a representation plus a decoder that always yields a feasible schedule, so the search never wastes evaluations on infeasible ones
  • Pair permutation genomes with order-preserving crossover (order crossover, partially mapped crossover) rather than one-point cuts
  • Seed the initial population with the incumbent schedule and perturbations of it, plus a few random immigrants to keep diversity
  • Separate hard constraints (repair or reject) from soft preferences (weighted cost)
Evaluation

Compare cost against the incumbent heuristic and, where tractable, against the bound from a mixed-integer relaxation, so that the gap is a number rather than a feeling. Re-run on perturbed instances to measure robustness, and report time-to-target alongside final cost, since the time budget is usually the binding constraint.

Pitfalls
  • Treating the best schedule found as optimal when no bound was ever computed
  • A decoder that is slow, which makes it the whole cost; the search is only as fast as one fitness evaluation
  • Over-tuned penalty weights that encode this week data and break when demand changes
  • Solving a problem a mixed-integer solver would have settled exactly, in a fraction of the engineering time
Deployment

What it costs to run

Training

There is no training, and the cost is entirely evaluations: roughly population size times generations times the cost of one fitness evaluation. When fitness is a few arithmetic operations (a knapsack score) a search of a million evaluations is seconds. When it is a model-training run or a simulator, the same count is days and the operators are free by comparison, so the parallelism of evaluating a generation at once, across cores or machines, is the main lever. Illustrative cost profile, not a measured benchmark.

Inference

The output is a solution, not a model, so there is no inference step, and serving the result is free. The recurring cost is re-running the search whenever the instance changes. For repeated decisions (daily rosters, hourly schedules), warm-starting from the previous solution and bounding the run by a wall-clock budget rather than a generation count is what makes it deployable.

Retraining

Re-run whenever the problem instance changes: new orders, new constraints, new costs. This is an event, not a drift. Seed the new population with the previous best and perturbations of it, plus a fraction of random immigrants, so the search starts from a good answer without being trapped in the old basin.

Drift & monitoring
  • Mean pairwise Hamming distance (or scaled Euclidean distance for real genomes) per generation, since a collapse toward zero is premature convergence caught early
  • Best-versus-mean fitness gap per generation, which measures remaining selection pressure
  • Best-so-far fitness against evaluation count across seeds, summarised as median and interquartile band, to detect a regression in search quality after any change
  • Fraction of feasible candidates in the population, which exposes a penalty weight drifting away from the constraint boundary
  • Evaluation cache hit rate, a free indicator of how much of the budget is spent on genomes already seen
Production gotchas
  • Seed every run and thread the generator explicitly; with a global random source or a thread-dependent stream, the same input produces a different answer and no regression can be diagnosed
  • In a parallel run, derive each child random stream from (seed, generation, child index) instead of sharing one generator, or the result depends on thread scheduling
  • Noisy fitness plus elitism freezes a lucky evaluation: re-evaluate elites each generation, or average several evaluations, when the function is stochastic
  • Quote the distribution over seeds, not the best run, or the headline number will not reproduce
  • Penalty-based constraint handling is a hidden hyperparameter that interacts with the instance scale, so prefer repair or feasibility-first comparison where possible
  • Evaluate in batches and cache by genome hash; duplicates become the majority of the population near convergence
  • A wall-clock budget needs a stopping rule that tolerates an unfinished generation, and the result must still be a valid, feasible candidate
Trade-offs

Pros and cons, in context

Strengths
  • Needs only the ability to score a candidate: no gradient, no convexity, no closed form

    The decisive advantage when f is a simulator, a physical experiment, or defined on schedules, subsets and graphs. Worthless when f is differentiable, since gradient methods then get the same answer in far fewer evaluations.

  • Handles discrete, combinatorial and mixed search spaces natively

    Feature masks, orderings and architecture choices are the genome itself, with no relaxation step. The catch is that the encoding and operators carry the real intelligence, and a poor encoding makes it slower than random search.

  • Returns a population of diverse good answers, not one

    Useful when several distinct designs are wanted (trade-offs along a Pareto front with a multi-objective variant), or when the best answer may be infeasible for reasons the fitness did not capture. Irrelevant when only the single best value counts.

  • Embarrassingly parallel across the population within a generation

    Evaluations are independent, so throughput scales with cores or machines when fitness is expensive. It does nothing for the sequential dependence between generations, so wall-clock speedup caps at the population size.

  • Tolerates noise, discontinuity and plateaus that break local methods

    Rank-based selection ignores the magnitude of f, so a few outliers do not dominate. Noise still needs explicit handling, because elitism will freeze a lucky evaluation.

Limitations
  • Sample-inefficient: spends thousands of evaluations to learn what a gradient reads off in one

    Dominant cost when each evaluation is a training run. If f is differentiable, gradient methods are orders of magnitude cheaper; if evaluations are expensive and the dimension is modest, Bayesian optimization is the better deal.

  • No optimality guarantee and no bound on how far the answer is from the best

    A mixed-integer or linear solver returns a certificate; this returns a candidate and a convergence plot. Acceptable for engineering heuristics, unacceptable where a proof of optimality or a worst-case gap is required.

  • Premature convergence: the population collapses into near-copies of one decent answer

    Caused by selection pressure outrunning mutation. Visible as mean Hamming distance falling toward zero while best fitness plateaus. Mitigated with fitness sharing, crowding, a lower tournament size, or restarts, and each mitigation costs speed.

  • Many interacting hyperparameters, and results depend on them and on the random seed

    Population size, mutation rate, selection pressure and the penalty weight interact, and a single run is a random sample. Judge it by a distribution over seeds; a one-off demo that works is the usual source of disappointment.

  • Often matched by simpler stochastic search

    Random search is a strong baseline for hyperparameters, and simulated annealing or a (1+1) evolution strategy needs no crossover and often ties on rugged landscapes. The population machinery earns its cost only when recombination of building blocks actually helps.

Implementation

Intuitive, idiomatic, optimized

The same algorithm in three languages, each progressing through Kent Beck's three stages. Every optimization names the lever it pulls and the trade-off it accepts — cost profiles are illustrative, not measured benchmarks.

1. Intuitive

The math, transcribed literally. No dependencies.

"""A genetic algorithm on 0/1 knapsack - every operator written out.

Population, fitness, tournament selection, one-point crossover, bit-flip
mutation, elitism. The only thing the search ever asks of the problem is
fitness(genome) -> number: no gradient, no structure, just a ranking.
"""

import random


def fitness(genome, values, weights, capacity):
    total_value = 0
    total_weight = 0
    for j in range(len(genome)):
        if genome[j] == 1:
            total_value += values[j]
            total_weight += weights[j]

    if total_weight > capacity:
        # infeasible: rank by how far over the limit, so any feasible genome wins
        return -(total_weight - capacity)
    return total_value


def hamming(a, b):
    return sum(1 for j in range(len(a)) if a[j] != b[j])


def diversity(population):
    """Mean pairwise Hamming distance: it collapsing is premature convergence."""
    total = 0
    pairs = 0
    for i in range(len(population)):
        for k in range(i + 1, len(population)):
            total += hamming(population[i], population[k])
            pairs += 1
    return total / pairs


def tournament(population, scores, size):
    best = random.randrange(len(population))
    for _ in range(size - 1):
        challenger = random.randrange(len(population))
        if scores[challenger] > scores[best]:
            best = challenger
    return population[best]


def evolve(values, weights, capacity, pop_size=60, generations=200,
           tournament_size=3, crossover_rate=0.9, elite=2):
    length = len(values)
    mutation_rate = 1.0 / length          # about one flipped bit per child
    population = [[random.randint(0, 1) for _ in range(length)]
                  for _ in range(pop_size)]
    history = []

    for generation in range(generations):
        scores = [fitness(g, values, weights, capacity) for g in population]
        history.append((max(scores), diversity(population)))

        # elitism: the best genomes pass to the next generation unchanged
        ranked = sorted(range(pop_size), key=lambda i: scores[i], reverse=True)
        next_population = [population[i][:] for i in ranked[:elite]]

        while len(next_population) < pop_size:
            mother = tournament(population, scores, tournament_size)
            father = tournament(population, scores, tournament_size)

            if random.random() < crossover_rate:
                cut = random.randint(1, length - 1)      # one-point crossover
                child = mother[:cut] + father[cut:]
            else:
                child = mother[:]

            for j in range(length):
                if random.random() < mutation_rate:
                    child[j] = 1 - child[j]              # bit flip
            next_population.append(child)

        population = next_population

    scores = [fitness(g, values, weights, capacity) for g in population]
    best = max(range(pop_size), key=lambda i: scores[i])
    return population[best], scores[best], history

O(G * N * (L + tournament)) in pure Python, plus an O(N^2 * L) diversity measure every generation that dominates for large N.

2. IdiomaticNumPy

How this language actually wants it written.

"""Genetic algorithm - typed, seeded, validated, bit-identical on rerun."""

from dataclasses import dataclass
from typing import Callable

import numpy as np
from numpy.typing import NDArray

Genome = NDArray[np.bool_]           # shape (L,)
Population = NDArray[np.bool_]       # shape (N, L)
Fitness = Callable[[Genome], float]  # the only thing the problem supplies


@dataclass(frozen=True)
class GAConfig:
    pop_size: int = 100
    generations: int = 200
    tournament_size: int = 3
    crossover_rate: float = 0.9
    mutation_rate: float | None = None   # None means 1/L
    elite: int = 2
    seed: int = 0

    def __post_init__(self) -> None:
        if self.pop_size < 2:
            raise ValueError(f"pop_size must be at least 2, got {self.pop_size}")
        if not 0 <= self.elite < self.pop_size:
            raise ValueError(f"elite must be in [0, pop_size), got {self.elite}")
        if self.tournament_size < 1:
            raise ValueError("tournament_size must be at least 1")
        for name in ("crossover_rate", "mutation_rate"):
            rate = getattr(self, name)
            if rate is not None and not 0.0 <= rate <= 1.0:
                raise ValueError(f"{name} must be in [0, 1], got {rate}")


@dataclass(frozen=True)
class GAResult:
    best_genome: Genome
    best_fitness: float
    best_history: NDArray[np.float64]
    diversity_history: NDArray[np.float64]
    evaluations: int


def mean_pairwise_hamming(population: Population) -> float:
    """Mean Hamming distance over all pairs in O(N*L), not O(N^2 * L).

    Per locus, k ones and (N - k) zeros contribute k * (N - k) differing pairs.
    """
    n = population.shape[0]
    ones = population.sum(axis=0, dtype=np.int64)
    return float((ones * (n - ones)).sum()) / (n * (n - 1) / 2)


def _tournament(scores: NDArray[np.float64], size: int, count: int,
                rng: np.random.Generator) -> NDArray[np.intp]:
    contenders = rng.integers(0, scores.size, size=(count, size))
    best_slot = scores[contenders].argmax(axis=1)
    return contenders[np.arange(count), best_slot]


def _crossover(mothers: Population, fathers: Population, rate: float,
               rng: np.random.Generator) -> Population:
    count, length = mothers.shape
    cut = rng.integers(1, length, size=(count, 1))
    children = np.where(np.arange(length) < cut, mothers, fathers)
    keep_mother = rng.random(count) >= rate
    children[keep_mother] = mothers[keep_mother]
    return children


def evolve(fitness: Fitness, genome_length: int, config: GAConfig) -> GAResult:
    """Maximise fitness over bit strings. Same config and seed, same answer."""
    if genome_length < 2:
        raise ValueError(f"genome_length must be at least 2, got {genome_length}")

    rng = np.random.default_rng(config.seed)   # explicit stream, never global
    rate = config.mutation_rate if config.mutation_rate is not None else 1.0 / genome_length
    n_children = config.pop_size - config.elite

    population: Population = rng.random((config.pop_size, genome_length)) < 0.5
    scores = np.array([fitness(genome) for genome in population], dtype=np.float64)
    evaluations = config.pop_size
    best_history = np.empty(config.generations)
    diversity_history = np.empty(config.generations)

    for generation in range(config.generations):
        best_history[generation] = scores.max()
        diversity_history[generation] = mean_pairwise_hamming(population)

        elite_rows = np.argsort(scores)[::-1][: config.elite]
        mothers = population[_tournament(scores, config.tournament_size, n_children, rng)]
        fathers = population[_tournament(scores, config.tournament_size, n_children, rng)]
        children = _crossover(mothers, fathers, config.crossover_rate, rng)
        children ^= rng.random(children.shape) < rate           # bit-flip mutation

        # Elite scores are carried over rather than recomputed. That is only
        # sound for a deterministic fitness; re-score elites if it is noisy.
        child_scores = np.array([fitness(genome) for genome in children])
        evaluations += n_children
        population = np.concatenate([population[elite_rows], children])
        scores = np.concatenate([scores[elite_rows], child_scores])

    best = int(scores.argmax())
    return GAResult(population[best].copy(), float(scores[best]),
                    best_history, diversity_history, evaluations)

What changed: Four changes, none of them about the operators. Randomness comes from one explicit numpy Generator seeded from the config, instead of the global random module, so a run is reproducible and two runs cannot interfere. Configuration is a frozen, validated dataclass, so a bad mutation rate fails at construction instead of producing a quietly wrong search. The per-child loops become array operations: tournament, crossover and mutation each act on the whole generation at once. And diversity is measured in O(N*L) from per-locus bit counts rather than O(N^2*L) from all pairs, which makes it cheap enough to record every generation.

Explicit type hints on every public signatureNo mutable default argumentsDataclasses or NamedTuples over ad-hoc dictsRaise specific exceptions, never bare exceptGuard clauses over nested conditionals

O(G * N * L) for the operators, vectorised; fitness is still called once per child from Python.

3. OptimizedNumPy / BLAS

Faster, and the justification for each change.

"""Genetic algorithm - whole-generation array ops, preallocated, one pass per score."""

import numpy as np
from numpy.typing import NDArray

Matrix = NDArray[np.float32]


def knapsack_scores(population: Matrix, value_weight: Matrix, capacity: float) -> NDArray[np.float32]:
    """Score every genome in one matrix product.

    value_weight is (L, 2): column 0 values, column 1 weights. One gemm reads
    the population once and yields both totals for every genome.
    """
    totals = population @ value_weight
    overweight = np.maximum(totals[:, 1] - capacity, 0.0)
    return np.where(overweight > 0.0, -overweight, totals[:, 0])


def evolve_fast(values: Matrix, weights: Matrix, capacity: float, *, pop_size: int,
                generations: int, tournament_size: int, crossover_rate: float,
                elite: int, seed: int) -> tuple[NDArray[np.float32], float]:
    rng = np.random.default_rng(seed)
    length = values.size
    n_children = pop_size - elite
    rate = 1.0 / length
    value_weight = np.ascontiguousarray(np.stack([values, weights], axis=1), dtype=np.float32)
    columns = np.arange(length)[None, :]

    # float32 0/1 genomes: the matmul needs no cast copy on every generation.
    current = (rng.random((pop_size, length), dtype=np.float32) < 0.5).astype(np.float32)
    following = np.empty_like(current)
    scores = knapsack_scores(current, value_weight, capacity)

    for _ in range(generations):
        top = np.argpartition(scores, -elite)[-elite:]            # O(N), not a full sort
        elite_scores = scores[top].copy()
        following[:elite] = current[top]

        # one RNG call draws every contender for both parents of every child
        contenders = rng.integers(0, pop_size, size=(2, n_children, tournament_size))
        winners = np.take_along_axis(
            contenders, scores[contenders].argmax(axis=2)[..., None], axis=2
        )[..., 0]

        children = following[elite:]                              # a view, not a copy
        children[...] = current[winners[1]]                       # father everywhere
        cut = rng.integers(1, length, size=(n_children, 1))
        np.copyto(children, current[winners[0]], where=columns < cut)           # mother's prefix
        np.copyto(children, current[winners[0]],
                  where=(rng.random(n_children) >= crossover_rate)[:, None])    # no crossover
        flips = rng.random(children.shape, dtype=np.float32) < rate
        np.subtract(1.0, children, out=children, where=flips)     # in-place bit flip

        scores[elite:] = knapsack_scores(children, value_weight, capacity)
        scores[:elite] = elite_scores
        current, following = following, current                   # swap buffers, no allocation

    best = int(scores.argmax())
    return current[best].astype(np.uint8), float(scores[best])

What changed: The fitness function stops being a per-genome Python callable and becomes one matrix product over the whole population, so scoring a generation costs a single BLAS call. That is the largest change and also the main limit: it only applies when fitness can be written as an array expression. The rest removes allocation and interpreter overhead. Both populations are allocated once and swapped, crossover and mutation write into the next generation in place, and the elite are picked with a partial selection instead of a sort. Where fitness is a black-box simulator that cannot be batched this way, the equivalent move is to evaluate the generation with a process pool, because the evaluations are independent.

  • Vectorize to NumPy/BLAS (@, np.dot, einsum)

    Stacking values and weights into an (L, 2) matrix turns scoring the whole population into one gemm that reads the genomes once and returns both totals for every candidate.

    Trade-off: Only works when fitness is expressible as an array operation. A simulator, a model-training run or any branching logic cannot be batched this way, and forcing it to is a rewrite of the problem, not an optimization.

  • Pre-allocate output arrays and use in-place operations

    The current and next populations are allocated once and swapped each generation, and crossover and mutation write into the next buffer with copyto and subtract(out=...), so no population-sized array is created in the loop.

    Trade-off: In-place writes into views make aliasing bugs easy (a write into the next buffer that silently reads the current one), and the code is harder to read and to test than the functional version.

  • Batch work to amortize interpreter overhead

    One integers() call draws every tournament contender for both parents of every child, and one random() call decides every mutation, so the interpreter is entered a fixed number of times per generation instead of once per child.

    Trade-off: Large temporaries (n_children by tournament_size by 2 indices, and an N by L random matrix) raise peak memory, which can matter for a long genome and a large population.

  • Ensure contiguous memory layout and a single dtype

    Genomes are stored as float32 zeros and ones in one C-contiguous array, so the matmul needs no per-generation cast copy of a bool array.

    Trade-off: Uses 4 bytes per gene where a bit would do (a 32-fold memory cost over bit packing), and float32 sums are exact only for integer totals below 2^24, so real-valued fitness can lose precision.

O(G * N * L) per run with one BLAS call per generation for fitness. Illustrative, not a measured benchmark.