rev4: Un comentario muy elocuente del usuario Sammaron ha señalado que, tal vez, esta respuesta anteriormente confundía de arriba hacia abajo y de abajo hacia arriba. Si bien originalmente esta respuesta (rev3) y otras respuestas decían que "de abajo hacia arriba es la memorización" ("asumir los subproblemas"), puede ser el inverso (es decir, "de arriba hacia abajo" puede ser "asumir los subproblemas" y " de abajo hacia arriba "puede ser" componga los subproblemas "). Anteriormente, he leído que la memorización es un tipo diferente de programación dinámica en lugar de un subtipo de programación dinámica. Estaba citando ese punto de vista a pesar de no suscribirme a él. He reescrito esta respuesta para ser agnóstico de la terminología hasta que se puedan encontrar referencias adecuadas en la literatura. También he convertido esta respuesta a una wiki comunitaria. Por favor, prefiera las fuentes académicas. Lista de referencias:} {Literatura:5 }
Resumen
La programación dinámica se trata de ordenar sus cálculos de una manera que evite recalcular el trabajo duplicado. Tiene un problema principal (la raíz de su árbol de subproblemas) y subproblemas (subárboles). Los subproblemas generalmente se repiten y se superponen .
Por ejemplo, considere su ejemplo favorito de Fibonnaci. Este es el árbol completo de subproblemas, si hicimos una llamada recursiva ingenua:
TOP of the tree
fib(4)
fib(3)...................... + fib(2)
fib(2)......... + fib(1) fib(1)........... + fib(0)
fib(1) + fib(0) fib(1) fib(1) fib(0)
fib(1) fib(0)
BOTTOM of the tree
(En algunos otros problemas raros, este árbol podría ser infinito en algunas ramas, lo que representa la no terminación y, por lo tanto, la parte inferior del árbol puede ser infinitamente grande. Además, en algunos problemas es posible que no sepa cómo se ve el árbol completo antes de tiempo. Por lo tanto, es posible que necesite una estrategia / algoritmo para decidir qué subproblemas revelar).
Memoization, Tabulación
Existen al menos dos técnicas principales de programación dinámica que no son mutuamente excluyentes:
Memorización: este es un enfoque de laissez-faire: supone que ya ha calculado todos los subproblemas y que no tiene idea de cuál es el orden óptimo de evaluación. Por lo general, realizaría una llamada recursiva (o algún equivalente iterativo) desde la raíz y esperaría acercarse al orden de evaluación óptimo u obtener una prueba de que lo ayudará a llegar al orden de evaluación óptimo. Se aseguraría de que la llamada recursiva nunca vuelva a calcular un subproblema porque almacena en caché los resultados y, por lo tanto, no se vuelven a calcular los subárboles duplicados.
- ejemplo: si está calculando la secuencia de Fibonacci
fib(100)
, simplemente llamaría a esto, y llamaría fib(100)=fib(99)+fib(98)
, que llamaría fib(99)=fib(98)+fib(97)
, ... etc ..., que llamaría fib(2)=fib(1)+fib(0)=1+0=1
. Entonces finalmente se resolvería fib(3)=fib(2)+fib(1)
, pero no es necesario volver a calcular fib(2)
, porque lo almacenamos en caché.
- Esto comienza en la parte superior del árbol y evalúa los subproblemas de las hojas / subárboles hacia la raíz.
Tabulación: también puede pensar en la programación dinámica como un algoritmo de "relleno de tabla" (aunque generalmente es multidimensional, esta 'tabla' puede tener geometría no euclidiana en casos muy raros *). Esto es como la memorización pero más activa, e implica un paso adicional: debe elegir, con anticipación, el orden exacto en el que hará sus cálculos. Esto no debería implicar que el pedido debe ser estático, sino que tiene mucha más flexibilidad que la memorización.
- ejemplo: Si va a realizar de Fibonacci, es posible elegir calcular los números en este orden:
fib(2)
, fib(3)
,fib(4)
... caché de todos los valores para que pueda calcular las próximas con mayor facilidad. También puede pensar que está llenando una tabla (otra forma de almacenamiento en caché).
- Personalmente no escucho mucho la palabra 'tabulación', pero es un término muy decente. Algunas personas consideran esta "programación dinámica".
- Antes de ejecutar el algoritmo, el programador considera todo el árbol, luego escribe un algoritmo para evaluar los subproblemas en un orden particular hacia la raíz, generalmente completando una tabla.
- * nota al pie: a veces la 'tabla' no es una tabla rectangular con conectividad similar a la cuadrícula, per se. Por el contrario, puede tener una estructura más complicada, como un árbol, o una estructura específica para el dominio del problema (por ejemplo, ciudades dentro de una distancia de vuelo en un mapa), o incluso un diagrama de enrejado, que, aunque en forma de cuadrícula, no tiene una estructura de conectividad arriba-abajo-izquierda-derecha, etc. Por ejemplo, user3290797 vinculó un ejemplo de programación dinámica de encontrar el conjunto independiente máximo en un árbol , que corresponde a completar los espacios en blanco en un árbol.
(En general, en un paradigma de "programación dinámica", diría que el programador considera todo el árbol, luegoescribe un algoritmo que implementa una estrategia para evaluar subproblemas que puede optimizar las propiedades que desee (generalmente una combinación de complejidad de tiempo y complejidad de espacio). Su estrategia debe comenzar en algún lugar, con algún subproblema particular, y tal vez pueda adaptarse en función de los resultados de esas evaluaciones. En el sentido general de "programación dinámica", puede intentar almacenar en caché estos subproblemas y, de manera más general, evitar volver a visitar los subproblemas con una sutil distinción, tal vez el caso de los gráficos en varias estructuras de datos. Muy a menudo, estas estructuras de datos están en su núcleo como matrices o tablas. Las soluciones a los subproblemas pueden descartarse si ya no las necesitamos).
[Anteriormente, esta respuesta hacía una declaración sobre la terminología de arriba hacia abajo frente a abajo hacia arriba; claramente hay dos enfoques principales llamados Memoization y Tabulation que pueden estar en biyección con esos términos (aunque no del todo). El término general que usa la mayoría de la gente sigue siendo "Programación dinámica" y algunas personas dicen "Memoization" para referirse a ese subtipo particular de "Programación dinámica". Esta respuesta se niega a decir cuál es de arriba hacia abajo y de abajo hacia arriba hasta que la comunidad pueda encontrar referencias adecuadas en los documentos académicos. En última instancia, es importante comprender la distinción más que la terminología.]
Pros y contras
Facilidad de codificación
La memorización es muy fácil de codificar (generalmente puede * escribir una anotación "memoradora" o una función de envoltura que lo hace automáticamente por usted), y debería ser su primera línea de enfoque. La desventaja de la tabulación es que tienes que hacer un pedido.
* (esto en realidad solo es fácil si está escribiendo la función usted mismo y / o codificando en un lenguaje de programación impuro / no funcional ... por ejemplo, si alguien ya escribió una fib
función precompilada , necesariamente hace llamadas recursivas para sí mismo, y no puede memorizar mágicamente la función sin asegurarse de que esas llamadas recursivas llamen a su nueva función memorizada (y no a la función original no conmemorada)
Recursividad
Tenga en cuenta que tanto de arriba hacia abajo como de abajo hacia arriba se pueden implementar con relleno de tabla recurrente o iterativo, aunque puede no ser natural.
Preocupaciones prácticas
Con la memorización, si el árbol es muy profundo (por ejemplo fib(10^6)
), se quedará sin espacio en la pila, porque cada cálculo retrasado debe colocarse en la pila, y tendrá 10 ^ 6 de ellos.
Óptima
Cualquiera de los dos enfoques puede no ser óptimo en el tiempo si el orden en que visita (o intenta) visitar subproblemas no es óptimo, específicamente si hay más de una forma de calcular un subproblema (normalmente el almacenamiento en caché resolvería esto, pero es teóricamente posible que el almacenamiento en caché pueda no en algunos casos exóticos). La memorización generalmente agregará su complejidad de tiempo a su complejidad de espacio (por ejemplo, con la tabulación tiene más libertad para tirar los cálculos, como usar la tabulación con Fib le permite usar el espacio O (1), pero la memorización con Fib usa O (N) pila de espacio).
Optimizaciones avanzadas
Si también está haciendo problemas extremadamente complicados, es posible que no tenga más remedio que hacer la tabulación (o al menos desempeñar un papel más activo en dirigir la memorización a donde quiere que vaya). Además, si se encuentra en una situación en la que la optimización es absolutamente crítica y debe optimizar, la tabulación le permitirá realizar optimizaciones que, de otro modo, la memorización no le permitiría hacer de una manera sensata. En mi humilde opinión, en la ingeniería de software normal, ninguno de estos dos casos aparece, así que simplemente usaría la memorización ("una función que almacena sus respuestas") a menos que algo (como el espacio de pila) haga necesaria la tabulación ... técnicamente para evitar una explosión de la pila, puede 1) aumentar el límite de tamaño de la pila en los idiomas que lo permiten, o 2) comer un factor constante de trabajo adicional para virtualizar su pila (ick),
Ejemplos más complicados
Aquí enumeramos ejemplos de particular interés, que no son solo problemas generales de DP, sino que distinguen de manera interesante la memorización y la tabulación. Por ejemplo, una formulación puede ser mucho más fácil que la otra, o puede haber una optimización que básicamente requiere tabulación:
- el algoritmo para calcular la distancia de edición [ 4 ], interesante como un ejemplo no trivial de un algoritmo de relleno de tabla bidimensional