La web de RutaBus vol publicar la taula "minuts mínims entre qualsevol parell de parades": el problema tots-amb-tots (all-pairs shortest paths). Dijkstra (04-05) el resol per força de repetició — un origen cada vegada —, però existeix un algorisme que ataca la matriu completa d'una sola peça, tolera pesos negatius i cap en cinc línies: Floyd-Warshall. És la promesa que vam deixar signada a 03-03: programació dinàmica sobre grafs — un subproblema enginyós ("camí mínim usant només les primeres k parades com a intermèdies"), una taula que s'omple, i la solució que emergeix en completar-la. En aquesta lliçó l'implementarem i traçarem matriu a matriu, aprendrem a detectar cicles negatius mirant una diagonal i a reconstruir camins amb una segona matriu; i, com que és la darrera parada del mòdul, tancarem amb el mapa complet dels sis clàssics i el pont cap a l'optimització.
Contingut
- El problema tots-amb-tots: repetir Dijkstra o alguna cosa millor?
- La idea de programació dinàmica: intermèdies permeses
- Representació: de diccionari a matriu d'adjacència
- Implementació: tres bucles i una desigualtat
- Traça matriu a matriu sobre una xarxa de 4 parades
- Cicles negatius: la diagonal delatora
- Complexitat i reconstrucció de camins
- Tancament del mòdul: els sis clàssics en una taula
El problema tots-amb-tots: repetir Dijkstra o alguna cosa millor?
L'opció òbvia: executar dijkstra(xarxa, origen) amb cadascuna de les V parades com a origen. És perfectament legítima — i de vegades la millor. Comparem costos (V parades, E trams):
| Dijkstra × V | Floyd-Warshall | |
|---|---|---|
| Temps | O(V · (V+E) · log V) | O(V³) |
| En graf dispers (E ≈ V) | ≈ O(V² log V) ← guanya | O(V³) |
| En graf dens (E ≈ V²) | ≈ O(V³ log V) | O(V³) ← guanya |
| Pesos negatius? | No (04-05) | Sí (sense cicles negatius) |
| Codi | Heap, obsoletes, V execucions | Tres bucles i un if |
| Espai | O(V²) per guardar el resultat | O(V²) |
Lectura pràctica: per a la xarxa urbana de RutaBus (dispersa, pesos positius), Dijkstra repetit és asimptòticament millor. Floyd-Warshall guanya quan el graf és dens, quan hi ha pesos negatius (on Dijkstra ni juga), o quan V és moderat (unes 400 parades → 400³ = 6,4·10⁷ operacions simplíssimes: mil·lisegons) i es valora un codi a prova de bugs. Com sempre des de 02-03: no hi ha campió, hi ha context.
La idea de programació dinàmica: intermèdies permeses
Aplicar programació dinàmica (03-03) exigeix trobar el subproblema correcte. El de Floyd-Warshall és una idea brillant. Numerem les parades 0, 1, ..., V−1 i definim:
D_k[i][j] = cost del camí més curt d'
iajusant com a parades intermèdies únicament les de la llista {0, 1, ..., k−1}.
- Cas base, D_0: sense intermèdies permeses, només valen els trams directes — la matriu d'adjacència tal qual.
- Pas recursiu: en autoritzar una intermèdia nova, la parada k, cada camí i→j té dues opcions exhaustives i excloents: o no usa k (i llavors el seu cost continua sent D_k[i][j]) o usa k exactament una vegada — va d'i a k i de k a j, tots dos trams sense usar k com a intermèdia, és a dir, D_k[i][k] + D_k[k][j]. El mínim de totes dues:
- Resposta final: D_V, amb totes les parades autoritzades com a intermèdies: el camí mínim sense restriccions.
És la signatura de la programació dinàmica de 03-03, punt per punt: subproblemes solapats (els D_k[i][k] es reutilitzen en tota la fila d'actualitzacions), subestructura òptima (el millor camí via k es compon de dos millors camins parcials), i tabulació de baix a dalt (de D_0 a D_V). On cost_minim_tab avançava parada a parada per la L2, aquí avancem permís a permís: cada iteració de k no explora camins, autoritza un punt de pas i deixa que la taula es recombini.
Representació: de diccionari a matriu d'adjacència
El diccionari d'adjacència de 03-04/04-05 és ideal per recórrer veïnes; Floyd-Warshall, en canvi, consulta i actualitza parells (i, j) arbitraris sense parar, així que li convé la matriu d'adjacència: una taula V×V on la cel·la [i][j] guarda el pes del tram directe i→j (∞ si no existeix, 0 a la diagonal).
| Diccionari d'adjacència (04-05) | Matriu d'adjacència (aquí) | |
|---|---|---|
| Espai | O(V + E): només el que existeix | O(V²): totes les cel·les, existeixin o no |
| Hi ha tram i→j? | Cercar a xarxa[i] |
O(1): llegir la cel·la |
| Recórrer veïnes d'i | O(grau): directe | O(V): escombrar la fila sencera |
| Ideal per a | Grafs dispersos, Dijkstra, BFS | Grafs densos, algorismes "tots els parells" |
Prenem la subxarxa de 4 parades de 03-04 amb els minuts de 04-05 — índexs 0 = Plaça Major, 1 = Estació Nord, 2 = Hospital Central, 3 = Parc del Riu:
INF = float("inf")
# PM EN HC PR
D = [
[ 0, 4, INF, 2], # Plaça Major
[ 4, 0, 5, INF], # Estació Nord
[ INF, 5, 0, 8], # Hospital Central
[ 2, INF, 8, 0], # Parc del Riu
]La matriu és simètrica perquè el graf és no dirigit; amb trams de sentit únic deixaria de ser-ho i res més no canviaria.
Implementació: tres bucles i una desigualtat
def floyd_warshall(D):
"""Rep la matriu d'adjacència; retorna la matriu de distàncies mínimes."""
n = len(D)
dist = [fila[:] for fila in D] # còpia: no trepitgem l'entrada
for k in range(n): # intermèdia que s'AUTORITZA
for i in range(n): # origen
for j in range(n): # destí
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j] # millor via k
return distL'actualització interior és la relaxació de 04-05 amb un altre vestit: "anar d'i a j passant per k millora el que tinc?". Detalls importants:
- L'ordre dels bucles no és negociable: k ha de ser l'extern. Cada volta de k completa la transició D_k → D_{k+1} per a tota la matriu; posar k dins trenca la definició del subproblema i produeix distàncies incorrectes (és el bug clàssic d'aquest algorisme).
- I no caldria guardar D_k i D_{k+1} en matrius separades? Sorprenentment no: a la volta k, les cel·les
dist[i][k]idist[k][j]no canvien (la millora via k d'un camí que acaba o comença en k passaria dues vegades per k, i això mai no millora sense cicles negatius), així que actualitzar sobre la mateixa matriu és segur. Estalvi d'O(V²) a cost zero — un regal que la tabulació de 03-03 no sempre fa. - La còpia inicial (
[fila[:] for fila in D]) evita el clàssic àlies de llistes imbricades:dist = Dno copiaria res, idist = D[:]copiaria la llista externa però compartiria les files.
Traça matriu a matriu sobre una xarxa de 4 parades
Executem sobre la matriu anterior, mostrant la matriu després de cada valor de k. Canvis en negreta.
k = 0 (s'autoritza la Plaça Major com a intermèdia). Quins parells milloren passant per PM? EN↔PR: 4 + 2 = 6 < ∞. Res més (els altres ja tenen directe millor o involucren PM com a extrem):
| D₁ | PM | EN | HC | PR |
|---|---|---|---|---|
| PM | 0 | 4 | ∞ | 2 |
| EN | 4 | 0 | 5 | 6 |
| HC | ∞ | 5 | 0 | 8 |
| PR | 2 | 6 | 8 | 0 |
k = 1 (s'autoritza l'Estació Nord). PM↔HC: 4 + 5 = 9 < ∞ ✔. PR↔HC via EN: 6 + 5 = 11, no millora el directe 8 ✘:
| D₂ | PM | EN | HC | PR |
|---|---|---|---|---|
| PM | 0 | 4 | 9 | 2 |
| EN | 4 | 0 | 5 | 6 |
| HC | 9 | 5 | 0 | 8 |
| PR | 2 | 6 | 8 | 0 |
k = 2 (Hospital Central) i k = 3 (Parc del Riu). Es comproven totes les cel·les i... res no millora: per exemple PM→PR via HC costaria 9 + 8 = 17 contra el 2 directe, i PM→HC via PR costaria 2 + 8 = 10 contra el 9 vigent. D₄ = D₂: és la matriu final.
Verificació creuada: la fila de la Plaça Major — 0, 4, 9, 2 — coincideix exactament amb el que Dijkstra va calcular des de la Plaça Major a 04-05. Dos algorismes, dues estratègies mare, una mateixa veritat; executar-los tots dos i comparar és, de passada, una tècnica de testeig excel·lent.
Observa què ha passat a la traça: el camí PM→HC = 9 (que a Dijkstra va requerir descobrir 10 i corregir a 9) aquí simplement va aparèixer quan es va autoritzar la intermèdia adequada (EN, a k = 1). Floyd-Warshall no explora camins: deixa que la taula els compongui.
Cicles negatius: la diagonal delatora
Floyd-Warshall admet pesos negatius — la relaxació no "consolida" res irrevocablement, així que l'argument que tombava Dijkstra (04-05) no l'afecta. El seu límit és un altre: els cicles de cost total negatiu. Si existeix un cicle la suma de pesos del qual és < 0, fer-hi voltes redueix el cost indefinidament i "el camí més curt" deixa d'existir (tendeix a −∞).
L'elegant és com es detecta: en acabar, n'hi ha prou de mirar la diagonal. dist[i][i] comença en 0 (anar d'i a i sense moure's); si l'algorisme troba dist[i][i] < 0, és que hi ha un camí d'i a i amb cost negatiu — un cicle negatiu assolible des d'i:
A RutaBus els minuts no són negatius i això no aplica; però si els pesos modelessin cost econòmic amb subvencions per tram, un cicle negatiu significaria "ruta circular que genera diners infinits" — senyal inequívoc de dades corruptes o d'un model mal plantejat, i aquesta comprovació d'una línia el delata.
Complexitat i reconstrucció de camins
Temps: Θ(V³). Tres bucles imbricats de V voltes amb feina O(1) dins — l'anàlisi línia a línia de 02-01 en la seva forma més pura. Sense millors ni pitjors casos: l'estructura del graf no canvia el recompte (com a màxim canvia quants if disparen l'assignació). Per a la taula web amb V = 200 parades: 8·10⁶ operacions, trivial. Amb V = 100.000... 10¹⁵: mai. El cub posa la frontera en "milers de nodes, no centenars de milers" — la jerarquia de 01-03 manant una altra vegada.
Espai: Θ(V²). La matriu mateixa (gràcies a l'actualització in situ, no hi ha còpia per iteració). És també la mida de la sortida — la taula tots-amb-tots que RutaBus volia publicar —, així que en aquest problema l'espai quadràtic és irreductible.
Reconstruir els camins. Com a Dijkstra (i com a la motxilla de 03-03), el "quant" no basta: cal el "per on". La molla de pa aquí és una matriu de següents: sig[i][j] = primera parada cap a la qual moure's en el camí òptim d'i a j. S'inicialitza amb sig[i][j] = j per a cada tram directe, i cada vegada que la relaxació millora un camí via k, el primer pas cap a j passa a ser el primer pas cap a k:
def floyd_warshall_amb_camins(D):
n = len(D)
dist = [fila[:] for fila in D]
sig = [[j if D[i][j] != INF else None for j in range(n)] for i in range(n)]
for k in range(n):
for i in range(n):
for j in range(n):
if dist[i][k] + dist[k][j] < dist[i][j]:
dist[i][j] = dist[i][k] + dist[k][j]
sig[i][j] = sig[i][k] # anar cap a j comença anant cap a k
return dist, sig
def cami(sig, i, j):
if sig[i][j] is None:
return [] # no hi ha camí
ruta = [i]
while i != j:
i = sig[i][j] # saltar a la següent parada
ruta.append(i)
return ruta
# cami(sig, 0, 2) → [0, 1, 2]: Plaça Major → Estació Nord → Hospital CentralDavant del diccionari previa de Dijkstra (una molla per parada, cap enrere), aquí la matriu guarda una molla per parell i es recorre cap endavant. O(V²) molles per a O(V²) camins: proporcionat.
Tancament del mòdul: els sis clàssics en una taula
Sis lliçons, sis algorismes, i cap peça solta: cada clàssic és l'encarnació d'una estratègia del Mòdul 3, analitzada amb les eines del Mòdul 2.
| Algorisme | Estratègia mare | Temps (pitjor / mitjà) | Espai aux. | Quan usar-lo a RutaBus |
|---|---|---|---|---|
| Cerca binària (04-01) | Divideix i venceràs (descarta meitat) | O(log n) | O(1) | Cercar al catàleg ordenat de parades |
| Inserció (04-02) | Incremental (+ millor cas greedy de l'ordre) | O(n²) / O(n²); O(n) si gairebé ordenat | O(1) | Panells petits o gairebé ordenats; flux online |
| Merge sort (04-03) | Divideix i venceràs (feina en combinar) | O(n log n) garantit | O(n) | Històrics enormes, ordenació externa, estabilitat |
| Quick sort (04-04) | Divideix i venceràs (feina en dividir) | O(n²) / O(n log n) | O(log n) | Ordenació general en memòria (amb pivot domat) |
| Dijkstra (04-05) | Greedy (el que sí que és òptim) | O((V+E) log V) | O(V+E) | Temps des d'una parada; xarxa dispersa, pesos ≥ 0 |
| Floyd-Warshall (04-06) | Programació dinàmica | Θ(V³) | O(V²) | Matriu completa de temps; tolera pesos negatius |
(El quart pal de 03-04, backtracking, no hi té representant: el seu territori són els problemes sense estructura explotable, on no va néixer cap "clàssic eficient" — aquesta absència també és una lliçó.)
Errors Comuns i Consells
- Posar k com a bucle intern. L'error canònic: el codi corre, retorna una matriu amb bona pinta... amb distàncies incorrectes (subestima rutes que necessiten intermèdies "futures"). k autoritza intermèdies i ha de ser el bucle extern. Test ràpid: a la nostra xarxa, si PM→HC no surt 9, els bucles estan malament.
- Inicialitzar malament la matriu: oblidar els 0 de la diagonal, o usar un "infinit" fals com
999999que en sumar-se dues vegades desborda la lògica de comparació amb pesos grans.float("inf")suma i compara correctament (inf + 5 == inf); usa'l. I si hi ha trams duplicats a les dades, la cel·la s'ha de quedar amb el mínim. - Copiar la matriu amb àlies:
dist = Dodist = D[:]comparteixen les files (llistes imbricades), i l'algorisme corromprà la matriu d'entrada. La còpia correcta és per files:[fila[:] for fila in D]— cost ocult de Python que ja coneixem de 02-01. - Usar-lo per comoditat en grafs enormes i dispersos: V³ amb V = 50.000 són 1,25·10¹⁴ operacions. Per a la xarxa nacional de parades, Dijkstra des de cada origen (o només des dels que es consulten) és l'opció; Floyd-Warshall és per a V moderat o dens. La taula de l'apartat 1 és la xuleta.
- Consell: verifica el teu Floyd-Warshall contrastant una fila de la seva matriu amb un Dijkstra des d'aquella parada (si els pesos són ≥ 0). Dues implementacions independents que coincideixen valen més que vint lectures del propi codi.
Exercicis
Exercici 1
Afegeix a la matriu de 4 parades un tram de sentit únic Hospital Central → Plaça Major de 3 minuts (la matriu deixa de ser simètrica: només canvia D[2][0] = 3). Executa la traça k = 0..3 i dona la matriu final. Quin parell de parades millora gràcies al tram nou i per què el parell invers no?
Exercici 2
Amb la matriu dist i la matriu sig de l'apartat 7 sobre la xarxa original de 4 parades: reconstrueix pas a pas cami(sig, 3, 1) (Parc del Riu → Estació Nord), indicant el valor de sig consultat a cada salt i verificant que els minuts sumen dist[3][1].
Exercici 3
RutaBus estudia dos desplegaments: (a) la xarxa urbana amb V = 300 parades i E ≈ 900 trams, matriu completa recalculada cada nit; (b) la xarxa metropolitana amb V = 20.000 i E ≈ 60.000, on només l'1 % dels parells es consulta realment. Per a cada cas, tria entre Floyd-Warshall i Dijkstra-repetit i justifica-ho amb números aproximats (usa log₂ V ≈ 8 per a 300 i ≈ 14 per a 20.000).
Solucions
Solució 1
Només canvia la cel·la D[2][0] = 3 (HC→PM); D[0][2] continua sent ∞ fins que les intermèdies facin la seva feina. Traça de canvis: amb k = 0 (PM): EN→PR = 6 i PR→EN = 6 com a la xarxa original; HC→EN via PM = 3+4 = 7 no millora el 5 directe ✘; però HC→PR via PM = 3+2 = 5 millora el 8 directe ✔. Amb k = 1 (EN): PM→HC = 4+5 = 9 ✔; en canvi PR→HC via EN = 6+5 = 11 no millora el seu 8 directe ✘ (compte amb l'asimetria: HC→PR sí que va baixar a 5, però PR→HC continua en 8 — són cel·les diferents). Amb k = 2 (HC): EN→PM via HC = 5+3 = 8 ✘ (4 directe), PR→PM via HC = 8+3 = 11 ✘ (2 directe). k = 3: sense canvis. Matriu final:
| PM | EN | HC | PR | |
|---|---|---|---|---|
| PM | 0 | 4 | 9 | 2 |
| EN | 4 | 0 | 5 | 6 |
| HC | 3 | 5 | 0 | 5 |
| PR | 2 | 6 | 8 | 0 |
Millora HC→PR (8 → 5, via PM gràcies a la drecera HC→PM). L'invers PR→HC es queda en 8: el tram nou és de sentit únic i només ajuda els camins que van cap a la Plaça Major. En grafs dirigits, cada sentit viu la seva vida.
Solució 2
Sobre la xarxa original, dist[3][1] = 6 (PR→EN via PM). Inicialment sig[3][1] = None... fins que a k = 0 la millora via PM assigna sig[3][1] = sig[3][0] = 0. Reconstrucció de cami(sig, 3, 1): ruta = [3]; consulta sig[3][1] = 0 → salta a PM, ruta = [3, 0]; consulta sig[0][1] = 1 (tram directe) → salta a EN, ruta = [3, 0, 1]. Camí: Parc del Riu → Plaça Major → Estació Nord; minuts: 2 + 4 = 6 = dist[3][1] ✔.
Solució 3
(a) V = 300: Floyd-Warshall costa 300³ = 2,7·10⁷ operacions elementals — dècimes de segon, codi mínim, i la sortida (la matriu de 9·10⁴ cel·les) és exactament el que es vol publicar. Dijkstra × 300 ≈ 300 · (300+900) · 8 ≈ 2,9·10⁶ — també trivial i una mica menor, però amb més codi i sense avantatge material. Qualsevol dels dos serveix; Floyd-Warshall és defensable per simplicitat. (b) V = 20.000: Floyd-Warshall costaria 20.000³ = 8·10¹² operacions (hores o dies) i 4·10⁸ cel·les de matriu (diversos GB): inviable. Dijkstra des d'un origen ≈ (20.000 + 60.000) · 14 ≈ 1,1·10⁶; fins i tot llançant-lo des dels 200 orígens consultats (l'1 %), ≈ 2,2·10⁸: perfectament assumible, i calculant només el que es consulta. Dijkstra sota demanda, sense discussió. Moralitat: el tots-amb-tots només es materialitza quan de debò es necessita tot.
Conclusió
Floyd-Warshall corona el mòdul demostrant que la programació dinàmica de 03-03 escala a grafs: el subproblema "camí mínim usant només les primeres k parades com a intermèdies" converteix el tots-amb-tots en una taula V×V que tres bucles omplen en Θ(V³), amb actualització in situ, detecció de cicles negatius a la diagonal i una matriu de següents per reconstruir qualsevol itinerari — tot plegat tolerant els pesos negatius que a Dijkstra li estaven vedats, a canvi d'un cub que en limita l'ús a xarxes de mida moderada o denses. Amb ell es completa el repertori: cerca binària i les dues grans ordenacions com a filles de divideix i venceràs, Dijkstra com el greedy demostradament òptim i Floyd-Warshall com la programació dinàmica feta matriu — sis clàssics que ja sabem implementar, traçar, analitzar per casos i, sobretot, triar segons el problema de RutaBus que tinguem davant. Ja sabem triar l'algorisme correcte; el Mòdul 5 comença on aquesta tria acaba: aprendre a esprémer-ne la implementació — optimitzar el codi (05-01), la memòria (05-02) i repartir la feina entre diversos nuclis (05-03) — perquè entre un algorisme ben triat i un programa ràpid encara hi ha un ofici pel mig, i aquest ofici és el que ve ara.
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
