El branch and bound ens va portar a l'òptim del TSP de Rutalia expandint mil nodes en lloc d'un milió — però vam tancar la lliçó admetent que l'explosió combinatòria només estava retardada: amb les 60-120 parades d'una furgoneta real, cap mètode exacte no acaba a temps. Aquesta lliçó creua la frontera de manera deliberada: els algorismes genètics (AG) renuncien a la garantia d'optimalitat i, a canvi, produeixen solucions molt bones en temps controlat, per a gairebé qualsevol problema que sàpigues avaluar. Veurem l'esquema evolutiu complet — codificació, població, fitness, selecció, encreuament, mutació, elitisme — i l'implementarem sencer en Python sobre la mateixa instància de 10 punts de 02-02/02-03, per poder comparar contra l'òptim conegut de 35,22 km. Acabarem amb una guia d'hiperparàmetres i un cop d'ull breu a les metaheurístiques "cosines" (recuita simulada, cerca tabú).
Contingut
- Exactes vs heurístics: què sacrifiquem i què guanyem
- La metàfora evolutiva
- Representació: cromosomes binaris i permutacions
- Funció de fitness
- Selecció: torneig i ruleta
- Encreuament: un punt i order crossover (OX)
- Mutació, elitisme i criteris de parada
- Implementació completa: AG per al TSP de Rutalia
- Hiperparàmetres: què tocar i què esperar
- Les cosines de veïnatge: recuita simulada i cerca tabú
Exactes vs heurístics: què sacrifiquem i què guanyem
| Exactes (02-03) | Metaheurístiques (02-04, 02-05) | |
|---|---|---|
| Resultat | Òptim certificat | Solució bona, sense certificat |
| Temps | Impredictible (exponencial en el pitjor cas) | Controlat: tu decideixes quantes iteracions |
| Escala | TSP: desenes de nodes (centenars amb fites fines) | Milers de nodes i problemes "bruts" |
| Requisits | Estructura explotable (fites, relaxacions) | Només saber avaluar una solució |
| Reproduïbilitat | Determinista | Estocàstica: cada execució pot diferir |
La paraula clau és certificat. Un AG pot donar-te la ruta òptima — de fet, en la nostra instància petita ho farà — però no t'ho pot dir: no produeix fites que demostrin que no existeix res millor. Per a Rutalia el tracte sol compensar: entre "la ruta òptima d'aquí a tres setmanes" i "una ruta un 2 % pitjor d'aquí a un minut", operacions tria el segon cada dia. La disciplina professional consisteix a saber quan estàs fent aquest tracte i a mesurar la qualitat contra fites o instàncies amb òptim conegut — exactament el que farem aquí.
La metàfora evolutiva
Un AG manté una població de solucions candidates i la fa evolucionar imitant la selecció natural:
flowchart LR
A["Població inicial<br/>(aleatòria)"] --> B["Avaluar el fitness<br/>de cada individu"]
B --> C["Seleccionar pares<br/>(els millors tenen avantatge)"]
C --> D["Encreuament:<br/>combinar dos pares"]
D --> E["Mutació:<br/>alterar a l'atzar"]
E --> F["Nova generació<br/>(+ elitisme)"]
F --> B
B -->|"criteri de parada"| G["Millor individu<br/>trobat"]
La intuïció de per què funciona: la selecció concentra la població en zones bones de l'espai de solucions (explotació), l'encreuament combina fragments bons de pares diferents — potser una mare resol bé el nord de la ciutat i un pare el sud —, i la mutació injecta varietat per no quedar-se encallat (exploració). Res d'això no garanteix l'òptim; el que produeix és una pressió estadística sostinguda cap a solucions millors.
Vocabulari que farem servir: individu/cromosoma (una solució codificada), gen (una posició del cromosoma), fitness (qualitat de l'individu), generació (una iteració del bucle).
Representació: cromosomes binaris i permutacions
La primera decisió de disseny — i la més important — és com codificar una solució:
- Codificació binària: una llista de 0/1. Natural per a problemes de subconjunt, com la motxilla de 02-02:
[0, 1, 0, 1, 0]significa "carregar E2 i E4". Els operadors clàssics (encreuament per punt, mutació bit a bit) funcionen directament. - Codificació de permutació: un ordre d'elements. Natural per a problemes de seqüència com el TSP:
[3, 1, 5, ...]és l'ordre de visita de les parades. Compte: els operadors binaris clàssics aquí trenquen la solució — encreuar dues permutacions per un punt sol duplicar unes parades i ometre'n d'altres, és a dir, produeix fills que ni tan sols són rutes. Les permutacions exigeixen operadors especialitzats (OX, que veurem de seguida).
| Problema de Rutalia | Codificació | Individu d'exemple | Compte amb |
|---|---|---|---|
| Què carregar a la furgoneta (motxilla) | Binària | [1, 0, 0, 1, 1] |
Individus infactibles (sobrepès): penalitzar o reparar |
| Ordre de la ruta (TSP) | Permutació | [4, 1, 6, 9, 3, 7, 5, 2, 8] |
L'encreuament ha de preservar que sigui permutació |
Funció de fitness
El fitness tradueix "qualitat" a un número que la selecció pugui comparar. Per al TSP, menor distància = millor individu. Dues opcions habituals: usar directament la longitud de la ruta (i seleccionar els menors), o convertir a "més gran és millor" amb fitness = 1 / longitud. Usarem la primera per claredat, amb una precaució d'enginyeria: l'avaluació de fitness és el 90 % del cost d'un AG típic (s'executa població × generacions vegades), així que ha de ser barata — i amb la nostra matriu D precalculada a 02-02, ho és.
Per a problemes amb restriccions (motxilla amb sobrepès), el fitness a més ha de decidir què fer amb els infactibles: penalitzar (restar molt valor per quilo d'excés) o reparar (descarregar objectes fins que hi càpiga). Les penalitzacions mal calibrades són una font clàssica d'AG que "funcionen" plens de solucions inservibles.
Selecció: torneig i ruleta
La selecció tria quins individus es reprodueixen. Ha d'afavorir els bons sense eliminar la diversitat (si només es reprodueix el millor, en tres generacions la població són clons i l'AG degenera en un voraç car).
- Torneig (k participants): tria k individus a l'atzar i guanya el millor. Simple, robust i amb pressió regulable: k = 2 és suau, k = 7 és agressiu. És l'opció per defecte a la pràctica.
- Ruleta: cada individu rep una probabilitat proporcional al seu fitness (una "ruleta" amb sectors de mida desigual). Elegant en teoria, delicada a la pràctica: exigeix fitness positiu i "més gran = millor", i si un individu domina molt, monopolitza la ruleta (convergència prematura); si tots són semblants, la selecció esdevé gairebé aleatòria.
import random
def seleccio_torneig(poblacio, k=3):
"""Retorna el millor de k individus triats a l'atzar (menor longitud guanya)."""
candidats = random.sample(poblacio, k)
return min(candidats, key=longitud_individu)
def seleccio_ruleta(poblacio):
"""Probabilitat proporcional al fitness 1/longitud."""
pesos = [1.0 / longitud_individu(ind) for ind in poblacio]
return random.choices(poblacio, weights=pesos, k=1)[0]Encreuament: un punt i order crossover (OX)
Encreuament d'un punt (codificació binària): talla tots dos pares per la mateixa posició aleatòria i intercanvia les meitats. Per a la motxilla: [1,0|0,1,1] × [0,1|1,0,0] → fills [1,0,1,0,0] i [0,1,0,1,1]. Barat i efectiu quan gens contigus formen "blocs" amb sentit.
Order crossover (OX) (permutacions): l'operador estrella per a rutes. Copia un segment del pare 1 tal qual, i omple els forats amb les parades restants en l'ordre en què apareixen al pare 2. El fill hereta una subruta literal d'un pare i l'ordre relatiu de l'altre — i sempre és una permutació vàlida:
Pare 1: 4 1 | 6 9 3 | 7 5 2 8 segment triat: posicions 2-4
Pare 2: 9 3 5 2 6 8 1 4 7
Fill: _ _ | 6 9 3 | _ _ _ _ 1) copiar el segment del pare 1
Restants en l'ordre del pare 2 (saltant 6, 9, 3): 5 2 8 1 4 7
Fill: 5 2 | 6 9 3 | 8 1 4 7 2) omplir els forats en aquest ordredef encreuament_ox(pare1, pare2):
n = len(pare1)
a, b = sorted(random.sample(range(n), 2)) # extrems del segment
fill = [None] * n
fill[a:b + 1] = pare1[a:b + 1] # 1) segment literal del pare 1
usats = set(fill[a:b + 1])
restants = [g for g in pare2 if g not in usats] # 2) ordre del pare 2
forats = [i for i in range(n) if fill[i] is None]
for i, gen in zip(forats, restants):
fill[i] = gen
return fillMutació, elitisme i criteris de parada
- Mutació: amb probabilitat petita, alterar l'individu. Binària: capgirar un bit. Permutacions: intercanviar dues posicions (swap) o invertir un tram. Sense mutació, la població només recombina material genètic inicial: si cap permutació inicial no posa H al final, l'encreuament mai no ho inventarà; la mutació sí.
- Elitisme: copiar intactes els 1-2 millors individus a la generació següent. Garanteix que el millor resultat mai no empitjora entre generacions (monotonia que la selecció estocàstica per si sola no dona). Amb massa elit, la població s'uniformitza.
- Criteris de parada: nombre fix de generacions (predictible, el nostre default), estancament (X generacions sense millora — el més usat en producció), pressupost de temps, o assolir una fita coneguda.
Implementació completa: AG per al TSP de Rutalia
Tot junt, sobre la instància de 02-02 (requereix NOMS, D i longitud_ruta d'aquella lliçó). El dipòsit (índex 0) queda fora del cromosoma: sempre és inici i final, així que l'individu és una permutació d'[1..9]:
import random
def longitud_individu(individu):
"""Individu = permutació d'1..9. La ruta completa anteposa el dipòsit."""
return longitud_ruta(tuple([0] + individu))
def crear_individu():
gens = list(range(1, len(NOMS)))
random.shuffle(gens)
return gens
def mutacio_swap(individu, prob=0.2):
if random.random() < prob:
i, j = random.sample(range(len(individu)), 2)
individu[i], individu[j] = individu[j], individu[i]
return individu
def algorisme_genetic(mida_poblacio=100, generacions=200,
k_torneig=3, prob_mutacio=0.2, elit=2, llavor=42):
random.seed(llavor) # reproduïbilitat de l'execució
poblacio = [crear_individu() for _ in range(mida_poblacio)]
historial = [] # millor longitud per generació
for g in range(generacions):
poblacio.sort(key=longitud_individu) # millors primer
historial.append(longitud_individu(poblacio[0]))
nova = [ind[:] for ind in poblacio[:elit]] # ELITISME: còpies, no referències
while len(nova) < mida_poblacio:
pare1 = seleccio_torneig(poblacio, k_torneig)
pare2 = seleccio_torneig(poblacio, k_torneig)
fill = encreuament_ox(pare1, pare2)
nova.append(mutacio_swap(fill, prob_mutacio))
poblacio = nova
millor = min(poblacio, key=longitud_individu)
return millor, longitud_individu(millor), historial
millor, km, hist = algorisme_genetic()
print([NOMS[i] for i in [0] + millor], round(km, 2))
# ['DEP', 'D', 'A', 'F', 'I', 'C', 'G', 'E', 'B', 'H'] 35.22
print("Generació en què s'ha assolit:", hist.index(min(hist)))Punts que mereixen lupa:
- Avaluacions totals: 100 individus × 200 generacions = 20.000 avaluacions de ruta — del mateix ordre de feina útil que els ~1.200 nodes de B&B multiplicats pel seu cost de fita, i molt per sota de les 362.880 rutes de la força bruta. En la nostra execució, l'AG assoleix l'òptim conegut de 35,22 km (la mateixa ruta que B&B), normalment en les primeres desenes de generacions. Que això passi gairebé sempre en una instància de 10 nodes és esperable: l'espai és petit per a la potència del mètode. La diferència apareix en escalar: amb 100 parades, B&B no acaba i l'AG continua lliurant bones rutes amb el mateix codi.
- L'elit es copia (
ind[:]), no es referencia: si la mutació toqués una referència compartida, corrompria el millor individu silenciosament. Errors així són el malson de depurar AG, perquè l'algorisme continua funcionant, només que pitjor. - L'historial és el teu instrument de diagnòstic: una corba que cau ràpid i s'aplana aviat suggereix convergència (prematura?); una que baixa a batzegades fins al final demana més generacions.
- La llavor fixa la seqüència aleatòria. En producció s'executen diverses llavors i es reporta mitjana i millor cas — una sola execució d'un algorisme estocàstic és una anècdota, no una mesura.
Hiperparàmetres: què tocar i què esperar
Els AG no es programen una vegada: s'ajusten. Guia d'efectes:
| Hiperparàmetre | Valor típic | Si és massa baix | Si és massa alt |
|---|---|---|---|
| Mida de població | 50–200 | Poca diversitat: convergència prematura | Cost per generació alt sense millora proporcional |
| Generacions | 100–1000 | S'atura abans de convergir | Temps malbaratat després de l'estancament |
| k del torneig | 2–5 | Pressió feble: deriva gairebé aleatòria | Pressió brutal: clons en poques generacions |
| Prob. de mutació | 0,05–0,3 per individu | La població s'uniformitza i s'encalla | La cerca degenera en passeig aleatori |
| Elit | 1–2 (≈ 1–2 %) | El millor es pot perdre entre generacions | L'elit domina i congela l'evolució |
La tensió de fons és sempre la mateixa: explotació (refinar el que és bo: més pressió de selecció, més elit) contra exploració (buscar el que és nou: més mutació, més població). No existeix cap configuració universal; existeix l'hàbit de mesurar amb l'historial i ajustar un paràmetre cada vegada.
Les cosines de veïnatge: recuita simulada i cerca tabú
Els AG no són l'única manera de cercar sense garanties. Dues famílies que convé conèixer de nom — treballen amb un sol individu que es mou pel seu veïnatge (solucions a un petit canvi de distància), en lloc de fer-ho amb una població:
- Recuita simulada (simulated annealing): accepta de vegades moviments que empitjoren, amb una probabilitat que decau amb el temps (la "temperatura" baixa). Al principi explora amb llibertat; al final només refina. Inspiració: el refredament lent dels metalls.
- Cerca tabú: cerca local que manté una llista tabú de moviments recents prohibits, per no desfer el camí fet ni ciclar al voltant d'un òptim local.
| Algorisme genètic | Recuita simulada | Cerca tabú | |
|---|---|---|---|
| Estat | Població | Un individu | Un individu + memòria |
| Motor de millora | Encreuament + selecció | Veí aleatori + acceptació probabilística | Millor veí no tabú |
| Escapa d'òptims locals per… | Diversitat poblacional i mutació | Acceptar empitjoraments (temperatura) | Prohibir tornar enrere |
| Hiperparàmetre crític | Pressió de selecció / mutació | Esquema de refredament | Mida de la llista tabú |
A la pràctica competeixen de tu a tu amb els AG (i sovint s'hibriden: un AG el millor individu del qual es refina amb cerca local). No les desenvoluparem més: en tenim prou de saber que existeixen i què les distingeix, perquè la propera lliçó presenta una metaheurística d'esperit molt diferent — basada en cooperació mitjançant rastres compartits — i tancarem comparant les tres aproximacions del mòdul sobre el mateix TSP.
Errors Comuns i Consells
- Encreuament clàssic sobre permutacions. L'encreuament d'un punt aplicat a rutes produeix fills amb parades duplicades i omeses — ni tan sols són solucions. Amb permutacions, usa OX (o PMX/CX, els seus parents). És l'error número u en adaptar codi de motxilla a TSP.
- Oblidar copiar l'elit.
nova = poblacio[:2]guarda referències; una mutació posterior corromp els millors. Copia ambind[:]olist(ind). - Fitness car. Si avaluar un individu costa 10 ms, 100 × 200 avaluacions són 3,3 minuts només de fitness. Precalcula (la nostra matriu
D), fes memòria cau, vectoritza. L'anàlisi de costos de 01-02 s'aplica dins de la metaheurística. - Convergència prematura no diagnosticada. Si a la generació 20 tots els individus són gairebé idèntics, la resta del pressupost es llença. Vigila la diversitat (p. ex., longituds diferents a la població) a més del millor fitness.
- Concloure a partir d'una execució. Estocàstic significa que la llavor 42 pot ser afortunada. Executa 10-30 llavors i mira la distribució, no l'anècdota.
- Usar un AG on hi ha mètode exacte viable. Per a la motxilla de 5 enviaments o el TSP de 10 punts, PD i B&B donen l'òptim certificat en menys temps del que costa ajustar hiperparàmetres. Les metaheurístiques són l'eina de frontera, no la primera opció.
- Consell: guarda sempre el millor individu global de l'execució (no només el de la darrera generació) i, si existeix, compara'l amb una fita o un òptim conegut. Sense referència, "l'AG ha millorat" no significa res.
Exercicis
-
OX a mà. Amb pares
P1 = [2, 5, 1, 4, 3, 6]iP2 = [4, 1, 6, 2, 5, 3]i segment a les posicions 1–3 (0-indexat, totes dues incloses), calcula el fill de l'encreuament_oxpas a pas. Verifica que és una permutació vàlida. -
AG per a la motxilla. Adapta l'esquema complet a la motxilla 0/1 de 02-02 (capacitat 15, pesos
[12, 7, 11, 8, 9], valors[40, 24, 35, 26, 30]): codificació binària, encreuament d'un punt, mutació de capgirament de bit, i fitness amb penalització per sobrepès (valor − 10 × excés). Assoleix l'òptim de 50 €? Què passa si la penalització és 1 per unitat d'excés en lloc de 10? -
Estudi de mutació. Executa
algorisme_geneticambprob_mutacioa{0.0, 0.05, 0.2, 0.8}i 10 llavors cadascuna (deixa la resta de paràmetres per defecte). Per a cada valor anota quantes llavors assoleixen 35,22 km i la mitjana del millor resultat. Interpreta els extrems.
Solucions
Exercici 1. Segment de P1, posicions 1–3: 5, 1, 4 → fill parcial [_, 5, 1, 4, _, _]. Restants de P2 en el seu ordre, saltant 5, 1, 4: P2 = [4, 1, 6, 2, 5, 3] → queden 6, 2, 3. Forats: posicions 0, 4, 5 → fill = [6, 5, 1, 4, 2, 3]. Conté cada gen 1..6 exactament una vegada: permutació vàlida. Observa l'herència: el bloc 5-1-4 ve literal de P1; l'ordre relatiu 6 → 2 → 3 ve de P2.
Exercici 2. Esquelet dels canvis:
def crear_individu():
return [random.randint(0, 1) for _ in range(5)]
def fitness(ind, penal=10):
pes = 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))
exces = max(0, pes - 15)
return valor - penal * exces
def encreuament_un_punt(p1, p2):
c = random.randint(1, len(p1) - 1)
return p1[:c] + p2[c:]
def mutacio_bit(ind, prob=0.2):
if random.random() < prob:
i = random.randrange(len(ind))
ind[i] = 1 - ind[i]
return indAmb penalització 10, el sobrepès surt caríssim i la població convergeix a individus factibles; l'òptim [0,1,0,1,0] (50 €) apareix en poques generacions — l'espai només té 2⁵ = 32 punts, així que més que un repte és un banc de proves. Amb penalització 1 l'AG descobreix un "truc" indesitjat: [1,1,1,1,1] pesa 47 (excés 32) i puntua 155 − 32 = 123 > 50 — el millor individu és infactible i l'AG optimitza amb entusiasme el problema equivocat. És la lliçó important de l'exercici: la penalització ha de fer que cap infactible no superi un factible, o el fitness menteix.
Exercici 3. Resultats típics (els teus variaran, d'això tracta l'exercici): amb 0.0, diverses llavors es queden clavades per sobre de l'òptim — sense mutació, la població esgota el seu material genètic inicial i no pot fabricar ordenacions noves; amb 0.05 i 0.2, la gran majoria de llavors assoleixen 35,22 km, amb 0.2 una mica més robust en aquesta instància; amb 0.8, els fills es destrueixen gairebé sempre tot just néixer i la mitjana empitjora — la cerca s'acosta a un mostreig aleatori amb elit. La corba qualitat-vs-mutació té forma d'U invertida: el punt dolç és on hi ha varietat suficient sense destruir l'herència.
Conclusió
Hem fet el tracte heurístic amb els ulls oberts: a canvi del certificat d'optimalitat que donaven PD i B&B, els algorismes genètics ens donen escala i generalitat — només demanen una codificació sensata (binària per a subconjunts, permutacions amb OX per a rutes), un fitness barat i honest amb les restriccions, i l'equilibri etern entre pressió de selecció i mutació. Sobre el TSP de Rutalia, el nostre AG de 100 individus i 200 generacions va assolir els mateixos 35,22 km que B&B va certificar a 02-03 — sense poder certificar-los, però amb un codi que continuaria funcionant igual amb 100 parades, on B&B ja no juga. A la propera lliçó coneixerem una metaheurística amb una altra inspiració biològica: en lloc d'herència i selecció, cooperació indirecta — formigues que es deixen senyals químics sobre les bones rutes. Implementarem l'optimització per colònia de formigues sobre aquesta mateixa instància i tancarem el mòdul amb la comparació experimental de les tres aproximacions: exacta, evolutiva i estigmèrgica.
Algorismes Avançats
Mòdul 1: Introducció als Algorismes Avançats
- Conceptes Bàsics i Notació
- Anàlisi de Complexitat
- Recursió i Programació Dinàmica
- Estructures de Dades Avançades
Mòdul 2: Algorismes d'Optimització
- Programació Lineal
- Algorismes d'Optimització Combinatòria
- Backtracking i Branch and Bound
- Algorismes Genètics
- Optimització de Colònia de Formigues
Mòdul 3: Algorismes en Grafs
- Representació de Grafs
- Cerca en Grafs: BFS i DFS
- Algorismes de Camins Mínims
- Arbres d'Expansió Mínima
- Algorismes de Flux Màxim
- Algorismes d'Aparellament en Grafs
Mòdul 4: Algorismes de Cerca i Ordenació
Mòdul 5: Algorismes d'Aprenentatge Automàtic
- Introducció a l'Aprenentatge Automàtic
- Algorismes de Classificació
- Algorismes de Regressió
- Xarxes Neuronals i Deep Learning
- Algorismes de Clustering
Mòdul 6: Casos d'Estudi i Aplicacions
- Optimització a la Indústria
- Aplicacions de Grafs a les Xarxes Socials
- Cerca i Ordenació en Grans Volums de Dades
- Aplicacions d'Aprenentatge Automàtic a la Vida Real
