seegongsik
Algoritmos

Cómo funciona la programación dinámica

¿Alguna vez calculaste los números de Fibonacci con recursión? Para obtener fib(5) hay que llamar a fib(4) y a fib(3), y fib(4) a su vez llama a fib(3) y a fib(2). Pero fib(2) aparece una y otra vez, aquí y allá. Se termina resolviendo lo mismo muchas veces. ¿Y si apuntaras cada respuesta la primera vez que la resuelves?

01

Resolver lo mismo otra vez

La recursión resuelve un problema grande partiéndolo en problemas más pequeños, y Fibonacci es el ejemplo perfecto.
fib(5) llama a fib(4) y a fib(3), y fib(4) llama a fib(3) y a fib(2). Pero si te fijas bien, fib(2) aparece una y otra vez en una rama tras otra.
Toca un nodo del árbol de abajo para comprobarlo.

Resolver fib(5) con recursion se despliega en este arbol. Toca un nodo.
Toca cualquier nodo. El mismo problema chico se esconde por todo el arbol.

El mismo problema pequeño aparece por todo el árbol. (toca un nodo → se resaltan todos los que tienen el mismo valor)

El mismo fib(2) se resuelve no una vez, sino varias, y lo mismo pasa con fib(3).
Cuando n crece, esta repetición se infla de forma alarmante.
Resolver una y otra vez una respuesta que ya tienes es, a todas luces, un desperdicio.

02

Apuntar las respuestas

La solución es sorprendentemente sencilla: apuntar en una nota cada respuesta una vez resuelta.
La próxima vez que necesites lo mismo, no lo vuelvas a calcular; tómalo de la nota. Calcula solo lo que ves por primera vez y reutiliza lo que ya has visto.
Toca el botón para atender las llamadas una a una.

La misma llamada vuelve a entrar. Toca para atender la siguiente llamada.
fib(2)
fib(3)
fib(2)
fib(4)
fib(2)
fib(3)
Bloc de nota
Aun vacio
Pulsa el boton y las llamadas entran una a una. Las nuevas se calculan, las ya vistas se sacan de la nota.

Lo nuevo se calcula y se apunta; lo ya visto se toma de la nota. Esto es la memoización.

Cada valor se calcula una sola vez, la primera. Todo lo demás sale directamente de la nota, así que resolver lo mismo dos veces desaparece por completo.
Apuntar las respuestas resueltas y reutilizarlas así se llama memoización.
El nombre suena pomposo, pero en el fondo es solo «apuntarlo».

03

Llenar una tabla de abajo arriba

También se puede aplicar la misma idea al revés. En vez de partir de arriba abajo, se llena una tabla hacia arriba, empezando por los valores más pequeños de abajo.
fib(0) y fib(1) se conocen sin más. Sumándolos sale fib(2), y sumando fib(1) y fib(2) sale fib(3).
Como las casillas de abajo ya están escritas, cada casilla de arriba es solo una suma.

Al reves: llena la tabla hacia arriba empezando por 0 y 1 abajo.
fib(0)0sabido desde el inicio
fib(1)1sabido desde el inicio
fib(2)?
fib(3)?
fib(4)?
fib(5)?
fib(6)?
fib(0)=0 y fib(1)=1 simplemente los sabemos. Desde aquí llenamos hacia arriba celda por celda.

Llena hacia arriba casilla por casilla, empezando por 0 y 1. (siguiente casilla = suma de las dos casillas justo debajo · de abajo arriba)

De esta forma ni siquiera hace falta una nota aparte, porque la tabla misma es la nota.
Basta con recorrerla una vez de abajo arriba, en orden, y nunca hay nada que volver a resolver.
Si la memoización es «apuntar cuando hace falta», esto es «apuntarlo todo de antemano». La idea de fondo es la misma.

04

Muchísimo más rápido

¿Y cuánto más rápido se vuelve? Solo con apuntar las cosas, la diferencia es enorme.
La recursión pura resuelve incluso los repetidos, así que su trabajo explota a medida que crece n. Con memoización, cada casilla se resuelve exactamente una vez, y el trabajo crece solo en proporción a n.
Si haces crecer n y comparas las dos, la diferencia salta a la vista.

Aumenta n y compara cuanto trabajo hace cada via.
n = 5
Recursion pura15 resuelve
Memo6 resuelve
Para fib(5), la recursion pura resuelve 15 veces, el memo resuelve 6 veces. Una brecha de 3x. Cuanto mas grande es n, mas explota la brecha.

Número de veces que se resuelve con el mismo n. (recursión pura = explota de forma exponencial · memo = lineal, proporcional a n)

Con que n crezca un poco, la barra de la recursión pura se dispara como si fuera a atravesar la pantalla, mientras que la de la memo apenas se mueve.
No volver a resolver la misma respuesta: esa sola cosa separa lo lento de lo rápido.

05

Resumen

La programación dinámica tiene un nombre difícil, pero en el fondo son tres pasos.
Darse cuenta de que se resuelve lo mismo otra vez, apuntar cada respuesta una vez resuelta y reutilizarla, con lo que todo se vuelve muchísimo más rápido.
Toca las tarjetas de abajo una a una para resumir los tres pasos.

La programacion dinamica son solo tres pasos. Toca para revelarlos uno a uno.
1
Resolver de nuevo
La recursion resuelve el mismo problema chico una y otra vez
2
Anotarlo
Apunta una respuesta resuelta en una nota y la sacas otra vez
3
Mucho mas rapido
Lo exponencial se vuelve lineal, la brecha explota
Pulsa el boton para revelar los tres pasos uno a uno.

Resolver lo mismo otra vez → apuntarlo → muchísimo más rápido. También recuerda por qué en divide y vencerás, cuyos trozos nunca se superponen, no hacía falta apuntar nada.

En divide y vencerás, que vimos antes, los trozos partidos no se superponían, así que no había nada que apuntar.
Ahí está justamente la bifurcación. Cuando los trozos se superponen, es decir, cuando el mismo problema pequeño reaparece una y otra vez, la sencilla jugada de apuntar lo cambia todo.

En una líneaLa recursión a menudo resuelve el mismo problema pequeño una y otra vez; basta mirar el árbol de Fibonacci para ver fib(2) por todas partes. Si apuntas cada respuesta una vez resuelta y la reutilizas, nunca resuelves lo mismo dos veces. Eso es la memoización. Llenar una tabla hacia arriba desde los valores más pequeños es la misma idea al revés. Así, lo que era exponencialmente lento se vuelve lineal y muchísimo más rápido. En divide y vencerás los trozos no se superponían, así que no había nada que apuntar, pero cuando se superponen, apuntar las respuestas lo cambia todo.
← Algoritmos
¿Te fue útil? Apoyar seegongsik