Planificar un proyecto con PERT y el camino crítico, con su diagrama de Gantt
Planifica el desarrollo de una aplicación: duraciones PERT a partir de tres estimaciones, orden de las tareas con sus dependencias (y detección de ciclos), fechas más tempranas y más tardías, holguras, camino crítico, probabilidad de cumplir el plazo y un diagrama de Gantt en texto.
- Planificación de proyectos de software
- PERT: estimación de tres valores
- CPM: camino crítico y holguras
- Orden topológico y ciclos
- Distribución normal (math.erf)
- Diagrama de Gantt
Enunciado
Antes de empezar un proyecto hay que responder a dos preguntas: cuánto va a durar y qué tareas no pueden retrasarse ni un día. Como nadie sabe cuánto dura una tarea, PERT pide tres estimaciones (optimista O, más probable M y pesimista P) y usa la media (O + 4M + P) / 6, con una desviación (P − O) / 6. Con las dependencias entre tareas, el método del camino crítico (CPM) calcula cuándo puede empezar cada una como pronto y como tarde.
La diferencia entre las dos fechas es la holgura: lo que una tarea puede retrasarse sin retrasar el proyecto. Las de holgura 0 forman el camino crítico. Cada línea de la entrada es una tarea: ID O M P dependencias nombre (las dependencias separadas por comas, o - si no tiene), y puede haber una línea plazo N con los días disponibles.
Qué tiene que hacer el programa
- Una línea con menos de seis datos, estimaciones que no son números (admiten coma decimal) o un ID repetido:
Línea N: no se entiende «línea»; estimaciones que no cumplen 0 ≤ O ≤ M ≤ P:Línea N: la tarea X tiene estimaciones incoherentes (deben cumplir 0 ≤ O ≤ M ≤ P). Después, cada dependencia que no existe:La tarea X depende de Y, que no existe. Si hubo algún error de estos:No se puede planificary nada más. Sin tareas:No hay tareas. - Si las dependencias forman un ciclo:
Hay un ciclo de dependencias: A → B → C → A(el primero que se encuentra recorriendo las tareas en el orden de entrada y sus dependencias en su orden, con un recorrido en profundidad) y nada más. - Duración de cada tarea:
(O + 4M + P) / 6. Inicio temprano: el mayor fin de sus dependencias (0 si no tiene); fin: inicio + duración. La duración del proyecto es el mayor fin. Hacia atrás, el fin tardío de una tarea es el menor inicio tardío de las que dependen de ella (o la duración del proyecto si no hay ninguna); inicio tardío = fin tardío − duración; holgura = inicio tardío − inicio. Son críticas las de holgura 0. La tabla (ya escrita entabla) va en el orden de entrada. Duración prevista: T días (σ = S), donde S es la raíz de la suma de las varianzas((P − O) / 6)²de las tareas del camino crítico;Camino crítico: A → B → …, que se reconstruye hacia atrás desde la primera tarea crítica (en orden topológico) que acaba al final, eligiendo cada vez la primera dependencia crítica que acaba justo cuando empieza la tarea. Con plazo:Probabilidad de acabar en N días: X %con la normal50 · (1 + erf((N − T) / (S · √2)))(si S es 0: 100 o 0). Todos los números con un decimal y coma.- Al final,
Gantt (# crítica, = no crítica, . holgura):y una fila por tarea en el orden de entrada: el ID en 6 columnas y un carácter por día (de 0 hasta la duración del proyecto redondeada hacia arriba):#(crítica) o=si el día [d, d + 1) se solapa con la tarea,.si se solapa con su holgura (entre el fin y el fin tardío), y espacio si no; sin espacios al final.
Entrada
Una tarea por línea: ID O M P dependencias nombre (- si no tiene dependencias).
Opcional: plazo N.
Datos de referencia
| ID | O | M | P | Depende de | Tarea |
|---|---|---|---|---|---|
| A | 2 | 3 | 4 | - | Requisitos |
| B | 3 | 5 | 9 | A | Diseño de la base de datos |
| C | 2 | 4 | 6 | A | Diseño de la interfaz |
| D | 5 | 8 | 14 | B | Backend y API |
| E | 4 | 6 | 8 | C | Frontend |
| F | 2 | 3 | 5 | D,E | Integración y pruebas |
| G | 1 | 1 | 2 | C | Manual de usuario |
Ejemplos de ejecución
Tu programa debe escribir exactamente esta salida para estas entradas. Las pruebas del editor incluyen estos ejemplos y otros casos ocultos.
Una aplicación web
Entrada
A 2 3 4 - Requisitos B 3 5 9 A Diseño de la base de datos C 2 4 6 A Diseño de la interfaz D 5 8 14 B Backend y API E 4 6 8 C Frontend F 2 3 5 D,E Integración y pruebas G 1 1 2 C Manual de usuario plazo 22
Salida por consola
Tarea Duración Inicio Fin Holgura A 3,0 0,0 3,0 0,0 * B 5,3 3,0 8,3 0,0 * C 4,0 3,0 7,0 3,8 D 8,5 8,3 16,8 0,0 * E 6,0 7,0 13,0 3,8 F 3,2 16,8 20,0 0,0 * G 1,2 7,0 8,2 11,8 Duración prevista: 20,0 días (σ = 1,9) Camino crítico: A → B → D → F Probabilidad de acabar en 22 días: 85,4 % Gantt (# crítica, = no crítica, . holgura): A ### B ###### C ====.... D ######### E ======.... F #### G ==...........
Errores en las tareas
Entrada
A 1 2 3 - Uno B 2 1 3 A Dos C 1 2 X A Tres D 1 2 3 Z Cuatro A 1 1 1 - Repetida ===== X 1 2 3 Z Equis
Salida por consola
Línea 2: la tarea B tiene estimaciones incoherentes (deben cumplir 0 ≤ O ≤ M ≤ P) Línea 3: no se entiende «C 1 2 X A Tres» Línea 5: no se entiende «A 1 1 1 - Repetida» Línea 6: no se entiende «=====» La tarea D depende de Z, que no existe La tarea X depende de Z, que no existe No se puede planificar
Guía paso a paso
Intenta resolverlo por tu cuenta y abre un paso solo cuando te atasques: cada uno te acerca a la solución sin dártela entera.
1. Primero, un orden válido
Un recorrido en profundidad que añade cada tarea después de sus dependencias da un orden topológico. Si al bajar encuentras una tarea que está «en curso», hay un ciclo: el camino desde ella hasta aquí.
def visitar(t, camino):
if estado.get(t) == "en curso":
return camino[camino.index(t):] + [t]
...2. Dos pasadas
Hacia delante, en orden topológico, las fechas más tempranas; hacia atrás, en el orden inverso, las más tardías. Cada pasada solo mira a las tareas vecinas.
3. El camino y su incertidumbre
Las varianzas de tareas seguidas se suman (la desviación no): σ es la raíz de la suma. Con math.erf se calcula la probabilidad de la normal sin ninguna librería.
4. El Gantt
Cada día es una columna; compara el intervalo del día con el de la tarea y con el de su holgura. Con fracciones de día, un día cuenta si la tarea ocupa cualquier parte de él.
Resuélvelo aquí
El editor trae el esqueleto del programa. Pulsa «Ejecutar» para comprobarlo con los ejemplos y con 3 casos ocultos que buscan los errores típicos.
Ejemplo
A 2 3 4 - Requisitos B 3 5 9 A Diseño de la base de datos C 2 4 6 A Diseño de la interfaz D 5 8 14 B Backend y API E 4 6 8 C Frontend F 2 3 5 D,E Integración y pruebas G 1 1 2 C Manual de usuario plazo 22
Tarea Duración Inicio Fin Holgura A 3,0 0,0 3,0 0,0 * B 5,3 3,0 8,3 0,0 * C 4,0 3,0 7,0 3,8 D 8,5 8,3 16,8 0,0 * E 6,0 7,0 13,0 3,8 F 3,2 16,8 20,0 0,0 * G 1,2 7,0 8,2 11,8 Duración prevista: 20,0 días (σ = 1,9) Camino crítico: A → B → D → F Probabilidad de acabar en 22 días: 85,4 % Gantt (# crítica, = no crítica, . holgura): A ### B ###### C ====.... D ######### E ======.... F #### G ==...........
Solución explicada
Ver la solución completa
1import math
2import sys
3
4
5def num(x):
6 """Un número con un decimal y coma: 4,2 (los restos de redondeo como -0,0000001 son 0)"""
7 if abs(x) < 1e-9:
8 x = 0.0
9 return f"{x:.1f}".replace(".", ",")
10
11
12def tabla(tareas, orden, plan):
13 print("Tarea Duración Inicio Fin Holgura")
14 for t in orden:
15 p = plan[t]
16 print(f"{t:<6}{num(p['dur']):>9}{num(p['ini']):>8}{num(p['fin']):>8}{num(p['holgura']):>9}{' *' if p['critica'] else ''}")
17
18def leer(lineas):
19 """{id: (o, m, p, [dependencias], nombre)} en el orden de entrada, o None si hay errores."""
20 tareas, plazo, ok = {}, None, True
21 for n, linea in enumerate(lineas, 1):
22 p = linea.split()
23 if not p:
24 continue
25 if p[0] == "plazo" and len(p) == 2 and p[1].isdigit():
26 plazo = int(p[1])
27 continue
28 try:
29 o, m, pe = (float(x.replace(",", ".")) for x in p[1:4])
30 assert len(p) >= 6 and p[0] not in tareas
31 except (ValueError, AssertionError):
32 print(f"Línea {n}: no se entiende «{linea.strip()}»")
33 ok = False
34 continue
35 if not 0 <= o <= m <= pe:
36 print(f"Línea {n}: la tarea {p[0]} tiene estimaciones incoherentes (deben cumplir 0 ≤ O ≤ M ≤ P)")
37 ok = False
38 continue
39 deps = [] if p[4] == "-" else p[4].split(",")
40 tareas[p[0]] = (o, m, pe, deps, " ".join(p[5:]))
41 for t, (_, _, _, deps, _) in tareas.items():
42 for d in deps:
43 if d not in tareas:
44 print(f"La tarea {t} depende de {d}, que no existe")
45 ok = False
46 return (tareas, plazo) if ok else (None, None)
47
48
49def orden_topologico(tareas):
50 """Las tareas en un orden en que cada una va después de sus dependencias, o el ciclo si lo hay."""
51 estado, orden = {}, []
52
53 def visitar(t, camino):
54 if estado.get(t) == "hecha":
55 return None
56 if estado.get(t) == "en curso":
57 return camino[camino.index(t):] + [t]
58 estado[t] = "en curso"
59 for d in tareas[t][3]:
60 ciclo = visitar(d, camino + [t])
61 if ciclo:
62 return ciclo
63 estado[t] = "hecha"
64 orden.append(t)
65 return None
66
67 for t in tareas:
68 ciclo = visitar(t, [])
69 if ciclo:
70 return None, ciclo
71 return orden, None
72
73
74def calcular(tareas, orden):
75 plan = {}
76 for t in orden: # pasada hacia delante: lo antes posible
77 o, m, p, deps, _ = tareas[t]
78 dur = (o + 4 * m + p) / 6
79 ini = max((plan[d]["fin"] for d in deps), default=0)
80 plan[t] = {"dur": dur, "var": ((p - o) / 6) ** 2, "ini": ini, "fin": ini + dur}
81 total = max(p["fin"] for p in plan.values())
82 sucesores = {t: [s for s in orden if t in tareas[s][3]] for t in orden}
83 for t in reversed(orden): # pasada hacia atrás: lo más tarde posible
84 fin_tarde = min((plan[s]["ini_tarde"] for s in sucesores[t]), default=total)
85 plan[t]["ini_tarde"] = fin_tarde - plan[t]["dur"]
86 plan[t]["holgura"] = plan[t]["ini_tarde"] - plan[t]["ini"]
87 plan[t]["critica"] = abs(plan[t]["holgura"]) < 1e-9
88 plan[t]["fin_tarde"] = fin_tarde
89 return plan, total
90
91
92def camino_critico(tareas, orden, plan, total):
93 """Desde la primera tarea crítica que acaba al final, hacia atrás por predecesoras críticas encadenadas."""
94 actual = next(t for t in orden if plan[t]["critica"] and abs(plan[t]["fin"] - total) < 1e-9)
95 camino = [actual]
96 while True:
97 previa = next((d for d in tareas[actual][3] if plan[d]["critica"] and abs(plan[d]["fin"] - plan[actual]["ini"]) < 1e-9), None)
98 if previa is None:
99 return list(reversed(camino))
100 camino.append(previa)
101 actual = previa
102
103
104def fila_gantt(p, dias):
105 """# o = los días de la tarea, . los de holgura: un día d cuenta si [d, d+1) se solapa con el intervalo."""
106 fila = ""
107 for d in range(dias):
108 if d < p["fin"] - 1e-9 and d + 1 > p["ini"] + 1e-9:
109 fila += "#" if p["critica"] else "="
110 elif d < p["fin_tarde"] - 1e-9 and d + 1 > p["fin"] + 1e-9:
111 fila += "."
112 else:
113 fila += " "
114 return fila.rstrip()
115
116
117def main():
118 tareas, plazo = leer(sys.stdin.read().splitlines())
119 if tareas is None:
120 print("No se puede planificar")
121 return
122 if not tareas:
123 print("No hay tareas")
124 return
125 orden, ciclo = orden_topologico(tareas)
126 if ciclo:
127 print("Hay un ciclo de dependencias: " + " → ".join(ciclo))
128 return
129 plan, total = calcular(tareas, orden)
130 tabla(tareas, list(tareas), plan)
131 camino = camino_critico(tareas, orden, plan, total)
132 sigma = math.sqrt(sum(plan[t]["var"] for t in camino))
133 print(f"Duración prevista: {num(total)} días (σ = {num(sigma)})")
134 print("Camino crítico: " + " → ".join(camino))
135 if plazo is not None:
136 prob = 100.0 if sigma == 0 and plazo >= total else 0.0 if sigma == 0 else 50 * (1 + math.erf((plazo - total) / (sigma * math.sqrt(2))))
137 print(f"Probabilidad de acabar en {plazo} días: {num(prob)} %")
138 print("Gantt (# crítica, = no crítica, . holgura):")
139 dias = math.ceil(total - 1e-9)
140 for t in tareas:
141 print(f"{t:<6}{fila_gantt(plan[t], dias)}".rstrip())
142
143
144main()La media de PERT da más peso al valor más probable, pero tiene en cuenta los extremos: una tarea con un pesimista muy alto alarga la estimación aunque lo normal sea rápido. Es la forma de convertir la incertidumbre en números.
El camino crítico dice dónde poner la atención: retrasar una tarea crítica retrasa el proyecto; una con holgura puede esperar. Añadir gente a una tarea que no es crítica no acorta nada.
La probabilidad de cumplir el plazo suele sorprender: un proyecto planificado justo en su duración prevista solo tiene un 50 % de acabar a tiempo. Por eso se añaden márgenes.
Las metodologías ágiles planifican por sprints y no por tareas con fechas, pero el camino crítico sigue apareciendo en las dependencias entre equipos y en la implantación de sistemas como un ERP.
Para ir más allá
- Añade recursos: cada tarea necesita una persona y solo hay dos; retrasa lo necesario (nivelación).
- Calcula el camino crítico más probable con una simulación de Monte Carlo (duraciones aleatorias entre O y P).
- Exporta el plan a un CSV que se pueda abrir como diagrama de Gantt en una hoja de cálculo.