Lecciones · #11 de 11

Programación Dinámica

Cambia memoria por tiempo — resuélvelo una vez, recuérdalo para siempre

Dynamic programming · 1D table fill

step 0 / 0

Pruébalo: Observa cómo se llena la tabla 1-D de izquierda a derecha — cada celda es solo la suma de las dos anteriores. Un valor que la recursión ingenua recalcularía miles de millones de veces se lee una sola vez de la celda vecina. Ese cambio — recalcular → consultar — es toda la idea.

Por qué importa

Un sorprendente número de problemas "exponenciales" son secretamente polinómicos — solo parecen exponenciales porque la recursión obvia resuelve el mismo subproblema millones de veces. La programación dinámica es la técnica de notar la repetición y almacenarla en caché. Una vez que ves el patrón, problemas que tardarían siglos colapsan a milisegundos.

En robótica es la base de la iteración de valor, la iteración de política, las recursiones de Bellman y los algoritmos de camino más corto que ya conociste. En cadenas de texto es el algoritmo de diferencias (diff) que usa git. En bioinformática es cómo funciona toda herramienta de alineamiento de secuencias.

La idea

Un problema con forma de DP (programación dinámica) tiene dos propiedades:

  1. Subestructura óptima — la respuesta óptima al problema completo puede construirse a partir de las respuestas óptimas a versiones más pequeñas del mismo problema.
  2. Subproblemas superpuestos — esas versiones más pequeñas se repiten. La recursión ingenua resolvería la misma muchas veces.

Cuando ambas se cumplen, el truco es resolver cada subproblema exactamente una vez y guardar el resultado. Dos variantes:

Ambas son O(estados × trabajo-por-estado). La memoización está más cerca de la recurrencia natural; la tabulación te da un control más fino sobre el espacio y el orden.

Puntos clave

Profundizando

Reconocer una forma de DP

Cinco preguntas que hacerle a cualquier problema que sospeches que es DP:

  1. ¿Puedo describir la respuesta recursivamente? "El costo mínimo para llegar al estado X es igual al mínimo, entre las opciones Y, de …"
  2. ¿Cuál es el estado? ¿Un solo entero? ¿Un par (i, j)? ¿Un índice más una bandera?
  3. ¿Cuál es la recurrencia? ¿Cómo se descompone f(estado) en f(estado-más-pequeño)?
  4. ¿Cuáles son los casos base? Los estados más pequeños cuya respuesta se conoce sin recursión.
  5. ¿En qué orden debo llenar la tabla? Cualquier orden donde las dependencias de cada celda estén llenas antes que ella.

Si puedes responder las cinco, puedes escribir la DP. Si no puedes, todavía no tienes un problema de DP — vuelve a reformularlo.

Memoización vs tabulación — cuándo elegir cuál

| | Memoización | Tabulación | |---|---|---| | Modelo mental | De arriba hacia abajo: confía en la recursión | De abajo hacia arriba: construye pieza por pieza | | Implementación | Función recursiva + caché | Bucle que llena un arreglo | | Subproblemas resueltos | Solo los alcanzables | Todos ellos | | Riesgo de pila | Recursión profunda → desbordamiento de pila | Ninguno | | Potencial de optimización de espacio | Más difícil | Fácil (filas deslizantes / "los dos últimos valores") |

Elige la memoización cuando el espacio de estados es disperso — existen muchos estados pero la respuesta solo depende de un pequeño conjunto alcanzable. Elige la tabulación cuando de todos modos tocarás la mayor parte del espacio de estados, o cuando necesitas optimizar el espacio.

Optimización de espacio — el truco del "deslizamiento"

La mayoría de las recurrencias de DP solo dependen de la fila inmediatamente anterior o de una cantidad constante de celdas previas. Así que no necesitas la tabla completa.

Observa la recurrencia: si f(i) solo usa f(i-1), f(i-2), …, f(i-k) para un k pequeño, puedes conservar solo esos k valores e ir deslizándote.

Anclaje en robótica — DP es Bellman es iteración de valor

La ecuación de Bellman en aprendizaje por refuerzo es literalmente una recurrencia de DP:

V*(s) = max over actions a of [ R(s, a) + γ · Σ P(s' | s, a) · V*(s') ]

El valor óptimo del estado s es igual a la recompensa inmediata más el valor futuro descontado del mejor estado siguiente. La iteración de valor no es más que tabular la recurrencia de Bellman sobre el espacio de estados, iterada hasta la convergencia. Todo solucionador de mundos de cuadrícula (gridworld), todo planificador de rutas que maneja costos, todo controlador MPC del libro de texto — son DP.

Cuándo falla la DP

La DP necesita subestructura óptima. Algunos problemas la violan:

Si no puedes encontrar un estado finito que capture todo lo que los pasos futuros necesitan saber, la DP no aplica.

En la práctica

Detalles matemáticos

Tiempo
O(estados × trabajo-por-estado) — una vez que has elegido una representación del estado, esto es exacto.
Espacio (tabla completa)
O(estados)
Espacio (deslizante)
A menudo O(d) donde d es la "profundidad de retroceso" de la recurrencia — usualmente una pequeña constante o una fila.
Sobrecarga de la memoización
Búsqueda en HashMap + pila de recursión — factor constante de 2-3× sobre un bucle de tabulación ajustado. Vale la pena para espacios de estados dispersos.

Implementación

El esqueleto — toda DP encaja en esta plantilla

def solve():
    memo = {}
    def f(state):
        if state in BASE_CASES: return base_answer
        if state in memo: return memo[state]
        result = combine(f(smaller_state_1), f(smaller_state_2), ...)
        memo[state] = result
        return result
    return f(initial_state)

O, tabulado:

def solve():
    dp = [base_answer for _ in range(N + 1)]
    for state in range(SMALLEST + 1, LARGEST + 1):
        dp[state] = combine(dp[smaller_1], dp[smaller_2], ...)
    return dp[LARGEST]

Formas clásicas de problemas

Práctica

Tres problemas que afianzan las tres formas fundamentales de DP:

Explora todos los problemas de práctica