Branch and bound nos llevó al óptimo del TSP de Rutalia expandiendo mil nodos en lugar de un millón — pero cerramos la lección admitiendo que la explosión combinatoria solo estaba retrasada: con las 60-120 paradas de una furgoneta real, ningún método exacto termina a tiempo. Esta lección cruza la frontera de forma deliberada: los algoritmos genéticos (AG) renuncian a la garantía de optimalidad y, a cambio, producen soluciones muy buenas en tiempo controlado, para casi cualquier problema que sepas evaluar. Veremos el esquema evolutivo completo — codificación, población, fitness, selección, cruce, mutación, elitismo — y lo implementaremos entero en Python sobre la misma instancia de 10 puntos de 02-02/02-03, para poder comparar contra el óptimo conocido de 35,22 km. Terminaremos con una guía de hiperparámetros y un vistazo breve a las metaheurísticas "primas" (recocido simulado, búsqueda tabú).

Contenido

  1. Exactos vs heurísticos: qué sacrificamos y qué ganamos
  2. La metáfora evolutiva
  3. Representación: cromosomas binarios y permutaciones
  4. Función de fitness
  5. Selección: torneo y ruleta
  6. Cruce: un punto y order crossover (OX)
  7. Mutación, elitismo y criterios de parada
  8. Implementación completa: AG para el TSP de Rutalia
  9. Hiperparámetros: qué tocar y qué esperar
  10. Las primas de vecindad: recocido simulado y búsqueda tabú

Exactos vs heurísticos: qué sacrificamos y qué ganamos

Exactos (02-03) Metaheurísticas (02-04, 02-05)
Resultado Óptimo certificado Solución buena, sin certificado
Tiempo Impredecible (exponencial en el peor caso) Controlado: tú decides cuántas iteraciones
Escala TSP: decenas de nodos (cientos con cotas finas) Miles de nodos y problemas "sucios"
Requisitos Estructura explotable (cotas, relajaciones) Solo saber evaluar una solución
Reproducibilidad Determinista Estocástica: cada ejecución puede diferir

La palabra clave es certificado. Un AG puede darte la ruta óptima — de hecho, en nuestra instancia pequeña lo hará — pero no puede decírtelo: no produce cotas que demuestren que no existe nada mejor. Para Rutalia el trato suele compensar: entre "la ruta óptima dentro de tres semanas" y "una ruta un 2 % peor dentro de un minuto", operaciones elige lo segundo todos los días. La disciplina profesional consiste en saber cuándo estás haciendo ese trato y en medir la calidad contra cotas o instancias con óptimo conocido — exactamente lo que haremos aquí.

La metáfora evolutiva

Un AG mantiene una población de soluciones candidatas y la hace evolucionar imitando la selección natural:

flowchart LR
    A["Población inicial<br/>(aleatoria)"] --> B["Evaluar fitness<br/>de cada individuo"]
    B --> C["Seleccionar padres<br/>(los mejores tienen ventaja)"]
    C --> D["Cruce:<br/>combinar dos padres"]
    D --> E["Mutación:<br/>alterar al azar"]
    E --> F["Nueva generación<br/>(+ elitismo)"]
    F --> B
    B -->|"criterio de parada"| G["Mejor individuo<br/>encontrado"]

La intuición de por qué funciona: la selección concentra a la población en zonas buenas del espacio de soluciones (explotación), el cruce combina fragmentos buenos de padres distintos — quizá una madre resuelve bien el norte de la ciudad y un padre el sur —, y la mutación inyecta variedad para no quedarse atascado (exploración). Nada de esto garantiza el óptimo; lo que produce es una presión estadística sostenida hacia soluciones mejores.

Vocabulario que usaremos: individuo/cromosoma (una solución codificada), gen (una posición del cromosoma), fitness (calidad del individuo), generación (una iteración del bucle).

Representación: cromosomas binarios y permutaciones

La primera decisión de diseño — y la más importante — es cómo codificar una solución:

  • Codificación binaria: una lista de 0/1. Natural para problemas de subconjunto, como la mochila de 02-02: [0, 1, 0, 1, 0] significa "cargar E2 y E4". Los operadores clásicos (cruce por punto, mutación bit a bit) funcionan directamente.
  • Codificación de permutación: un orden de elementos. Natural para problemas de secuencia como el TSP: [3, 1, 5, ...] es el orden de visita de las paradas. Cuidado: los operadores binarios clásicos aquí rompen la solución — cruzar dos permutaciones por un punto suele duplicar unas paradas y omitir otras, es decir, produce hijos que ni siquiera son rutas. Las permutaciones exigen operadores especializados (OX, que veremos enseguida).
Problema de Rutalia Codificación Individuo de ejemplo Ojo con
Qué cargar en la furgoneta (mochila) Binaria [1, 0, 0, 1, 1] Individuos infactibles (sobrepeso): penalizar o reparar
Orden de la ruta (TSP) Permutación [4, 1, 6, 9, 3, 7, 5, 2, 8] El cruce debe preservar que sea permutación

Función de fitness

El fitness traduce "calidad" a un número que la selección pueda comparar. Para el TSP, menor distancia = mejor individuo. Dos opciones habituales: usar directamente la longitud de la ruta (y seleccionar los menores), o convertir a "mayor es mejor" con fitness = 1 / longitud. Usaremos la primera por claridad, con una precaución de ingeniería: la evaluación de fitness es el 90 % del coste de un AG típico (se ejecuta población × generaciones veces), así que debe ser barata — y con nuestra matriz D precalculada en 02-02, lo es.

Para problemas con restricciones (mochila con sobrepeso), el fitness además debe decidir qué hacer con los infactibles: penalizar (restar mucho valor por kilo de exceso) o reparar (descargar objetos hasta que quepa). Las penalizaciones mal calibradas son una fuente clásica de AGs que "funcionan" llenos de soluciones inservibles.

Selección: torneo y ruleta

La selección elige qué individuos se reproducen. Debe favorecer a los buenos sin eliminar la diversidad (si solo se reproduce el mejor, en tres generaciones la población son clones y el AG degenera en un voraz caro).

  • Torneo (k participantes): elige k individuos al azar y gana el mejor. Simple, robusto y con presión regulable: k = 2 es suave, k = 7 es agresivo. Es la opción por defecto en la práctica.
  • Ruleta: cada individuo recibe una probabilidad proporcional a su fitness (una "ruleta" con sectores de tamaño desigual). Elegante en teoría, delicada en la práctica: exige fitness positivo y "mayor = mejor", y si un individuo domina mucho, monopoliza la ruleta (convergencia prematura); si todos son parecidos, la selección se vuelve casi aleatoria.
import random

def seleccion_torneo(poblacion, k=3):
    """Devuelve el mejor de k individuos elegidos al azar (menor longitud gana)."""
    candidatos = random.sample(poblacion, k)
    return min(candidatos, key=longitud_individuo)

def seleccion_ruleta(poblacion):
    """Probabilidad proporcional al fitness 1/longitud."""
    pesos = [1.0 / longitud_individuo(ind) for ind in poblacion]
    return random.choices(poblacion, weights=pesos, k=1)[0]

Cruce: un punto y order crossover (OX)

Cruce de un punto (codificación binaria): corta ambos padres por la misma posición aleatoria e intercambia las mitades. Para la mochila: [1,0|0,1,1] × [0,1|1,0,0] → hijos [1,0,1,0,0] y [0,1,0,1,1]. Barato y efectivo cuando genes contiguos forman "bloques" con sentido.

Order crossover (OX) (permutaciones): el operador estrella para rutas. Copia un segmento del padre 1 tal cual, y rellena los huecos con las paradas restantes en el orden en que aparecen en el padre 2. El hijo hereda una subruta literal de un padre y el orden relativo del otro — y siempre es una permutación válida:

Padre 1:  4 1 | 6 9 3 | 7 5 2 8        segmento elegido: posiciones 2-4
Padre 2:  9 3 5 2 6 8 1 4 7

Hijo:     _ _ | 6 9 3 | _ _ _ _        1) copiar el segmento del padre 1
Restantes en el orden del padre 2 (saltando 6, 9, 3): 5 2 8 1 4 7
Hijo:     5 2 | 6 9 3 | 8 1 4 7        2) rellenar los huecos en ese orden
def cruce_ox(padre1, padre2):
    n = len(padre1)
    a, b = sorted(random.sample(range(n), 2))       # extremos del segmento
    hijo = [None] * n
    hijo[a:b + 1] = padre1[a:b + 1]                 # 1) segmento literal del padre 1
    usados = set(hijo[a:b + 1])
    restantes = [g for g in padre2 if g not in usados]   # 2) orden del padre 2
    huecos = [i for i in range(n) if hijo[i] is None]
    for i, gen in zip(huecos, restantes):
        hijo[i] = gen
    return hijo

Mutación, elitismo y criterios de parada

  • Mutación: con probabilidad pequeña, alterar el individuo. Binaria: voltear un bit. Permutaciones: intercambiar dos posiciones (swap) o invertir un tramo. Sin mutación, la población solo recombina material genético inicial: si ninguna permutación inicial pone a H al final, el cruce jamás lo inventará; la mutación sí.
  • Elitismo: copiar intactos los 1-2 mejores individuos a la siguiente generación. Garantiza que el mejor resultado nunca empeora entre generaciones (monotonía que la selección estocástica por sí sola no da). Con demasiada élite, la población se uniformiza.
  • Criterios de parada: número fijo de generaciones (predecible, nuestro default), estancamiento (X generaciones sin mejora — el más usado en producción), presupuesto de tiempo, o alcanzar una cota conocida.

Implementación completa: AG para el TSP de Rutalia

Todo junto, sobre la instancia de 02-02 (requiere NOMBRES, D y longitud_ruta de esa lección). El depósito (índice 0) queda fuera del cromosoma: siempre es inicio y fin, así que el individuo es una permutación de [1..9]:

import random

def longitud_individuo(individuo):
    """Individuo = permutación de 1..9. La ruta completa antepone el depósito."""
    return longitud_ruta(tuple([0] + individuo))

def crear_individuo():
    genes = list(range(1, len(NOMBRES)))
    random.shuffle(genes)
    return genes

def mutacion_swap(individuo, prob=0.2):
    if random.random() < prob:
        i, j = random.sample(range(len(individuo)), 2)
        individuo[i], individuo[j] = individuo[j], individuo[i]
    return individuo

def algoritmo_genetico(tam_poblacion=100, generaciones=200,
                       k_torneo=3, prob_mutacion=0.2, elite=2, semilla=42):
    random.seed(semilla)                       # reproducibilidad de la ejecución
    poblacion = [crear_individuo() for _ in range(tam_poblacion)]
    historial = []                             # mejor longitud por generación

    for g in range(generaciones):
        poblacion.sort(key=longitud_individuo)          # mejores primero
        historial.append(longitud_individuo(poblacion[0]))

        nueva = [ind[:] for ind in poblacion[:elite]]   # ELITISMO: copias, no referencias
        while len(nueva) < tam_poblacion:
            padre1 = seleccion_torneo(poblacion, k_torneo)
            padre2 = seleccion_torneo(poblacion, k_torneo)
            hijo = cruce_ox(padre1, padre2)
            nueva.append(mutacion_swap(hijo, prob_mutacion))
        poblacion = nueva

    mejor = min(poblacion, key=longitud_individuo)
    return mejor, longitud_individuo(mejor), historial

mejor, km, hist = algoritmo_genetico()
print([NOMBRES[i] for i in [0] + mejor], round(km, 2))
# ['DEP', 'D', 'A', 'F', 'I', 'C', 'G', 'E', 'B', 'H'] 35.22
print("Generación en que se alcanzó:", hist.index(min(hist)))

Puntos que merecen lupa:

  • Evaluaciones totales: 100 individuos × 200 generaciones = 20.000 evaluaciones de ruta — del mismo orden de trabajo útil que los ~1.200 nodos de B&B multiplicados por su coste de cota, y muy por debajo de las 362.880 rutas de la fuerza bruta. En nuestra ejecución, el AG alcanza el óptimo conocido de 35,22 km (la misma ruta que B&B), normalmente en las primeras decenas de generaciones. Que esto ocurra casi siempre en una instancia de 10 nodos es esperable: el espacio es pequeño para la potencia del método. La diferencia aparece al escalar: con 100 paradas, B&B no termina y el AG sigue entregando buenas rutas con el mismo código.
  • La élite se copia (ind[:]), no se referencia: si la mutación tocara una referencia compartida, corrompería al mejor individuo silenciosamente. Errores así son la pesadilla de depurar AGs, porque el algoritmo sigue funcionando, solo que peor.
  • El historial es tu instrumento de diagnóstico: una curva que cae rápido y se aplana pronto sugiere convergencia (¿prematura?); una que baja a trompicones hasta el final pide más generaciones.
  • La semilla fija la secuencia aleatoria. En producción se ejecutan varias semillas y se reporta media y mejor caso — una sola ejecución de un algoritmo estocástico es una anécdota, no una medida.

Hiperparámetros: qué tocar y qué esperar

Los AG no se programan una vez: se ajustan. Guía de efectos:

Hiperparámetro Valor típico Si es demasiado bajo Si es demasiado alto
Tamaño de población 50–200 Poca diversidad: convergencia prematura Coste por generación alto sin mejora proporcional
Generaciones 100–1000 Se para antes de converger Tiempo desperdiciado tras el estancamiento
k del torneo 2–5 Presión débil: deriva casi aleatoria Presión brutal: clones en pocas generaciones
Prob. de mutación 0,05–0,3 por individuo La población se uniformiza y se atasca La búsqueda degenera en paseo aleatorio
Élite 1–2 (≈ 1–2 %) El mejor puede perderse entre generaciones La élite domina y congela la evolución

La tensión de fondo es siempre la misma: explotación (refinar lo bueno: más presión de selección, más élite) contra exploración (buscar lo nuevo: más mutación, más población). No existe configuración universal; existe el hábito de medir con el historial y ajustar un parámetro cada vez.

Las primas de vecindad: recocido simulado y búsqueda tabú

Los AG no son la única forma de buscar sin garantías. Dos familias que conviene conocer de nombre — trabajan con un solo individuo que se mueve por su vecindario (soluciones a un pequeño cambio de distancia), en lugar de con una población:

  • Recocido simulado (simulated annealing): acepta a veces movimientos que empeoran, con una probabilidad que decae con el tiempo (la "temperatura" baja). Al principio explora con libertad; al final solo refina. Inspiración: el enfriamiento lento de los metales.
  • Búsqueda tabú: búsqueda local que mantiene una lista tabú de movimientos recientes prohibidos, para no deshacer lo andado ni ciclar alrededor de un óptimo local.
Algoritmo genético Recocido simulado Búsqueda tabú
Estado Población Un individuo Un individuo + memoria
Motor de mejora Cruce + selección Vecino aleatorio + aceptación probabilística Mejor vecino no tabú
Escapa de óptimos locales por… Diversidad poblacional y mutación Aceptar empeoramientos (temperatura) Prohibir volver atrás
Hiperparámetro crítico Presión de selección / mutación Esquema de enfriamiento Tamaño de la lista tabú

En la práctica compiten de tú a tú con los AG (y a menudo se hibridan: un AG cuyo mejor individuo se refina con búsqueda local). No las desarrollaremos más: nos basta con saber que existen y qué las distingue, porque la próxima lección presenta una metaheurística de espíritu muy distinto — basada en cooperación mediante rastros compartidos — y cerraremos comparando las tres aproximaciones del módulo sobre el mismo TSP.

Errores Comunes y Consejos

  • Cruce clásico sobre permutaciones. El cruce de un punto aplicado a rutas produce hijos con paradas duplicadas y omitidas — ni siquiera son soluciones. Con permutaciones, usa OX (o PMX/CX, sus parientes). Es el error número uno al adaptar código de mochila a TSP.
  • Olvidar copiar la élite. nueva = poblacion[:2] guarda referencias; una mutación posterior corrompe a los mejores. Copia con ind[:] o list(ind).
  • Fitness caro. Si evaluar un individuo cuesta 10 ms, 100 × 200 evaluaciones son 3,3 minutos solo de fitness. Precalcula (nuestra matriz D), cachea, vectoriza. El análisis de costes de 01-02 se aplica dentro de la metaheurística.
  • Convergencia prematura no diagnosticada. Si en la generación 20 todos los individuos son casi idénticos, el resto del presupuesto se tira. Vigila la diversidad (p. ej., longitudes distintas en la población) además del mejor fitness.
  • Concluir a partir de una ejecución. Estocástico significa que la semilla 42 puede ser afortunada. Ejecuta 10-30 semillas y mira distribución, no anécdota.
  • Usar un AG donde hay método exacto viable. Para la mochila de 5 envíos o el TSP de 10 puntos, PD y B&B dan el óptimo certificado en menos tiempo del que cuesta ajustar hiperparámetros. Las metaheurísticas son la herramienta de frontera, no la primera opción.
  • Consejo: guarda siempre el mejor individuo global de la ejecución (no solo el de la última generación) y, si existe, compáralo con una cota o un óptimo conocido. Sin referencia, "el AG mejoró" no significa nada.

Ejercicios

  1. OX a mano. Con padres P1 = [2, 5, 1, 4, 3, 6] y P2 = [4, 1, 6, 2, 5, 3] y segmento en las posiciones 1–3 (0-indexado, ambas incluidas), calcula el hijo del cruce_ox paso a paso. Verifica que es una permutación válida.

  2. AG para la mochila. Adapta el esquema completo a la mochila 0/1 de 02-02 (capacidad 15, pesos [12, 7, 11, 8, 9], valores [40, 24, 35, 26, 30]): codificación binaria, cruce de un punto, mutación de volteo de bit, y fitness con penalización por sobrepeso (valor − 10 × exceso). ¿Alcanza el óptimo de 50 €? ¿Qué pasa si la penalización es 1 por unidad de exceso en vez de 10?

  3. Estudio de mutación. Ejecuta algoritmo_genetico con prob_mutacion en {0.0, 0.05, 0.2, 0.8} y 10 semillas cada una (deja el resto de parámetros por defecto). Para cada valor anota cuántas semillas alcanzan 35,22 km y la media del mejor resultado. Interpreta los extremos.

Soluciones

Ejercicio 1. Segmento de P1, posiciones 1–3: 5, 1, 4 → hijo parcial [_, 5, 1, 4, _, _]. Restantes de P2 en su orden, saltando 5, 1, 4: P2 = [4, 1, 6, 2, 5, 3] → quedan 6, 2, 3. Huecos: posiciones 0, 4, 5 → hijo = [6, 5, 1, 4, 2, 3]. Contiene cada gen 1..6 exactamente una vez: permutación válida. Observa la herencia: el bloque 5-1-4 viene literal de P1; el orden relativo 6 → 2 → 3 viene de P2.

Ejercicio 2. Esqueleto de los cambios:

def crear_individuo():
    return [random.randint(0, 1) for _ in range(5)]

def fitness(ind, penal=10):
    peso  = sum(p * x for p, x in zip([12, 7, 11, 8, 9], ind))
    valor = sum(v * x for v, x in zip([40, 24, 35, 26, 30], ind))
    exceso = max(0, peso - 15)
    return valor - penal * exceso

def cruce_un_punto(p1, p2):
    c = random.randint(1, len(p1) - 1)
    return p1[:c] + p2[c:]

def mutacion_bit(ind, prob=0.2):
    if random.random() < prob:
        i = random.randrange(len(ind))
        ind[i] = 1 - ind[i]
    return ind

Con penalización 10, el sobrepeso sale carísimo y la población converge a individuos factibles; el óptimo [0,1,0,1,0] (50 €) aparece en pocas generaciones — el espacio solo tiene 2⁵ = 32 puntos, así que más que un reto es un banco de pruebas. Con penalización 1 el AG descubre un "truco" indeseado: [1,1,1,1,1] pesa 47 (exceso 32) y puntúa 155 − 32 = 123 > 50 — el mejor individuo es infactible y el AG optimiza con entusiasmo el problema equivocado. Es la lección importante del ejercicio: la penalización debe hacer que ningún infactible supere a un factible, o el fitness miente.

Ejercicio 3. Resultados típicos (los tuyos variarán, de eso trata el ejercicio): con 0.0, varias semillas se quedan clavadas por encima del óptimo — sin mutación, la población agota su material genético inicial y no puede fabricar orderings nuevos; con 0.05 y 0.2, la gran mayoría de semillas alcanzan 35,22 km, con 0.2 algo más robusto en esta instancia; con 0.8, los hijos se destruyen casi siempre nada más nacer y la media empeora — la búsqueda se acerca a un muestreo aleatorio con élite. La curva calidad-vs-mutación tiene forma de U invertida: el punto dulce está donde hay variedad suficiente sin destruir la herencia.

Conclusión

Hemos hecho el trato heurístico con los ojos abiertos: a cambio del certificado de optimalidad que daban PD y B&B, los algoritmos genéticos nos dan escala y generalidad — solo piden una codificación sensata (binaria para subconjuntos, permutaciones con OX para rutas), un fitness barato y honesto con las restricciones, y el equilibrio eterno entre presión de selección y mutación. Sobre el TSP de Rutalia, nuestro AG de 100 individuos y 200 generaciones alcanzó los mismos 35,22 km que B&B certificó en 02-03 — sin poder certificarlos, pero con un código que seguiría funcionando igual con 100 paradas, donde B&B ya no juega. En la próxima lección conoceremos una metaheurística con otra inspiración biológica: en lugar de herencia y selección, cooperación indirecta — hormigas que se dejan señales químicas sobre las buenas rutas. Implementaremos la optimización por colonia de hormigas sobre esta misma instancia y cerraremos el módulo con la comparación experimental de las tres aproximaciones: exacta, evolutiva y estigmérgica.

© Copyright 2026. Todos los derechos reservados