¿Cuál es la diferencia entre bottom-up y top-down?

177

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?

Invitado
fuente

Respuestas:

247

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 fibfunció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
ninjagecko
fuente
3
@ coder000001: para ejemplos de python, puede buscar en google 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é)".
ninjagecko
16
No veo a nadie que mencione esto, pero creo que otra ventaja de Top down es que solo construirá escasamente la tabla de búsqueda / caché. (es decir, completa los valores donde realmente los necesita). Por lo tanto, esto podría ser una ventaja, además de una fácil codificación. En otras palabras, de arriba hacia abajo podría ahorrarle tiempo de ejecución real ya que no calcula todo (es posible que tenga un tiempo de ejecución tremendamente mejor pero el mismo tiempo de ejecución asintótico). Sin embargo, requiere memoria adicional para mantener los marcos de pila adicionales (de nuevo, el consumo de memoria 'puede' (solo puede) duplicarse pero asintóticamente es lo mismo.
InformadoA
2
Tengo la impresión de que los enfoques descendentes de que las soluciones de caché para los subproblemas superpuestos son una técnica llamada memorización . Una técnica de abajo hacia arriba que llena una tabla y también evita volver a calcular los subproblemas superpuestos se conoce como tabulación . Estas técnicas se pueden emplear cuando se usa programación dinámica , que se refiere a la resolución de subproblemas para resolver un problema mucho mayor. Esto parece contradictorio con esta respuesta, donde esta respuesta utiliza programación dinámica en lugar de tabulación en muchos lugares. Quien esta correcto?
Sammaron
1
@Sammaron: hmm, haces un buen punto. Quizás debería haber revisado mi fuente en Wikipedia, que no puedo encontrar. Al verificar un poco cstheory.stackexchange, ahora estoy de acuerdo "de abajo hacia arriba" implicaría que el fondo se conoce de antemano (tabulación), y "de arriba hacia abajo" es que asume la solución a subproblemas / subárboles. En ese momento encontré el término ambiguo e interpreté las frases en la vista dual ("de abajo hacia arriba", asumes la solución a los subproblemas y memorizas, "de arriba hacia abajo", sabes de qué subproblemas estás y puedes tabular). Intentaré abordar esto en una edición.
ninjagecko
1
@mgiuffrida: el espacio de pila a veces se trata de manera diferente según el lenguaje de programación. Por ejemplo, en python, intentar realizar una fib recursiva memorable fallará, por ejemplo 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).
ninjagecko
76

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.

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

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.

Rob Neuhaus
fuente
1
Ah, ahora veo lo que significa "de arriba hacia abajo" y "de abajo hacia arriba"; de hecho, solo se refiere a la memorización vs DP. Y pensar que yo era el que edita la pregunta mencionar DP en el título ...
ninjagecko
¿Cuál es el tiempo de ejecución de la fibra recursiva normal memorizada fib v / s?
Siddhartha
exponencial (2 ^ n) para normal porque es un árbol de recursión, creo.
Siddhartha
1
Sí, es lineal! Dibujé el árbol de recursión y vi qué llamadas se podían evitar y me di cuenta de que las llamadas memo_fib (n - 2) se evitarían después de la primera llamada, por lo que todas las ramas correctas del árbol de recursión se cortarían y se Reduciré a lineal.
Siddhartha
1
Dado que DP implica esencialmente construir una tabla de resultados donde cada resultado se calcula como máximo una vez, una forma simple de visualizar el tiempo de ejecución de un algoritmo DP es ver qué tan grande es la tabla. En este caso, es de tamaño n (un resultado por valor de entrada), entonces O (n). En otros casos, podría ser una matriz n ^ 2, resultando en O (n ^ 2), etc.
Johnson Wong
22

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.

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

Usa tu idioma favorito e intenta ejecutarlo fib(50). Tomará mucho, mucho tiempo. ¡Aproximadamente tanto tiempo como él fib(50)mismo! Sin embargo, se está haciendo mucho trabajo innecesario. fib(50)llamará fib(49)y fib(48), pero ambos terminarán llamando fib(47), aunque el valor sea el mismo. De hecho, fib(47)se calculará tres veces: por una llamada directa de fib(49), por una llamada directa de fib(48), y también por una llamada directa de otro fib(48), el que se generó por el cálculo de fib(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.

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

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 .

osa
fuente
¡¡¡Hola!!! Quiero determinar si las siguientes proposiciones son correctas. - Para un algoritmo de programación dinámica, el cálculo de todos los valores con ascendente es asintóticamente más rápido que el uso de recursividad y memorización. - El tiempo de un algoritmo dinámico es siempre Ο (Ρ) donde Ρ es el número de subproblemas. - Cada problema en NP se puede resolver en tiempo exponencial.
Mary Star
¿Qué podría decir sobre las proposiciones anteriores? ¿Tienes una idea? @osa
Mary Star
@evinda, (1) siempre está mal. Es lo mismo o asintóticamente más lento (cuando no necesita todos los subproblemas, la recursión puede ser más rápida). (2) solo es correcto si puede resolver cada subproblema en O (1). (3) es un poco correcto. Cada problema en NP se puede resolver en tiempo polinomial en una máquina no determinista (como una computadora cuántica, que puede hacer varias cosas simultáneamente: tener su pastel, comerlo simultáneamente y rastrear ambos resultados). Entonces, en cierto sentido, cada problema en NP se puede resolver en tiempo exponencial en una computadora normal. Nota de seguridad: todo en P también está en NP. Por ejemplo, agregar dos enteros
osa
19

Tomemos como ejemplo las series de Fibonacci

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

Otra forma de decirlo,

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

En el caso de los primeros cinco números de Fibonacci

Bottom(first) number :1
Top (fifth) number: 5 

Ahora echemos un vistazo al algoritmo recursivo de la serie de Fibonacci como ejemplo

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

Ahora si ejecutamos este programa con los siguientes comandos

rcursive(5);

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.

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

Y ejecutamos este método como el siguiente

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

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,

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

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.

minhaz
fuente
¿Podemos decir que el enfoque ascendente a menudo se implementa de una manera no recursiva?
Lewis Chan
No, puedes convertir cualquier lógica de bucle en recursividad
Ashvin Sharma
3

¡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

A diferencia de Memoization, que resuelve solo los subproblemas necesarios

  1. DP tiene el potencial de transformar soluciones de fuerza bruta de tiempo exponencial en algoritmos de tiempo polinomial.

  2. DP puede ser mucho más eficiente porque es iterativo

Por el contrario, Memoization debe pagar los gastos generales (a menudo significativos) debido a la recurrencia.

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.

Farah Nazifa
fuente
44
eso no es cierto, la memoria utiliza un caché que lo ayudará a ahorrar la complejidad del tiempo al mismo tiempo que DP
InformadoA
3

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
1

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:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

Puede pensar en su implementación recursiva en su hogar. Es bastante bueno y desafiante si no has resuelto algo como esto antes.

piyush121
fuente
1

De arriba hacia abajo : realizar un seguimiento del valor calculado hasta ahora y devolver el resultado cuando se cumple la condición base.

int n = 5;
fibTopDown(1, 1, 2, n);

private int fibTopDown(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibTopDown(j, i + j, count + 1, n);
}

De abajo hacia arriba : el resultado actual depende del resultado de su subproblema.

int n = 5;
fibBottomUp(n);

private int fibBottomUp(int n) {
    if (n <= 1) return 1;
    return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}
Ashwin
fuente