Ja tenim tota la teoria del mòdul: recorreguts (07-03), camins mínims (07-04) i arbres d'expansió (07-05). Aquesta lliçó no introdueix ni un algorisme nou: els posa a treballar en aplicacions completes i recognoscibles. Primer, el plat fort: el planificador de TaskFlow, que combina ordre topològic, detecció de cicles, paral·lelització per nivells i el càlcul de la ruta crítica del projecte. Després sortirem de TaskFlow per veure dos clàssics: el suggeriment de col·laboradors d'una xarxa social (amics-d'amics amb BFS) i un PageRank en miniatura, l'algorisme que va fer enlairar Google. Tanquem amb un panorama dels grafs en producció. Tot el codi corre sobre la classe Graf de la 07-02.
Contingut
- El planificador de TaskFlow complet
- Validar: detecció de cicles
- Ordenar: ordre topològic
- Paral·lelitzar: tasques per nivells
- Estimar: la ruta crítica del projecte
- Suggeriment de col·laboradors: amics-d'amics
- PageRank simplificat
- Grafs en producció: panorama breu
El planificador de TaskFlow complet
Partim del graf dependencies i de l'índex tasques de la 07-02 (recorda: cada tasca és un dict amb id, titol, prioritat, estat i hores; l'aresta A → B significa "B depèn d'A"). El planificador ha de respondre, en aquest ordre, quatre preguntes: és executable el projecte?, en quin ordre?, què es pot fer en paral·lel?, quantes hores durarà com a mínim?
Validar: detecció de cicles
Res no té sentit si hi ha un cicle, així que el planificador comença reutilitzant hi_ha_cicle (blanc/gris/negre, 07-03) com a guàrdia d'entrada:
def planificar(graf, tasques):
if hi_ha_cicle(graf):
raise ValueError("Dependències circulars: revisa el projecte")
return {
"ordre": ordre_topologic(graf), # 07-03, Kahn
"nivells": nivells_parallels(graf), # nou, a sota
"durada": ruta_critica(graf, tasques), # nou, a sota
}En una aplicació real aquesta validació correria també abans d'acceptar cada nova dependència, que és més barat que deixar que l'error s'acumuli.
Ordenar: ordre topològic
ordre_topologic (Kahn, 07-03) ja ens dóna una seqüència vàlida d'execució per a un sol treballador. Res de nou a afegir — llevat de notar que un ordre en sèrie desaprofita l'equip: si dissenyar_esquema i dissenyar_ui no depenen entre si, per què esperar-se?
Paral·lelitzar: tasques per nivells
La versió per capes de Kahn agrupa les tasques en nivells: el nivell 0 són les tasques sense dependències; el nivell k, les que queden llestes quan acaben els nivells anteriors. Totes les tasques d'un mateix nivell es poden executar en paral·lel:
from collections import deque
def nivells_parallels(graf):
graus = graf.graus_entrada()
nivell_actual = [v for v, g in graus.items() if g == 0]
nivells = []
while nivell_actual:
nivells.append(nivell_actual)
seguent = []
for v in nivell_actual: # "acabem" tot el nivell alhora
for vei in graf.veins(v):
graus[vei] -= 1
if graus[vei] == 0:
seguent.append(vei)
nivell_actual = seguent
return nivells
for i, nivell in enumerate(nivells_parallels(dependencies)):
print(f"Nivell {i}: {nivell}")
# Nivell 0: ['dissenyar_esquema', 'configurar_servidor', 'dissenyar_ui']
# Nivell 1: ['migrar_bd', 'implementar_ui']
# Nivell 2: ['desplegar_api']
# Nivell 3: ['proves_integracio']
# Nivell 4: ['llancament']És el mateix Kahn de la 07-03, però processant la cua per tandes en lloc d'element a element — compara-ho amb el recorregut per nivells del mòdul 6, que feia exactament aquesta maniobra sobre arbres. Lectura de gestió: amb prou gent, el projecte necessita 5 tandes; tres persones poden arrencar el dilluns alhora.
Estimar: la ruta crítica del projecte
Pregunta final: amb paral·lelisme il·limitat, quantes hores com a mínim dura el projecte? No és la suma de totes les hores (hi ha paral·lel), ni el camí mínim de la 07-04. És el camí més llarg del DAG comptant les hores de cada tasca: la ruta crítica. Qualsevol retard en ella endarrereix el projecte sencer; les altres tasques tenen marge.
Camí més llarg? Sona a invertir Dijkstra (negar pesos... només vàlid sense cicles), però en un DAG hi ha una via més neta i sense trampes: ordre topològic + programació dinàmica. Recorrent les tasques en ordre topològic, quan arribem a una tasca ja coneixem el moment de fi de totes les seves dependències, així que:
def ruta_critica(graf, tasques):
fi = {} # id -> hora de finalitzacio mes primerenca possible
pare_critic = {}
for v in ordre_topologic(graf): # garanteix fi(u) ja calculat
millor, culpable = 0, None
for u in graf.vertexs(): # dependències de v: arestes u -> v
if graf.existeix_aresta(u, v) and fi[u] > millor:
millor, culpable = fi[u], u
fi[v] = millor + tasques[v]["hores"]
pare_critic[v] = culpable # la dependència que marca el ritme
ultim = max(fi, key=fi.get) # la tasca que acaba mes tard
cami, actual = [], ultim # reconstruir cap enrere (07-04)
while actual is not None:
cami.append(actual)
actual = pare_critic[actual]
return {"hores": fi[ultim], "ruta": cami[::-1]}
print(ruta_critica(dependencies, tasques))
# {'hores': 17,
# 'ruta': ['dissenyar_ui', 'implementar_ui', 'proves_integracio', 'llancament']}Comprovació a mà dels moments de fi: dissenyar_ui 5 → implementar_ui 5+8=13; per l'altra branca, migrar_bd acaba en 5 i desplegar_api en 7. proves_integracio espera la més lenta: max(13, 7) + 3 = 16, i llancament tanca en 17 hores. La ruta crítica passa per la interfície: accelerar migrar_bd no avançaria el projecte ni un minut (té 6 hores de marge), però cada hora guanyada a implementar_ui és una hora de projecte.
graph LR
A[dissenyar_esquema 3h] --> B[migrar_bd 2h]
S[configurar_servidor 1h] --> C
B --> C[desplegar_api 2h]
C --> D
U[dissenyar_ui 5h] ==> I[implementar_ui 8h]
I ==> D[proves_integracio 3h]
D ==> L[llancament 1h]
style U stroke:#c00,stroke-width:3px
style I stroke:#c00,stroke-width:3px
style D stroke:#c00,stroke-width:3px
style L stroke:#c00,stroke-width:3px
Nota d'eficiència: el doble bucle amb existeix_aresta és O(n · a) per claredat didàctica; amb el graf invertit de la 07-03 (exercici 2) quedaria en O(n + a). Amb això, el planificador és complet: validació, ordre, paral·lelisme i estimació en unes 60 línies sobre estructures que ja teníem.
Suggeriment de col·laboradors: amics-d'amics
Canviem de domini. TaskFlow afegeix un toc social: suggerir col·laboradors a cada usuari. L'heurística clàssica de les xarxes socials: suggerir els que són a distància exactament 2 — col·laboradors dels meus col·laboradors que encara no són meus — ordenats per quants contactes comuns tenim. És un BFS limitat a dues capes sobre un graf no dirigit:
def suggerir_collaboradors(graf, usuari, maxim=3):
directes = set(graf.veins(usuari)) # distància 1
candidats = {} # candidat -> contactes comuns
for amic in directes:
for segon in graf.veins(amic): # distància 2
if segon != usuari and segon not in directes:
candidats[segon] = candidats.get(segon, 0) + 1
return sorted(candidats, key=candidats.get, reverse=True)[:maxim]
social = Graf(dirigit=False)
for a, b in [("anna", "bruno"), ("anna", "carla"), ("bruno", "david"),
("carla", "david"), ("carla", "elena"), ("david", "ferran")]:
social.afegir_aresta(a, b)
print(suggerir_collaboradors(social, "anna")) # ['david', 'elena']A l'anna se li suggereix david abans que elena perquè comparteix amb ell dos contactes (bruno i carla) i amb elena només un. Aquí no va caldre ni la cua: en ser exactament dues capes, dos bucles imbricats són el BFS truncat — però conceptualment és bfs_distancies (exercici 1 de la 07-03) filtrant distància 2. Així funcionen, amb molts refinaments a sobre, els "potser coneixes" de LinkedIn o Facebook.
PageRank simplificat
La web és un graf dirigit (pàgines → enllaços, lliçó 07-01). La pregunta de Google el 1998: quines pàgines són importants? La idea de PageRank: una pàgina és important si l'enllacen pàgines importants — definició circular que es resol per iteració. Model del "navegant aleatori": amb probabilitat d (≈ 0,85) segueix un enllaç a l'atzar de la pàgina actual; amb 1 − d salta a una pàgina qualsevol. El rang d'una pàgina és la fracció de temps que el navegant hi passa:
def pagerank(graf, d=0.85, iteracions=30):
vertexs = graf.vertexs()
n = len(vertexs)
rang = {v: 1 / n for v in vertexs} # arrencada: tots iguals
for _ in range(iteracions):
nou = {v: (1 - d) / n for v in vertexs} # el salt aleatori
for v in vertexs:
sortides = graf.grau_sortida(v)
if sortides == 0: # pagina sense enllacos:
for desti in vertexs: # reparteix entre totes
nou[desti] += d * rang[v] / n
else:
for desti in graf.veins(v): # reparteix el seu rang
nou[desti] += d * rang[v] / sortides
rang = nou
return rang
web = Graf(dirigit=True)
for origen, desti in [("blog", "docs"), ("blog", "home"), ("docs", "home"),
("forum", "home"), ("home", "docs")]:
web.afegir_aresta(origen, desti)
for pagina, r in sorted(pagerank(web).items(), key=lambda x: -x[1]):
print(f"{pagina}: {r:.3f}")
# home: 0.470
# docs: 0.455
# blog: 0.038
# forum: 0.038Lectura del codi: a cada iteració, cada pàgina reparteix el seu rang entre els seus enllaços sortints (d * rang / sortides), i totes reben a més l'engruna del salt aleatori ((1 - d) / n). Després d'unes desenes d'iteracions els valors s'estabilitzen (convergeix a un punt fix). home guanya perquè l'enllacen les altres tres; docs la segueix molt de prop amb un únic enllaç entrant, perquè qui l'enllaça és la importantíssima home — aquí hi ha la circularitat resolta: no compta només quants enllaços reps, sinó de qui. blog i forum, sense enllaços entrants, es queden amb l'engruna del salt aleatori. A TaskFlow, la mateixa idea aplicada al graf de dependències invertit assenyalaria les tasques "estructuralment centrals" del projecte.
Grafs en producció: panorama breu
Per acabar, on viuen els grafs en sistemes reals (sense codi, només mapa):
| Àmbit | Què modela el graf | Què s'hi executa a sobre |
|---|---|---|
| Bases de dades de grafs (Neo4j, Neptune) | Entitats i relacions com a ciutadans de primera | Consultes de patrons i camins (Cypher, Gremlin) |
| Recomanació (Amazon, Netflix, Spotify) | Usuaris ↔ productes, bipartit | Veïnatge, passejades aleatòries, filtratge col·laboratiu |
| CI/CD i build (Make, Gradle, Airflow) | Passos i les seves dependències: un DAG | Ordre topològic i paral·lelització — el nostre planificador, a escala |
| Mapes i logística | Cruïlles i trams ponderats | Dijkstra i variants amb heurístiques (A*) |
| Detecció de frau | Comptes, targetes, dispositius compartits | Components connexes i patrons sospitosos |
La moralitat: el que hem construït aquest mòdul no és un exercici acadèmic; és la versió petita i comprensible de la maquinària que orquestra pipelines, rutes i recomanacions diàriament. Quan l'escala creix, canvien els motors (bases de dades de grafs, processament distribuït), però els conceptes —adjacència, BFS, ordre topològic, camins— són exactament aquests.
Errors Comuns i Consells
- Confondre ruta crítica amb camí mínim. La ruta crítica és el camí més LLARG del DAG: marca la durada inevitable del projecte. Minimitzar (07-04) i maximitzar (aquí) responen preguntes oposades; la programació dinàmica sobre ordre topològic només és vàlida per maximitzar perquè el DAG no té cicles.
- Calcular la ruta crítica sense validar abans el DAG. Amb un cicle,
ordre_topologicllança una excepció (bé); si fessis servir una altra implementació silenciosa, la programació dinàmica llegiria valors no calculats. Valida sempre primer. - Oblidar el cas "sense enllaços sortints" a PageRank. Sense aquest repartiment, el rang dels carrerons sense sortida s'evapora a cada iteració i la suma total deixa de ser 1 — un bug clàssic difícil de notar perquè el rànquing relatiu pot continuar semblant raonable.
- Suggerir amics amb graf dirigit sense voler. Si construeixes la xarxa social amb
dirigit=True(el valor per defecte de la nostra classe), "amics dels meus amics" només mirarà en un sentit. Per a amistats,Graf(dirigit=False)explícit. - Consell: fixa't en el patró d'aquesta lliçó: cap aplicació no va necessitar estructures noves, només compondre les existents. Aquesta composició és l'habilitat que distingeix qui "sap algorismes" de qui resol problemes.
Exercicis
Exercici 1: marge d'una tasca
Amplia ruta_critica per calcular el marge de cada tasca: quantes hores pot endarrerir-se sense afectar la durada total. Pista: calcula també l'instant més tardà de fi permès, recorrent l'ordre topològic al revés des de la durada total; marge = tardà − primerenc.
Exercici 2: col·laboradors a distància 3
Generalitza suggerir_collaboradors a collaboradors_a_distancia(graf, usuari, k) fent servir bfs_distancies (exercici 1 de la 07-03) per retornar els usuaris a distància exactament k.
Exercici 3: la tasca més central
Aplica pagerank al graf dependencies invertit (amb invertir de la 07-03) i raona per què la tasca amb major rang és la que és.
Solucions
Solució 1:
def marges(graf, tasques):
dades = ruta_critica(graf, tasques)
total = dades["hores"]
fi = {}
for v in ordre_topologic(graf):
previ = max((fi[u] for u in graf.vertexs()
if graf.existeix_aresta(u, v)), default=0)
fi[v] = previ + tasques[v]["hores"]
tardana = {}
for v in reversed(ordre_topologic(graf)): # del final cap al principi
successors = [tardana[s] - tasques[s]["hores"] for s in graf.veins(v)]
tardana[v] = min(successors, default=total) # sense successors: la fi del projecte
return {v: tardana[v] - fi[v] for v in fi}
print(marges(dependencies, tasques))
# {'dissenyar_esquema': 6, 'configurar_servidor': 10, 'migrar_bd': 6,
# 'desplegar_api': 6, 'dissenyar_ui': 0, 'implementar_ui': 0,
# 'proves_integracio': 0, 'llancament': 0}Les tasques amb marge 0 són exactament la ruta crítica; configurar_servidor pot endarrerir-se fins a 10 hores sense moure el llançament. Aquesta és l'anàlisi PERT/CPM dels manuals de gestió de projectes, construïda amb les nostres peces.
Solució 2:
def collaboradors_a_distancia(graf, usuari, k):
distancies = bfs_distancies(graf, usuari) # 07-03, exercici 1
return [v for v, d in distancies.items() if d == k]
print(collaboradors_a_distancia(social, "anna", 2)) # ['david', 'elena']
print(collaboradors_a_distancia(social, "anna", 3)) # ['ferran']Tota la lògica ja era a bfs_distancies: l'aplicació és un filtre d'una línia. (Es perd el desempat per contactes comuns; combinar-ho amb el recompte de la versió original queda com a millora opcional.)
Solució 3: En invertir, les fletxes apunten de cada tasca cap a les seves dependències, així que el rang flueix des del final del projecte cap als seus fonaments. Guanya dissenyar_esquema o configurar_servidor... no: recorrent el flux, el rang s'acumula a les tasques de les quals més es depèn transitivament — amb aquest graf, dissenyar_esquema i dissenyar_ui reben el rang que baixa per les seves cadenes, i la més beneficiada és dissenyar_ui, de la qual penja la branca més pesant (tota la UI i, a través de les proves, el llançament). Executar-ho confirma la intuïció: les tasques "arrel" de les cadenes llargues són les estructuralment crítiques, coherent amb la ruta crítica de la secció 1.
Conclusió
Hem vist els grafs guanyar-se el sou: el planificador de TaskFlow (validar amb cicles, ordenar amb Kahn, paral·lelitzar per nivells, estimar amb la ruta crítica — el camí més llarg del DAG per programació dinàmica), els suggeriments socials per distància 2 i un PageRank de joguina que destil·la la idea que va ordenar la web. Res d'això no va requerir algorismes nous: només compondre BFS, DFS, Kahn i Dijkstra amb les estructures dels mòduls anteriors. Queda l'últim pas del mòdul, i és tot teu: una lliçó íntegra d'exercicis per consolidar grafs de cap a cap, del modelatge als algorismes, un altre cop amb TaskFlow com a camp de proves.
Curs d'Estructures de Dades
Mòdul 1: Introducció a les Estructures de Dades
- Què són les Estructures de Dades?
- Importància de les Estructures de Dades en la Programació
- Tipus d'Estructures de Dades
- Complexitat Algorísmica i Notació Big O
- Arrays i Memòria: la Base de les Estructures de Dades
Mòdul 2: Llistes
- Introducció a les Llistes
- Llistes Enllaçades
- Llistes Doblement Enllaçades
- Llistes Circulars
- Exercicis amb Llistes
Mòdul 3: Piles
- Introducció a les Piles
- Operacions Bàsiques amb Piles
- Implementació de Piles
- Aplicacions de les Piles
- Exercicis amb Piles
Mòdul 4: Cues
- Introducció a les Cues
- Operacions Bàsiques amb Cues
- Cues Circulars
- Cues de Prioritat
- Cues Dobles (Deques)
- Exercicis amb Cues
Mòdul 5: Taules Hash i Diccionaris
- Introducció a les Taules Hash
- Funcions Hash i Resolució de Col·lisions
- Diccionaris i Conjunts a la Pràctica
- Exercicis amb Taules Hash
Mòdul 6: Arbres
- Introducció als Arbres
- Arbres Binaris
- Recorreguts d'Arbres
- Arbres Binaris de Cerca
- Arbres AVL
- Arbres B
- Monticles (Heaps)
- Exercicis amb Arbres
Mòdul 7: Grafs
- Introducció als Grafs
- Representació de Grafs
- Algorismes de Cerca en Grafs
- Algorismes de Camins Mínims
- Arbres d'Expansió Mínima
- Aplicacions dels Grafs
- Exercicis amb Grafs
