← Euler Lab
#018

Suma Máxima de Camino I

Cargando Python...

Maximum Path Sum I

Empezando en la cima del triángulo y moviéndote a números adyacentes en la fila de abajo, encuentra la suma máxima desde la cima hasta la base.

Programación Dinámica Bottom-up Por qué falla greedy

1 El Problema del Triángulo

Tenemos un triángulo de números. Desde la cima, podemos bajar a uno de los dos números adyacentes en la fila siguiente. Queremos maximizar la suma del camino.

Ejemplo pequeño (4 filas):

3
7
4
2
4
6
8
5
9
3

Camino óptimo: 3 → 7 → 4 → 9 = 23

¿Qué significa "adyacente"?

Desde cada número, solo puedes moverte a los dos números directamente debajo:

Si estás en posición i de una fila,

puedes ir a posición i o i+1 de la siguiente.

7
2
4

Desde el 7, solo puedes ir al 2 o al 4 (no al 6)

¿Por qué NO funciona elegir siempre el mayor?

El enfoque "greedy" (codicioso) sería: en cada paso, elegir el número más grande. Pero esto puede llevarnos a un camino subóptimo:

Camino Greedy
3
7
4
2
4
6
8
5
9
3

3→7→4→5 = 19

Camino Óptimo
3
7
4
2
4
6
8
5
9
3

3→7→4→9 = 23

Greedy elige 5 sobre 9 porque desde el 4, el 5 está "disponible" pero el camino al 9 requiere planificación desde antes.

El Triángulo Real (15 filas)

El problema nos da un triángulo de 15 filas. ¿Cuántos caminos posibles hay?

En cada fila elegimos izquierda o derecha: 2 opciones.
14 decisiones (filas 1→2, 2→3, ..., 14→15).
Total: 214 = 16,384 caminos

Probar todos es factible para 15 filas, pero el problema 67 tiene ¡100 filas! (2⁹⁹ caminos)

Suma máxima del camino
1074
Filas en el triángulo
15

2 Fuerza Bruta: Probar Todos los Caminos

La solución más directa: explorar todos los caminos posibles con recursión y quedarnos con el mejor.

La Idea Recursiva

Para cada celda, la mejor suma desde ahí hasta abajo es:

mejor(fila, col) = valor[fila][col] + max(
mejor(fila+1, col),
mejor(fila+1, col+1)
)

Caso base: en la última fila, mejor(fila, col) = valor[fila][col]

Python — Recursión (fuerza bruta)
// Output aparecerá aquí

El Problema: Trabajo Repetido

Observa este triángulo de llamadas para el ejemplo pequeño:

max(0,0) = 3 + max(max(1,0), max(1,1))
           │
           ├── max(1,0) = 7 + max(max(2,0), max(2,1))
           │              │
           │              ├── max(2,0) = 2 + max(max(3,0), max(3,1))
           │              └── max(2,1) = 4 + max(max(3,1), max(3,2))  ← ¡REPETIDO!
           │
           └── max(1,1) = 4 + max(max(2,1), max(2,2))
                          │
                          ├── max(2,1) = 4 + max(max(3,1), max(3,2))  ← ¡REPETIDO!
                          └── max(2,2) = 6 + max(max(3,2), max(3,3))
                        

max(2,1) se calcula dos veces. En triángulos grandes, el mismo subproblema se recalcula miles de veces.

Complejidad temporal
O(2ⁿ)

Exponencial - cada nodo genera 2 llamadas

Complejidad espacial
O(n)

Profundidad del stack de recursión

Recursión Subproblemas solapados ! Ineficiente O(2ⁿ)

3 Programación Dinámica: Bottom-Up

En vez de calcular desde arriba (y repetir trabajo), calculamos desde abajo hacia arriba. Cada celda solo se procesa una vez.

La Idea Clave

Empezamos por la penúltima fila. Para cada número, sumamos el mayor de sus dos hijos:

nuevo[i] = original[i] + max(hijo_izq, hijo_der)

Repetimos hasta llegar a la cima. El número en la cima es la respuesta.

Ejemplo paso a paso:

Paso 0: Triángulo original

3
7
4
2
4
6
8
5
9
3

Paso 1: Procesar fila 3

3
7
4
10
13
15

2+max(8,5)=10, 4+max(5,9)=13, 6+max(9,3)=15

Paso 2: Procesar fila 2

3
20
19

7+max(10,13)=20, 4+max(13,15)=19

Paso 3: Procesar fila 1 (cima)

23

3+max(20,19)=23 ✓

Python — Programación Dinámica (bottom-up)
// Output aparecerá aquí
Python — Versión compacta (una línea por iteración)
// Output aparecerá aquí

Comparación de Complejidad

Método Tiempo Espacio 15 filas 100 filas
Recursión O(2ⁿ) O(n) 16,384 ops 10³⁰ ops
DP bottom-up O(n²) O(n²) 120 ops 5,050 ops

n = número de filas. DP es exponencialmente más rápido.

El Patrón de Programación Dinámica

  1. Identificar subproblemas solapados: los mismos cálculos se repiten
  2. Definir el estado: ¿qué información necesito? (fila, columna)
  3. Encontrar la recurrencia: dp[i][j] = valor + max(dp[i+1][j], dp[i+1][j+1])
  4. Establecer caso base: la última fila ya tiene los valores finales
  5. Elegir dirección: bottom-up suele ser más eficiente que top-down con memoización
Programación Dinámica Bottom-up vs Top-down Subproblemas solapados Por qué greedy falla