Toda la teoría del módulo está sobre la mesa: modelado (07-01), la clase Grafo (07-02), BFS/DFS/Kahn (07-03), Dijkstra y compañía (07-04), MST y Union-Find (07-05) y sus aplicaciones (07-06). Esta lección es íntegramente práctica: seis ejercicios progresivos, sin teoría nueva, alrededor de un proyecto real de TaskFlow — el lanzamiento de su app móvil. Intenta resolver cada uno por tu cuenta antes de mirar la solución; el orden importa, porque cada ejercicio reutiliza piezas del anterior. Necesitarás la clase Grafo y las funciones bfs, orden_topologico e invertir de las lecciones anteriores.
Contenido
- El proyecto de partida
- Ejercicio 1: construir y consultar el grafo del proyecto
- Ejercicio 2: el camino con menos pasos entre dos tareas
- Ejercicio 3: detectar y listar el ciclo de dependencias
- Ejercicio 4: orden de ejecución con desempate por prioridad
- Ejercicio 5: islas de tareas (proyectos independientes)
- Ejercicio 6: la rejilla como grafo implícito
- Soluciones
El proyecto de partida
El equipo planifica en TaskFlow el lanzamiento de la app móvil. Estas son las tareas (con su prioridad, 1 = máxima) y sus dependencias, en lenguaje natural:
| Tarea | Prioridad | Depende de |
|---|---|---|
| definir_alcance | 1 | — |
| disenar_pantallas | 2 | definir_alcance |
| elegir_stack | 1 | definir_alcance |
| implementar_login | 2 | disenar_pantallas, elegir_stack |
| implementar_tablero | 1 | disenar_pantallas, elegir_stack |
| conectar_api | 1 | implementar_login, implementar_tablero |
| pruebas_usabilidad | 2 | conectar_api |
| preparar_marketing | 3 | — |
| publicar_tienda | 1 | pruebas_usabilidad, preparar_marketing |
graph LR
A[definir_alcance] --> B[disenar_pantallas]
A --> C[elegir_stack]
B --> D[implementar_login]
C --> D
B --> E[implementar_tablero]
C --> E
D --> F[conectar_api]
E --> F
F --> G[pruebas_usabilidad]
M[preparar_marketing] --> H
G --> H[publicar_tienda]
Ejercicio 1: construir y consultar el grafo del proyecto
Construye el grafo movil con la clase Grafo (recuerda la convención del módulo: anadir_arista(A, B) = "B depende de A") y un diccionario prioridades con la prioridad de cada tarea. Después responde con código: (a) ¿qué tareas pueden empezar ya (grado de entrada 0)?; (b) ¿cuántas tareas desbloquea directamente elegir_stack y cuáles son?; (c) ¿es publicar_tienda la única tarea "final" (grado de salida 0)?
Ejercicio 2: el camino con menos pasos entre dos tareas
El jefe de proyecto pregunta: "¿cuál es la cadena de dependencias más corta que lleva de definir_alcance a publicar_tienda?". Escribe camino_minimo_pasos(grafo, origen, destino) que devuelva la lista de tareas del camino con menos aristas (o None si no hay camino). Pista: BFS guardando predecesores, y reconstrucción hacia atrás como en Dijkstra (07-04) — pero sin montículo: aquí no hay pesos.
Ejercicio 3: detectar y listar el ciclo de dependencias
Un usuario despistado añade dos dependencias nuevas: la retroalimentación de usuarios llega tras publicar (publicar_tienda → retro_usuarios) y, con las prisas, declara que el diseño de pantallas depende de esa retroalimentación (retro_usuarios → disenar_pantallas). El proyecto queda bloqueado. hay_ciclo (07-03) solo devuelve True; para un mensaje de error útil, TaskFlow necesita enseñar el ciclo. Escribe encontrar_ciclo(grafo) que devuelva la lista de vértices del ciclo (terminando en el vértice inicial repetido), o None si no hay. Pista: en el DFS blanco/gris/negro, mantén además la lista camino de vértices grises; al toparte con un gris, el ciclo es el tramo de camino desde ese vértice.
Ejercicio 4: orden de ejecución con desempate por prioridad
orden_topologico (Kahn) elige arbitrariamente entre las tareas disponibles. El equipo quiere algo mejor: entre las tareas ejecutables en cada momento, primero la de mayor prioridad (menor número; a igual prioridad, orden alfabético). Escribe orden_topologico_prioridad(grafo, prioridades) sustituyendo la deque de Kahn por el heapq del módulo 4 con tuplas (prioridad, id). Aplícalo al grafo movil (sin las aristas del ejercicio 3) y compara la posición de preparar_marketing con la que le daría una cola normal.
Ejercicio 5: islas de tareas (proyectos independientes)
Al grafo movil se le suman las tareas de otro frente de trabajo: redisenar_logo → actualizar_web y actualizar_web → nota_prensa, más una tarea suelta sin relaciones, renovar_dominio. Escribe islas_de_tareas(grafo) que devuelva las componentes conexas (ignorando el sentido de las flechas, como en 07-03) ordenadas de mayor a menor tamaño, y úsala para responder: ¿cuántos proyectos independientes conviven en el tablero y de qué tamaño?
Ejercicio 6: la rejilla como grafo implícito
La oficina de TaskFlow estrena un robot de reparto de paquetes que se mueve por un almacén cuadriculado: . es suelo libre, # una estantería, S la salida del robot y M la mesa de destino:
Calcula el mínimo número de movimientos (arriba/abajo/izquierda/derecha) de S a M con BFS... sin construir ningún objeto Grafo. La rejilla ya es un grafo: cada celda libre es un vértice y sus vecinos se calculan al vuelo mirando las cuatro casillas contiguas. Este patrón de grafo implícito es importantísimo: demuestra que BFS necesita saber "dame los vecinos de v", no una estructura concreta.
Soluciones
Solución 1
movil = Grafo(dirigido=True)
dependencias_movil = [
("definir_alcance", "disenar_pantallas"),
("definir_alcance", "elegir_stack"),
("disenar_pantallas", "implementar_login"),
("elegir_stack", "implementar_login"),
("disenar_pantallas", "implementar_tablero"),
("elegir_stack", "implementar_tablero"),
("implementar_login", "conectar_api"),
("implementar_tablero", "conectar_api"),
("conectar_api", "pruebas_usabilidad"),
("pruebas_usabilidad", "publicar_tienda"),
("preparar_marketing", "publicar_tienda"),
]
for origen, destino in dependencias_movil:
movil.anadir_arista(origen, destino)
prioridades = {
"definir_alcance": 1, "disenar_pantallas": 2, "elegir_stack": 1,
"implementar_login": 2, "implementar_tablero": 1, "conectar_api": 1,
"pruebas_usabilidad": 2, "preparar_marketing": 3, "publicar_tienda": 1,
}
# (a) grado de entrada 0: pueden empezar hoy
grados = movil.grados_entrada()
print([t for t, g in grados.items() if g == 0])
# ['definir_alcance', 'preparar_marketing']
# (b) desbloqueos directos de elegir_stack
print(movil.grado_salida("elegir_stack"), list(movil.vecinos("elegir_stack")))
# 2 ['implementar_login', 'implementar_tablero']
# (c) tareas finales: grado de salida 0
print([t for t in movil.vertices() if movil.grado_salida(t) == 0])
# ['publicar_tienda'] -> sí, es la únicaComentarios: todas las tareas aparecen en alguna arista, así que no hizo falta anadir_vertice explícito — pero revisa siempre esa suposición (aquí renovar_dominio del ejercicio 5 la romperá). Las respuestas salen de los métodos de la clase, sin ningún algoritmo: modelar bien ya responde preguntas.
Solución 2
from collections import deque
def camino_minimo_pasos(grafo, origen, destino):
if origen == destino:
return [origen]
predecesor = {origen: None} # hace tambien de visitados (07-03)
cola = deque([origen])
while cola:
actual = cola.popleft()
for vecino in grafo.vecinos(actual):
if vecino not in predecesor:
predecesor[vecino] = actual
if vecino == destino: # llegamos: podemos parar ya
camino = [destino]
while predecesor[camino[-1]] is not None:
camino.append(predecesor[camino[-1]])
return camino[::-1] # se reconstruye del reves, como en 07-04
cola.append(vecino)
return None # cola vacia sin llegar: inalcanzable
print(camino_minimo_pasos(movil, "definir_alcance", "publicar_tienda"))
# ['definir_alcance', 'disenar_pantallas', 'implementar_login',
# 'conectar_api', 'pruebas_usabilidad', 'publicar_tienda']Comentarios: el dict de predecesores cumple dos papeles (marcar visitados y recordar el camino), igual que en Dijkstra pero sin pesos ni montículo. Poder cortar en cuanto se alcanza el destino es un lujo exclusivo de BFS: la primera visita garantiza el mínimo de aristas. Hay dos caminos de 5 aristas (por login o por tablero); BFS devuelve el primero que descubre, y ambos son correctos.
Solución 3
def encontrar_ciclo(grafo):
BLANCO, GRIS, NEGRO = 0, 1, 2
color = {v: BLANCO for v in grafo.vertices()}
camino = [] # los vertices grises, en orden
def visitar(v):
color[v] = GRIS
camino.append(v)
for vecino in grafo.vecinos(v):
if color[vecino] == GRIS: # arista hacia el camino actual
inicio = camino.index(vecino) # donde empieza el ciclo
return camino[inicio:] + [vecino]
if color[vecino] == BLANCO:
ciclo = visitar(vecino)
if ciclo:
return ciclo # propagar el hallazgo hacia arriba
camino.pop() # backtracking: v deja el camino
color[v] = NEGRO
return None
for v in grafo.vertices():
if color[v] == BLANCO:
ciclo = visitar(v)
if ciclo:
return ciclo
return None
movil.anadir_arista("publicar_tienda", "retro_usuarios")
movil.anadir_arista("retro_usuarios", "disenar_pantallas")
print(encontrar_ciclo(movil))
# ['disenar_pantallas', 'implementar_login', 'conectar_api',
# 'pruebas_usabilidad', 'publicar_tienda', 'retro_usuarios', 'disenar_pantallas']
# limpiar para los siguientes ejercicios (eliminar_arista: ejercicio 2 de 07-02)
movil.eliminar_arista("publicar_tienda", "retro_usuarios")
movil.eliminar_arista("retro_usuarios", "disenar_pantallas")Comentarios: la única novedad sobre hay_ciclo (07-03) es la lista camino, que crece al pintar de gris y encoge en el backtracking — una pila explícita, módulo 3, que retrata en todo momento la rama actual del DFS. Al toparse con un gris, el tramo desde su posición es exactamente el ciclo; se añade el vértice repetido al final para que el mensaje al usuario se lea como un círculo cerrado. Con esta lista, TaskFlow puede mostrar: "no puedo añadir la dependencia: disenar_pantallas → ... → retro_usuarios → disenar_pantallas".
Solución 4
import heapq
def orden_topologico_prioridad(grafo, prioridades):
grados = grafo.grados_entrada()
monticulo = [(prioridades[v], v) for v, g in grados.items() if g == 0]
heapq.heapify(monticulo)
orden = []
while monticulo:
_, actual = heapq.heappop(monticulo) # la ejecutable mas prioritaria
orden.append(actual)
for vecino in grafo.vecinos(actual):
grados[vecino] -= 1
if grados[vecino] == 0:
heapq.heappush(monticulo, (prioridades[vecino], vecino))
if len(orden) < len(grados):
raise ValueError("Hay un ciclo de dependencias")
return orden
prioridades["retro_usuarios"] = 3 # quedo como vertice tras el ejercicio 3
print(orden_topologico_prioridad(movil, prioridades))
# ['definir_alcance', 'elegir_stack', 'disenar_pantallas', 'implementar_tablero',
# 'implementar_login', 'conectar_api', 'pruebas_usabilidad',
# 'preparar_marketing', 'publicar_tienda', 'retro_usuarios']Comentarios: es Kahn letra por letra con la deque cambiada por un montículo — la misma sustitución cola→cola de prioridad que convirtió la Cola en BandejaUrgencias en el módulo 4. Las tuplas (prioridad, id) desempatan solas: primero por número, luego alfabéticamente, sin necesidad del contador del módulo 4 porque los ids son strings comparables. Fíjate en preparar_marketing: es ejecutable desde el minuto uno, pero su prioridad 3 la relega hasta el final (solo sale cuando es la única opción); una cola FIFO la habría ejecutado la segunda o tercera. El orden sigue siendo topológicamente válido: el montículo solo contiene tareas con todas sus dependencias cumplidas.
Solución 5
movil.anadir_arista("redisenar_logo", "actualizar_web")
movil.anadir_arista("actualizar_web", "nota_prensa")
movil.anadir_vertice("renovar_dominio") # ¡sin aristas: alta explicita!
def islas_de_tareas(grafo):
nd = Grafo(dirigido=False) # version sin sentido de flechas
for v in grafo.vertices():
nd.anadir_vertice(v)
for destino in grafo.vecinos(v):
nd.anadir_arista(v, destino)
visitados, islas = set(), []
for v in nd.vertices():
if v not in visitados:
isla = bfs(nd, v) # toda la componente de v
visitados.update(isla)
islas.append(isla)
return sorted(islas, key=len, reverse=True)
for isla in islas_de_tareas(movil):
print(len(isla), isla)
# 9 ['definir_alcance', ..., 'pruebas_usabilidad', 'publicar_tienda']
# 3 ['redisenar_logo', 'actualizar_web', 'nota_prensa']
# 1 ['retro_usuarios']
# 1 ['renovar_dominio']Comentarios: cuatro islas — la app móvil (9 tareas), la campaña de imagen (3) y dos tareas sueltas. ¿Sorprende retro_usuarios? En el ejercicio 3 eliminamos sus dos aristas, pero el vértice siguió dado de alta: ahora aparece como isla propia, un recordatorio de que V y E son conjuntos independientes (07-01). Dos detalles más a vigilar: la conversión a no dirigido antes de buscar componentes (con las flechas, publicar_tienda no "vería" a preparar_marketing), y el alta explícita de renovar_dominio, que sin aristas no existiría en el grafo — el error clásico de 07-02. Alternativa perfectamente válida: UnionFind (07-05) uniendo los extremos de cada arista; las raíces finales son las islas.
Solución 6
from collections import deque
def pasos_minimos(rejilla):
filas, columnas = len(rejilla), len(rejilla[0])
for f in range(filas): # localizar S y M
for c in range(columnas):
if rejilla[f][c] == "S":
salida = (f, c)
elif rejilla[f][c] == "M":
meta = (f, c)
visitados = {salida} # el set de tuplas del modulo 5
cola = deque([(salida, 0)]) # (celda, pasos hasta ella)
while cola:
(f, c), pasos = cola.popleft()
if (f, c) == meta:
return pasos
for df, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]: # las 4 direcciones
nf, nc = f + df, c + dc
if (0 <= nf < filas and 0 <= nc < columnas # dentro del tablero
and rejilla[nf][nc] != "#" # no es estanteria
and (nf, nc) not in visitados):
visitados.add((nf, nc)) # marcar al encolar
cola.append(((nf, nc), pasos + 1))
return None # M inalcanzable
print(pasos_minimos(almacen)) # 6Comentarios: es el bfs de 07-03 con dos cambios cosméticos: los vértices son tuplas (fila, columna) —hashables, por eso valen como elementos del set— y los "vecinos" no se leen de ninguna lista de adyacencia: se generan al vuelo sumando los cuatro desplazamientos y filtrando límites y muros. Un camino óptimo de 6 pasos: (0,0) → (1,0) → (2,0) → (2,1) → (2,2) → (2,3) → (3,3); compruébalo dibujando sobre la rejilla. La lección de fondo: BFS no necesita la clase Grafo, solo una función de vecinos. Mapas de juegos, estados de un puzle, versiones de un documento: cualquier cosa con "estados y transiciones" es un grafo implícito y todo el módulo le aplica.
Errores Comunes y Consejos
- Construir aristas con la convención invertida. Si escribes
anadir_arista("disenar_pantallas", "definir_alcance")pensando "depende de", todos los algoritmos posteriores trabajarán sobre un proyecto del revés. Relee cada arista como "al terminar A se desbloquea B" antes de continuar. - Olvidar los vértices sin aristas (
renovar_dominio): no aparecen en islas, órdenes ni recuentos. Alta explícita siempre. camino.index(vecino)en grafos enormes esO(longitud del camino); para producción se guardaría también la posición de cada vértice gris en undict. Para entender el algoritmo, la versión clara gana.- Probar solo el camino feliz. Ejecuta
camino_minimo_pasoscon destino inalcanzable,encontrar_ciclosobre el grafo limpio y el almacén con la meta tapiada: la mitad de los errores reales viven en losNone. - Consejo final del módulo: cuando un problema nuevo te desconcierte, pregúntate "¿qué son aquí los vértices y qué las aristas?". Es la pregunta que convierte robots en rejillas, tareas en DAGs y usuarios en redes — y una vez respondida, los algoritmos son siempre los mismos seis de este módulo.
Conclusión
Seis ejercicios y un proyecto entero después, los grafos han pasado de concepto a herramienta: has construido y consultado el grafo de un lanzamiento real, encontrado la cadena de dependencias más corta con BFS y predecesores, convertido "hay un ciclo" en un mensaje de error que enseña el ciclo, hecho que Kahn respete las prioridades con el montículo del módulo 4, separado el tablero en proyectos independientes y descubierto que hasta un almacén cuadriculado es un grafo si se le pregunta bien.
Y con esto, algo más grande: TaskFlow está completo en estructuras. Repasa lo construido a lo largo del curso — el tablero sobre arrays y listas enlazadas, el deshacer con pilas, las notificaciones y urgencias con colas y montículos, los índices instantáneos con tablas hash, las jerarquías con árboles y, desde este módulo, las dependencias, órdenes y rutas con grafos. No queda ninguna pieza fundamental por conocer. Lo que queda es criterio: ante un problema nuevo, ¿qué estructura eliges y por qué? Ese es exactamente el tema del módulo 8: mirar atrás con perspectiva, aprender a elegir estructura con argumentos de coste y de diseño, y rematar el curso con recursos para seguir y proyectos finales que integren todo lo aprendido.
Curso de Estructuras de Datos
Módulo 1: Introducción a las Estructuras de Datos
- ¿Qué son las Estructuras de Datos?
- Importancia de las Estructuras de Datos en la Programación
- Tipos de Estructuras de Datos
- Complejidad Algorítmica y Notación Big O
- Arrays y Memoria: la Base de las Estructuras de Datos
Módulo 2: Listas
- Introducción a las Listas
- Listas Enlazadas
- Listas Doblemente Enlazadas
- Listas Circulares
- Ejercicios con Listas
Módulo 3: Pilas
- Introducción a las Pilas
- Operaciones Básicas con Pilas
- Implementación de Pilas
- Aplicaciones de las Pilas
- Ejercicios con Pilas
Módulo 4: Colas
- Introducción a las Colas
- Operaciones Básicas con Colas
- Colas Circulares
- Colas de Prioridad
- Colas Dobles (Deques)
- Ejercicios con Colas
Módulo 5: Tablas Hash y Diccionarios
- Introducción a las Tablas Hash
- Funciones Hash y Resolución de Colisiones
- Diccionarios y Conjuntos en la Práctica
- Ejercicios con Tablas Hash
Módulo 6: Árboles
- Introducción a los Árboles
- Árboles Binarios
- Recorridos de Árboles
- Árboles Binarios de Búsqueda
- Árboles AVL
- Árboles B
- Montículos (Heaps)
- Ejercicios con Árboles
Módulo 7: Grafos
- Introducción a los Grafos
- Representación de Grafos
- Algoritmos de Búsqueda en Grafos
- Algoritmos de Caminos Mínimos
- Árboles de Expansión Mínima
- Aplicaciones de los Grafos
- Ejercicios con Grafos
