El enfoque de abajo hacia arriba (para la programación dinámica) consiste en mirar primero los subproblemas "más pequeños" y luego resolver los subproblemas más grandes utilizando la solución a los problemas más pequeños.
El top-down consiste en resolver el problema de manera "natural" y verificar si ha calculado la solución del subproblema anteriormente.
Estoy un poco confundida. ¿Cuál es la diferencia entre estos dos?
dynamic-programming
difference
memoization
Invitado
fuente
fuente
Respuestas:
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:
(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.
fib(100)
, simplemente llamaría a esto, y llamaríafib(100)=fib(99)+fib(98)
, que llamaríafib(99)=fib(98)+fib(97)
, ... etc ..., que llamaríafib(2)=fib(1)+fib(0)=1+0=1
. Entonces finalmente se resolveríafib(3)=fib(2)+fib(1)
, pero no es necesario volver a calcularfib(2)
, porque lo almacenamos en caché.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.
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é).(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:
fuente
python memoization decorator
; algunos idiomas le permitirán escribir una macro o código que encapsula el patrón de memorización. El patrón de memorización no es más que "en lugar de llamar a la función, buscar el valor de un caché (si el valor no está allí, calcularlo y agregarlo primero al caché)".fib(513)
. La terminología sobrecargada que siento se está interponiendo en el camino aquí. 1) Siempre puedes tirar los subproblemas que ya no necesitas. 2) Siempre puede evitar calcular subproblemas que no necesita. 3) 1 y 2 pueden ser mucho más difíciles de codificar sin una estructura de datos explícita para almacenar subproblemas, O, más difícil si el flujo de control debe tejerse entre las llamadas a funciones (es posible que necesite estado o continuación).DP de arriba hacia abajo y de abajo hacia arriba son dos formas diferentes de resolver los mismos problemas. Considere una solución de programación memorable (de arriba abajo) frente a dinámica (de abajo hacia arriba) para calcular los números de Fibonacci.
Personalmente, la memorización me parece mucho más natural. Puede tomar una función recursiva y memorizarla mediante un proceso mecánico (primero busque la respuesta en la memoria caché y devuélvala si es posible; de lo contrario, calcule recursivamente y luego, antes de regresar, guarde el cálculo en la memoria caché para usarlo en el futuro). La programación dinámica requiere que codifique un orden en el que se calculan las soluciones, de modo que no se calcule ningún "gran problema" antes del problema menor del que depende.
fuente
Una característica clave de la programación dinámica es la presencia de subproblemas superpuestos . Es decir, el problema que está tratando de resolver puede dividirse en subproblemas, y muchos de esos subproblemas comparten subproblemas. Es como "Divide y vencerás", pero terminas haciendo lo mismo muchas, muchas veces. Un ejemplo que he usado desde 2003 al enseñar o explicar estos asuntos: puedes calcular los números de Fibonacci de forma recursiva.
Usa tu idioma favorito e intenta ejecutarlo
fib(50)
. Tomará mucho, mucho tiempo. ¡Aproximadamente tanto tiempo como élfib(50)
mismo! Sin embargo, se está haciendo mucho trabajo innecesario.fib(50)
llamaráfib(49)
yfib(48)
, pero ambos terminarán llamandofib(47)
, aunque el valor sea el mismo. De hecho,fib(47)
se calculará tres veces: por una llamada directa defib(49)
, por una llamada directa defib(48)
, y también por una llamada directa de otrofib(48)
, el que se generó por el cálculo defib(49)
... Así que ya ves, tenemos subproblemas superpuestos .Buenas noticias: no es necesario calcular el mismo valor muchas veces. Una vez que lo calcula una vez, guarde en caché el resultado y la próxima vez use el valor en caché. Esta es la esencia de la programación dinámica. Puede llamarlo "de arriba hacia abajo", "memorización" o cualquier otra cosa que desee. Este enfoque es muy intuitivo y muy fácil de implementar. Simplemente escriba una solución recursiva primero, pruébela en pruebas pequeñas, agregue memorización (almacenamiento en caché de valores ya calculados) y --- ¡bingo! --- estás listo.
Por lo general, también puede escribir un programa iterativo equivalente que funcione de abajo hacia arriba, sin recurrencia. En este caso, este sería el enfoque más natural: recorrer de 1 a 50 calculando todos los números de Fibonacci a medida que avanza.
En cualquier escenario interesante, la solución ascendente suele ser más difícil de entender. Sin embargo, una vez que lo comprenda, generalmente obtendrá una imagen general mucho más clara de cómo funciona el algoritmo. En la práctica, al resolver problemas no triviales, recomiendo primero escribir el enfoque de arriba hacia abajo y probarlo en pequeños ejemplos. Luego escriba la solución de abajo hacia arriba y compare las dos para asegurarse de que está obteniendo lo mismo. Idealmente, compare las dos soluciones automáticamente. Escriba una pequeña rutina que generaría muchas pruebas, idealmente, todaspruebas pequeñas hasta cierto tamaño --- y valida que ambas soluciones den el mismo resultado. Después de eso, use la solución de abajo hacia arriba en producción, pero mantenga el código de arriba hacia abajo, comentó. Esto facilitará que otros desarrolladores entiendan qué es lo que está haciendo: el código ascendente puede ser bastante incomprensible, incluso si lo escribió e incluso si sabe exactamente lo que está haciendo.
En muchas aplicaciones, el enfoque ascendente es ligeramente más rápido debido a la sobrecarga de llamadas recursivas. El desbordamiento de pila también puede ser un problema en ciertos problemas, y tenga en cuenta que esto puede depender mucho de los datos de entrada. En algunos casos, es posible que no pueda escribir una prueba que provoque un desbordamiento de la pila si no comprende la programación dinámica lo suficientemente bien, pero algún día esto puede suceder.
Ahora, hay problemas donde el enfoque de arriba hacia abajo es la única solución posible porque el espacio del problema es tan grande que no es posible resolver todos los subproblemas. Sin embargo, el "almacenamiento en caché" todavía funciona en un tiempo razonable porque su entrada solo necesita una fracción de los subproblemas para resolverse, pero es demasiado difícil de definir explícitamente, qué subproblemas necesita resolver y, por lo tanto, escribir un fondo. hasta la solución Por otro lado, hay situaciones en las que sabe que deberá resolver todos los subproblemas. En este caso, continúe y use de abajo hacia arriba.
Personalmente, usaría de arriba a abajo para la optimización de párrafo, también conocido como el problema de optimización de ajuste de Word (busque los algoritmos de salto de línea de Knuth-Plass; al menos TeX lo usa, y algunos softwares de Adobe Systems usan un enfoque similar). Usaría de abajo hacia arriba para la Transformada rápida de Fourier .
fuente
Tomemos como ejemplo las series de Fibonacci
Otra forma de decirlo,
En el caso de los primeros cinco números de Fibonacci
Ahora echemos un vistazo al algoritmo recursivo de la serie de Fibonacci como ejemplo
Ahora si ejecutamos este programa con los siguientes comandos
Si observamos detenidamente el algoritmo, para generar el quinto número, se requiere un tercer y un cuarto número. Entonces, mi recursión en realidad comienza desde arriba (5) y luego llega a números inferiores / inferiores. Este enfoque es en realidad un enfoque de arriba hacia abajo.
Para evitar hacer el mismo cálculo varias veces, utilizamos técnicas de programación dinámica. Almacenamos el valor previamente calculado y lo reutilizamos. Esta técnica se llama memorización. Hay más en la programación dinámica además de la memorización que no es necesaria para discutir el problema actual.
De arriba hacia abajo
Reescribamos nuestro algoritmo original y agreguemos técnicas memorables.
Y ejecutamos este método como el siguiente
Esta solución sigue siendo de arriba hacia abajo ya que el algoritmo comienza desde el valor superior y va al final de cada paso para obtener nuestro valor superior.
De abajo hacia arriba
Pero, la pregunta es, ¿podemos comenzar desde abajo, como desde el primer número de Fibonacci y luego caminar hacia arriba? Vamos a reescribirlo usando estas técnicas,
Ahora, si analizamos este algoritmo, en realidad comienza con valores más bajos y luego va al principio. Si necesito el quinto número de Fibonacci, en realidad estoy calculando el primero, luego el segundo y el tercero hasta el quinto número. Estas técnicas en realidad se llaman técnicas de abajo hacia arriba.
Los dos últimos, los algoritmos de llenado completo de requisitos de programación dinámica. Pero uno es de arriba hacia abajo y otro es de abajo hacia arriba. Ambos algoritmos tienen una complejidad de espacio y tiempo similar.
fuente
¡La programación dinámica a menudo se llama memorización!
1. La memorización es la técnica de arriba hacia abajo (comienza a resolver el problema dado desglosándolo) y la programación dinámica es una técnica de abajo hacia arriba (comienza a resolver desde el subproblema trivial, hacia el problema dado)
2.DP encuentra la solución comenzando desde el (los) caso (s) base (s) y avanza hacia arriba. DP resuelve todos los subproblemas porque lo hace de abajo hacia arriba
DP tiene el potencial de transformar soluciones de fuerza bruta de tiempo exponencial en algoritmos de tiempo polinomial.
DP puede ser mucho más eficiente porque es iterativo
Para ser más simple, Memoization utiliza el enfoque de arriba hacia abajo para resolver el problema, es decir, comienza con el problema central (principal), luego lo divide en subproblemas y resuelve estos subproblemas de manera similar. En este enfoque, el mismo subproblema puede ocurrir varias veces y consumir más ciclos de CPU, por lo tanto, aumentar la complejidad del tiempo. Mientras que en la programación dinámica, el mismo subproblema no se resolverá varias veces, pero el resultado anterior se utilizará para optimizar la solución.
fuente
Simplemente decir que el enfoque de arriba hacia abajo usa la recursividad para llamar a los problemas Sub una y otra vez,
mientras que el enfoque de abajo hacia arriba usa el sencillo sin llamar a nadie y, por lo tanto, es más eficiente.
fuente
La siguiente es la solución basada en DP para el problema Editar distancia, que es de arriba hacia abajo. Espero que también ayude a comprender el mundo de la programación dinámica:
Puede pensar en su implementación recursiva en su hogar. Es bastante bueno y desafiante si no has resuelto algo como esto antes.
fuente
De arriba hacia abajo : realizar un seguimiento del valor calculado hasta ahora y devolver el resultado cuando se cumple la condición base.
De abajo hacia arriba : el resultado actual depende del resultado de su subproblema.
fuente