A la lliçó anterior vam aprendre a trossejar problemes amb divideix i venceràs. La segona gran estratègia del mòdul és de temperament oposat: en lloc de resoldre-ho tot i després decidir, un algorisme greedy (voraç) construeix la solució pas a pas prenent a cada moment la decisió que sembla millor ara mateix, sense mirar enrere i sense calcular les conseqüències futures. Aquesta impaciència produeix algorismes senzills i rapidíssims... que unes vegades són òptims demostrables i d'altres fallen estrepitosament sense avisar. A RutaBus ja vam topar amb un greedy sense saber-ho: el recorregut_supervisor de 01-02, aquella heurística del veí més proper. En aquesta lliçó aprendrem l'anatomia d'un greedy, resoldrem un problema real d'assignació de trajectes, veurem amb els nostres propis ulls un greedy fallant, i — el més important — aprendrem a distingir quan es pot confiar en un.
Contingut
- La idea: decisió localment òptima i irrevocable
- Anatomia d'un algorisme greedy
- Exemple RutaBus: màxim nombre de trajectes per a un autobús
- Per què funciona: una demostració informal
- Quan greedy falla: dos contraexemples
- Com reconèixer si un problema admet greedy
- Algorismes greedy cèlebres
La idea: decisió localment òptima i irrevocable
Un algorisme greedy es caracteritza per dos trets:
- Elecció localment òptima: a cada pas escull el candidat que maximitza (o minimitza) un criteri immediat, sense simular el futur.
- Irrevocabilitat: una decisió presa no es revisa mai. No hi ha tornada enrere — just el contrari del backtracking que veurem a 03-04.
Aquesta combinació explica les seves dues cares:
| Cara bona | Cara dolenta |
|---|---|
| Molt ràpids: cada element es decideix una vegada, típicament O(n log n) per l'ordenació prèvia | La suma d'òptims locals no sempre és l'òptim global |
| Fàcils d'implementar i d'entendre | La fallada és silenciosa: retornen una solució, només que no la millor |
| Poca memòria: no guarden alternatives | Exigeixen una demostració (o almenys un argument sòlid) d'optimalitat |
Anatomia d'un algorisme greedy
Gairebé tots els greedy comparteixen quatre peces. Convé identificar-les explícitament abans d'escriure codi:
- Conjunt de candidats: els elements entre els quals es tria (trajectes, monedes, parades...).
- Funció de selecció: el criteri voraç que decideix quin candidat prendre a continuació ("el que acaba abans", "la moneda més grossa", "la parada més propera").
- Test de factibilitat: puc afegir aquest candidat a la solució parcial sense violar les restriccions?
- Solució: la construcció acaba quan s'esgoten els candidats o la solució és completa.
En pseudocodi, l'esquelet universal:
solucio = buida
candidats = ordena(candidats, per la funció de selecció)
per a cada candidat c en ordre:
si afegir c a solucio és factible:
afegir c a solucio # irrevocable: mai no es treu
retorna solucioTot l'enginy és al pas d'ordenar: triar bé la funció de selecció és dissenyar l'algorisme. Amb la funció correcta, greedy és òptim; amb una de plausible però incorrecta, és només una heurística (recorda la distinció exactes vs heurístics de 01-02).
Exemple RutaBus: màxim nombre de trajectes per a un autobús
El problema. RutaBus té un únic autobús de reforç i una llista de trajectes sol·licitats per a demà, cadascun amb hora d'inici i hora de fi (en minuts des de mitjanit). L'autobús només pot fer un trajecte alhora. Objectiu: assignar-li el màxim nombre de trajectes sense solapaments. (És la versió RutaBus del clàssic problema de selecció d'activitats.)
# (nom, inici, fi) — minuts des de les 00:00
trajectes = [
("Plaça Major -> Estació Nord", 540, 600), # 09:00-10:00
("Estació Nord -> Hospital", 570, 630), # 09:30-10:30
("Hospital -> Parc del Riu", 600, 660), # 10:00-11:00
("Parc del Riu -> Plaça Major", 615, 675), # 10:15-11:15
("Plaça Major -> Hospital", 660, 720), # 11:00-12:00
("Hospital -> Estació Nord", 690, 780), # 11:30-13:00
]Abans de veure la solució, pensa-hi: quina funció de selecció faries servir? Candidates raonables: el trajecte més curt primer, el que comença abans, el que té menys conflictes... Totes sonen bé i totes fallen en algun cas. La correcta és menys intuïtiva: el que acaba abans.
def assigna_trajectes(trajectes):
"""Màxim nre. de trajectes sense solapament per a un autobús (greedy).
Retorna la llista de trajectes assignats."""
# Funció de selecció: ordenar per hora de FI ascendent
ordenats = sorted(trajectes, key=lambda t: t[2])
assignats = []
fi_ultim = 0 # hora a la qual queda lliure l'autobús
for nom, inici, fi in ordenats:
if inici >= fi_ultim: # test de factibilitat: no se solapa
assignats.append((nom, inici, fi)) # decisió irrevocable
fi_ultim = fi
return assignats
for t in assigna_trajectes(trajectes):
print(t[0])
# Plaça Major -> Estació Nord (09:00-10:00)
# Hospital -> Parc del Riu (10:00-11:00)
# Plaça Major -> Hospital (11:00-12:00)Explicació línia a línia:
sorted(..., key=lambda t: t[2]): materialitza la funció de selecció. Cost O(n log n), que dominarà el total (02-01).fi_ultim: l'estat mínim que necessitem recordar — quan queda lliure l'autobús. Fixa't en l'ús d'una sola variable: espai auxiliar O(1) més la sortida (02-02).if inici >= fi_ultim: el test de factibilitat. Un trajecte és compatible si comença quan l'autobús ja està lliure.- L'
appendés la decisió irrevocable: mai no reconsiderem un trajecte acceptat ni recuperem un de rebutjat. - El bucle recorre els candidats una vegada: O(n). Total: O(n log n), davant del cost exponencial de provar tots els subconjunts de trajectes.
Resultat: 3 trajectes. Cap combinació no n'aconsegueix 4 — i això no és casualitat, com argumentarem tot seguit.
Per què funciona: una demostració informal
L'argument clàssic s'anomena argument d'intercanvi (exchange argument) i convé interioritzar-lo, perquè és l'eina estàndard per justificar un greedy:
- Sigui
gel primer trajecte que tria el greedy: el que acaba abans de tots. - Pren qualsevol solució òptima
OPT. SiOPTno contég, mira el primer trajecte d'OPT(anomenem-lox). Com quegacaba abans o igual que qualsevol trajecte — també abans quex—, podem substituirxpergdins d'OPTsense crear solapaments:gdeixa lliure l'autobús fins i tot abans quex, així que tot el que cabia després dexcontinua cabent després deg. - La solució modificada té la mateixa mida que
OPT, per tant continua sent òptima i comença com el greedy. - Eliminat
gi els trajectes incompatibles amb ell, queda un subproblema idèntic però més petit ("màxim de trajectes que comencen després defi_ultim"), al qual s'aplica el mateix raonament una vegada i una altra.
Conclusió: l'elecció voraç mai no ens allunya d'un òptim. Fixa't en la lògica: no diem que greedy sigui l'única solució òptima, sinó que sempre existeix un òptim que pren la decisió voraç — amb això n'hi ha prou.
Aquest argument explica també per què "el que acaba abans" és la clau: acabar aviat deixa el màxim marge possible al futur. Els criteris rivals no tenen aquesta propietat: el trajecte més curt pot estar "travessat" bloquejant dos de llargs compatibles, i el que comença abans pot ser llarguíssim i menjar-se el matí sencer.
Quan greedy falla: dos contraexemples
Contraexemple 1: el canvi de monedes no canònic
Els bitllets de recàrrega de la targeta RutaBus valen 1, 3 i 4 euros (un sistema deliberadament exòtic). Volem abonar una quantitat amb el mínim nombre de bitllets. El greedy natural — "pren sempre el bitllet més gros que hi càpiga" — falla:
def canvi_greedy(quantitat, valors=(4, 3, 1)):
usats = []
for v in valors: # de més gran a més petit
while quantitat >= v:
usats.append(v)
quantitat -= v
return usats
print(canvi_greedy(6)) # [4, 1, 1] -> 3 bitllets
# Òptim real: [3, 3] -> 2 bitlletsPer a 6 €, el greedy pren el 4 (localment òptim) i es condemna a completar amb dues monedes d'1. La decisió irrevocable d'agafar el 4 destrueix la possibilitat de fer servir 3+3. Amb el sistema de l'euro (1, 2, 5, 10, 20, 50...) el greedy sí que és òptim — es diu que el sistema és canònic —, cosa que il·lustra una veritat incòmoda: el mateix algorisme és exacte o incorrecte segons les dades del problema. Per això un greedy sense argument d'optimalitat és una aposta. (La versió general del canvi de monedes es resol amb garanties amb programació dinàmica, l'estratègia de 03-03.)
Contraexemple 2: retrobament amb recorregut_supervisor
A 01-02 vam escriure recorregut_supervisor: el supervisor visita totes les parades anant sempre a la més propera no visitada. Ara tenim vocabulari per diagnosticar-lo: és un greedy de manual — candidats: parades no visitades; selecció: mínima distancia; factibilitat: no repetir parada. I ja vam dir llavors que és una heurística: dona rutes raonables, no la ruta més curta.
Veure'l fallar és fàcil amb parades en línia recta (posicions en km): el supervisor surt del km 0 i ha de visitar les parades dels km 1, 2 i −1,5.
- Greedy (veí més proper): la més propera al 0 és la del km 1 → després la del 2 (a 1 km) → i finalment la del −1,5 (a 3,5 km). Total: 1 + 1 + 3,5 = 5,5 km.
- Òptim: anar primer "a contracorrent": 0 → −1,5 (1,5 km) → 1 (2,5 km) → 2 (1 km). Total: 5 km.
El greedy perd per anar primer al més còmode i deixar per al final el desplaçament llarg de tornada: cada elecció local va ser impecable i el total, subòptim.
La moralitat és doble. Primera: greedy falla quan una decisió còmoda ara crea un sobrecost inevitable després. Segona: un greedy no òptim no és cap escombraria — com a heurística, recorregut_supervisor dona solucions decents en O(n²) per a un problema (el del viatjant) la solució exacta del qual és exponencial. Saber que no és òptim, i decidir si ens serveix, és exactament la mena de judici professional que aquest curs entrena.
Com reconèixer si un problema admet greedy
No hi ha recepta infal·lible, però sí dues propietats que els problemes "greedy-compatibles" exhibeixen (les enunciem de manera informal; el seu tractament rigorós pertany a textos avançats):
- Propietat d'elecció voraç: existeix una elecció localment òptima que forma part d'alguna solució global òptima — és a dir, decidir ja, sense mirar el futur, no tanca la porta a l'òptim. Es comprova típicament amb un argument d'intercanvi com el de la secció anterior.
- Subestructura òptima: després de prendre la decisió voraç, el que queda és un subproblema del mateix tipus la solució òptima del qual, unida a la decisió presa, dona l'òptim global. (Aquesta propietat reapareixerà a 03-03: la programació dinàmica també l'exigeix. La diferència: la PD explora diverses decisions possibles i el greedy s'ho juga tot a una.)
Llista de comprovació pràctica abans de confiar en un greedy:
- Puc formular una funció de selecció clara?
- Tinc un argument d'intercanvi, encara que sigui informal, que l'elecció voraç no descarta l'òptim?
- He buscat activament contraexemples petits (3-6 elements, casos extrems)?
- Si no aconsegueixo 2 i 3: em serveix com a heurística, o el problema exigeix l'òptim exacte (→ PD a 03-03 o backtracking a 03-04)?
Algorismes greedy cèlebres
| Algorisme | Problema | Òptim? | On |
|---|---|---|---|
| Dijkstra | Camí més curt des d'un origen en un graf amb pesos no negatius | Sí (greedy amb demostració) | 04-05 |
| Kruskal / Prim | Arbre d'expansió mínima (connectar totes les parades amb el mínim cable/carretera) | Sí | (només menció) |
| Huffman | Codis de compressió de longitud mínima | Sí | (només menció) |
| Selecció d'activitats | Màxim de tasques sense solapament | Sí (vist avui) | 03-02 |
| Veí més proper | Ruta que visita totes les parades (viatjant) | No: heurística | 01-02 / avui |
| Canvi de monedes | Mínim nre. de monedes | Només en sistemes canònics | avui |
Dijkstra mereix una nota: és la prova que "greedy" no vol dir "aproximat". Triar sempre la parada no processada més propera a l'origen és òptim per a camins mínims — ho demostrarem i implementarem a 04-05, quan la xarxa de RutaBus sigui per fi un graf amb pesos.
Errors Comuns i Consells
- Assumir optimalitat sense argument. "Sona raonable" no és una demostració. El canvi de monedes amb (1, 3, 4) sona igual de raonable i falla. Exigeix sempre un argument d'intercanvi o contraexemples esgotats.
- Triar la funció de selecció equivocada. En el problema de trajectes, tres criteris plausibles fallen i només "acaba abans" funciona. Prova cada criteri candidat contra exemples petits dissenyats amb mala idea.
- Oblidar l'ordenació en l'anàlisi. El bucle greedy és O(n), però el
sortedprevi el fa O(n log n). No anunciïs costos que el teu propi codi desmenteix (02-01). - Confondre "greedy falla" amb "greedy inútil". Una heurística voraç amb garanties empíriques pot ser la millor opció d'enginyeria quan l'exacte és exponencial. Documenta que és heurística i llestos.
- Modificar la solució ja construïda. Si et descobreixes "traient" elements acceptats, ja no estàs escrivint un greedy: estàs improvisant un backtracking sense control. Replanteja l'estratègia (03-04).
Exercicis
Exercici 1
RutaBus vol instal·lar el mínim nombre de punts de recàrrega en una avinguda de manera que tota parada tingui un punt a menys de 500 m. Les posicions de les parades (en metres) són en una llista ordenada, p. ex. [100, 300, 850, 900, 1800]. Dissenya un greedy: indica candidats, funció de selecció i test de factibilitat, i implementa'l. Pista: processa les parades d'esquerra a dreta i col·loca cada punt tan a la dreta com sigui possible.
Exercici 2
Per al problema dels trajectes, demostra amb un contraexemple concret (3-4 trajectes amb hores) que la funció de selecció "el trajecte més curt primer" no és òptima.
Exercici 3
L'autobús de reforç té un dipòsit per a 300 km. A la ruta hi ha benzineres als km [90, 150, 230, 350, 480] i la ruta fa 520 km. Escriu un greedy que calculi el mínim nombre de repostatges (sortint amb el dipòsit ple) i raona informalment per què la seva elecció voraç és segura.
Solucions
Solució 1
- Candidats: posicions possibles de punts de recàrrega. Selecció: per a la parada descoberta més a l'esquerra, col·locar el punt a
parada + 500(tan a la dreta com sigui possible sense deixar-la de cobrir). Factibilitat: cada parada ha de quedar a ≤ 500 m d'algun punt.
def punts_recarrega(parades_m, radi=500):
punts = []
i, n = 0, len(parades_m)
while i < n:
punt = parades_m[i] + radi # tan a la dreta com sigui possible
punts.append(punt)
# saltar totes les parades que aquest punt ja cobreix
while i < n and parades_m[i] <= punt + radi:
i += 1
return punts
print(punts_recarrega([100, 300, 850, 900, 1800])) # [600, 2300]Amb [100, 300, 850, 900, 1800]: el punt a 600 cobreix 100, 300, 850 i 900 (totes a ≤ 500 de 600... 100 és a 500 justos, dins); el punt a 2300 cobreix 1800. Total: 2 punts. Argument d'intercanvi: qualsevol solució òptima necessita algun punt que cobreixi la primera parada; moure'l fins a parades[0] + 500 no descobreix ningú de l'esquerra (no hi ha ningú) i només pot cobrir més parades a la dreta. Cost: O(n).
Solució 2
Trajectes: A = (10:00-12:00), B = (11:45-12:15), C = (12:10-14:00). El més curt és B (30 min); el greedy "curt primer" el tria, i B se solapa tant amb A (11:45 < 12:00) com amb C (12:10 < 12:15): queden tots dos descartats i el total és 1 trajecte. Tanmateix, A i C són compatibles entre si (A acaba a les 12:00 ≤ 12:10): l'òptim és 2. B està "travessat" bloquejant dos trajectes compatibles — exactament el defecte que vam anticipar a la lliçó. Comprova que el criteri correcte, "acaba abans", tria A i després C i assoleix l'òptim.
Solució 3
Selecció voraç: no repostar fins que sigui imprescindible i, llavors, fer-ho a la benzinera assolible més llunyana que hàgim deixat enrere.
def repostatges_minims(benzineres, total_km, autonomia=300):
parades = [] # on repostem
posicio = 0 # últim repostatge (o sortida)
punts = benzineres + [total_km] # la destinació tanca la ruta
for punt in punts:
if punt - posicio > autonomia: # no arribo a 'punt' des de 'posicio'
# imprescindible repostar: a l'última benzinera assolible
assolibles = [g for g in benzineres
if posicio < g <= posicio + autonomia]
if not assolibles:
return None # forat més gran que l'autonomia
posicio = max(assolibles) # la més llunyana assolible
parades.append(posicio)
if punt - posicio > autonomia:
return None # ni repostant s'hi arriba
return parades
print(repostatges_minims([90, 150, 230, 350, 480], 520)) # [230]Traça amb autonomia 300: des del km 0 s'arriba fins al 300, així que 90, 150 i 230 es passen de llarg; el 350 ja no s'assoleix → repostatge imprescindible, i de les assolibles (90, 150, 230) es tria la més llunyana: 230. Des de 230 s'arriba al km 530 ≥ 520, així que s'arriba a la destinació passant de llarg 350 i 480. Resultat: 1 repostatge. Argument d'intercanvi: quan repostar és inevitable, qualsevol pla òptim reposta en alguna benzinera assolible; substituir-la per la més llunyana no ens pot deixar tirats (també era assolible) i deixa el dipòsit ple més endavant, cobrint un tram posterior igual o més gran. L'elecció voraç mai no allunya de l'òptim. (La cerca d'assolibles dins del bucle és O(n); amb dos índexs es deixa en O(n) total — bon refinament opcional.)
Conclusió
Els algorismes greedy construeixen la solució amb decisions locals i irrevocables: candidats + funció de selecció + test de factibilitat, gairebé sempre amb una ordenació O(n log n) com a únic cost rellevant. Hem vist l'estratègia brillar en l'assignació de trajectes — amb el seu argument d'intercanvi: triar el que acaba abans mai no tanca la porta a l'òptim — i fracassar en el canvi de monedes (1, 3, 4) i en el nostre vell conegut recorregut_supervisor, que avui hem rebatejat com el que sempre va ser: un greedy heurístic. La lliçó de fons: greedy exigeix demostrar (o almenys argumentar i buscar contraexemples), perquè la seva fallada és silenciosa. I què fem quan el problema té subestructura òptima però l'elecció voraç falla — quan 6 € es paguen millor amb 3+3 que començant pel 4? Necessitem una estratègia que, en lloc d'apostar-ho tot a una decisió, explori totes les decisions possibles reutilitzant els càlculs repetits — la idea de gastar memòria per no repetir feina que vam deixar apuntada a 02-02 amb construeix_index. Aquesta estratègia té nom propi i és la protagonista de la propera lliçó: la programació dinàmica.
Curs d'Anàlisi i Disseny d'Algorismes
Mòdul 1: Introducció als Algorismes
Mòdul 2: Anàlisi d'Algorismes
- Anàlisi de Complexitat Temporal
- Anàlisi de Complexitat Espacial
- Casos de Complexitat: Millor, Pitjor i Mitjà
Mòdul 3: Estratègies de Disseny d'Algorismes
Mòdul 4: Algorismes Clàssics
- Cerca Binària
- Ordenació per Inserció
- Ordenació per Mescla (Merge Sort)
- Ordenació Ràpida (Quick Sort)
- Algorisme de Dijkstra
- Algorisme de Floyd-Warshall
