Qual é a diferença entre ascendente e descendente?


177

A abordagem de baixo para cima (para programação dinâmica) consiste em examinar primeiro os subproblemas "menores" e depois resolver os subproblemas maiores usando a solução para os problemas menores.

O top-down consiste em resolver o problema de uma maneira "natural" e verificar se você já calculou a solução para o subproblema antes.

Estou um pouco confuso. Qual é a diferença entre esses dois?


Respostas:


247

rev4: Um comentário muito eloqüente do usuário Sammaron observou que, talvez, essa resposta tenha anteriormente confundido de cima para baixo e de baixo para cima. Embora originalmente esta resposta (rev3) e outras respostas dissessem que "de baixo para cima é memoização" ("assuma os subproblemas"), pode ser o inverso (ou seja, "de cima para baixo" pode ser "assuma os subproblemas" e " de baixo para cima "pode ​​ser" compor os subproblemas "). Anteriormente, eu li que a memoização é um tipo diferente de programação dinâmica, em oposição a um subtipo de programação dinâmica. Eu estava citando esse ponto de vista, apesar de não assinar. Reescrevi esta resposta como independente da terminologia até que as referências apropriadas possam ser encontradas na literatura. Também converti esta resposta em um wiki da comunidade. Por favor, prefira fontes acadêmicas. Lista de referências:} {Literatura: 5 }

Recapitular

A programação dinâmica é sobre ordenar seus cálculos de uma maneira que evite recalcular o trabalho duplicado. Você tem um problema principal (a raiz da sua árvore de subproblemas) e subproblemas (subárvores). Os subproblemas geralmente se repetem e se sobrepõem .

Por exemplo, considere seu exemplo favorito de Fibonnaci. Esta é a árvore completa dos subproblemas, se fizermos uma chamada recursiva ingênua:

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

(Em alguns outros problemas raros, essa árvore pode ser infinita em alguns ramos, representando a não terminação e, portanto, o fundo da árvore pode ser infinitamente grande. Além disso, em alguns problemas, talvez você não saiba como é a árvore inteira antes de Assim, você pode precisar de uma estratégia / algoritmo para decidir quais subproblemas revelar.)


Memoização, Tabulação

Existem pelo menos duas técnicas principais de programação dinâmica que não são mutuamente exclusivas:

  • Memoização - Essa é uma abordagem de laissez-faire: você assume que já calculou todos os subproblemas e que não tem idéia de qual é a ordem de avaliação ideal. Normalmente, você faria uma chamada recursiva (ou algum equivalente iterativo) a partir da raiz e espera que você chegue perto da ordem de avaliação ideal ou obtenha uma prova de que o ajudará a chegar à ordem de avaliação ideal. Você garantiria que a chamada recursiva nunca recalcule um subproblema porque você armazena em cache os resultados e, portanto, as subárvores duplicadas não são recalculadas.

    • exemplo: Se você está calculando a sequência de Fibonacci fib(100), basta chamar isso, e chamaria fib(100)=fib(99)+fib(98), o que chamaria fib(99)=fib(98)+fib(97)... etc ..., o que chamaria fib(2)=fib(1)+fib(0)=1+0=1. Finalmente fib(3)=fib(2)+fib(1), ele resolveria , mas não precisará recalcular fib(2), porque o armazenamos em cache.
    • Isso começa no topo da árvore e avalia os subproblemas das folhas / subárvores de volta à raiz.
  • Tabulação - Você também pode pensar na programação dinâmica como um algoritmo de "preenchimento de tabela" (embora geralmente multidimensional, essa 'tabela' possa ter geometria não euclidiana em casos muito raros *). Isso é como memorização, mas mais ativo, e envolve uma etapa adicional: você deve escolher, com antecedência, a ordem exata em que fará seus cálculos. Isso não deve implicar que o pedido deve ser estático, mas que você tem muito mais flexibilidade do que a memorização.

    • exemplo: Se você estiver executando Fibonacci, você pode optar por calcular os números nesta ordem: fib(2), fib(3), fib(4)... cache todos os valores para que possa calcular os próximos mais facilmente. Você também pode pensar nisso como preencher uma tabela (outra forma de armazenamento em cache).
    • Pessoalmente, não ouço muito a palavra 'tabulação', mas é um termo muito decente. Algumas pessoas consideram essa "programação dinâmica".
    • Antes de executar o algoritmo, o programador considera a árvore inteira e depois escreve um algoritmo para avaliar os subproblemas em uma ordem específica em relação à raiz, geralmente preenchendo uma tabela.
    • * nota de rodapé: Às vezes, a 'tabela' não é uma tabela retangular com conectividade semelhante a uma grade, por si só. Em vez disso, pode ter uma estrutura mais complicada, como uma árvore ou uma estrutura específica para o domínio do problema (por exemplo, cidades a uma distância de vôo em um mapa) ou até mesmo um diagrama de treliça que, embora parecido com uma grade, não possui uma estrutura de conectividade de cima para baixo, esquerda, direita etc. Por exemplo, o usuário3290797 vinculou um exemplo de programação dinâmica de localização do conjunto independente máximo em uma árvore , o que corresponde ao preenchimento de espaços em branco em uma árvore.

(Em geral, em um paradigma de "programação dinâmica", eu diria que o programador considera a árvore inteira, entãoescreve um algoritmo que implementa uma estratégia para avaliar subproblemas que podem otimizar as propriedades desejadas (geralmente uma combinação de complexidade de tempo e complexidade de espaço). Sua estratégia deve começar em algum lugar, com algum subproblema específico, e talvez possa se adaptar com base nos resultados dessas avaliações. No sentido geral de "programação dinâmica", você pode tentar armazenar em cache esses subproblemas e, mais geralmente, evitar revisitar subproblemas com uma distinção sutil, talvez seja o caso de gráficos em várias estruturas de dados. Muitas vezes, essas estruturas de dados são essenciais como matrizes ou tabelas. As soluções para os subproblemas podem ser descartadas se não precisarmos mais delas.)

[Anteriormente, essa resposta fazia uma declaração sobre a terminologia de cima para baixo versus de baixo para cima; existem claramente duas abordagens principais chamadas Memoização e Tabulação que podem estar em desuso com esses termos (embora não inteiramente). O termo geral que a maioria das pessoas usa ainda é "Programação Dinâmica" e algumas pessoas dizem "Memoização" para se referir a esse subtipo específico de "Programação Dinâmica". Esta resposta se recusa a dizer qual é de cima para baixo e de baixo para cima até que a comunidade encontre referências apropriadas em trabalhos acadêmicos. Por fim, é importante entender a distinção e não a terminologia.]


Prós e contras

Facilidade de codificação

Memoização é muito fácil de codificar (geralmente você pode * escrever uma anotação "memoizer" ou uma função de invólucro que faz isso automaticamente por você) e deve ser sua primeira linha de abordagem. A desvantagem da tabulação é que você precisa criar um pedido.

* (isso é realmente fácil apenas se você estiver escrevendo a função e / ou codificando em uma linguagem de programação impura / não-funcional ... por exemplo, se alguém já escreveu uma fibfunção pré-compilada , ela necessariamente faz chamadas recursivas para si mesma e você não pode memorizar magicamente a função sem garantir que as chamadas recursivas chamem sua nova função memorizada (e não a função não memorizada original))

Recursividade

Observe que de cima para baixo e de baixo para cima podem ser implementadas com recursão ou preenchimento iterativo de tabela, embora possa não ser natural.

Preocupações práticas

Com a memorização, se a árvore for muito profunda (por exemplo fib(10^6)), você ficará sem espaço na pilha, porque cada cálculo atrasado deve ser colocado na pilha e você terá 10 ^ 6 deles.

Optimalidade

Qualquer uma das abordagens pode não ter o tempo ideal se a ordem em que você acontecer (ou tentar) visitar subproblemas não for ideal, especificamente se houver mais de uma maneira de calcular um subproblema (normalmente o cache resolveria isso, mas é teoricamente possível que o cache possa em alguns casos exóticos). Memoização geralmente adiciona complexidade de tempo à complexidade do espaço (por exemplo, com tabulação, você tem mais liberdade para jogar fora os cálculos, como usar tabulação com Fib permite usar o espaço O (1), mas a memoização com Fib usa O (N) espaço de pilha).

Otimizações avançadas

Se você também estiver enfrentando problemas extremamente complicados, poderá não ter outra opção a não ser fazer tabulação (ou pelo menos assumir um papel mais ativo na orientação da memorização para onde deseja que ela vá). Além disso, se você estiver em uma situação em que a otimização é absolutamente crítica e você deve otimizar, a tabulação permitirá que você faça otimizações que a memorização não permitiria de maneira sadia. Na minha humilde opinião, na engenharia de software normal, nenhum desses dois casos é apresentado, então eu usaria a memorização ("uma função que armazena em cache suas respostas"), a menos que algo (como o espaço da pilha) torne a tabulação necessária ... tecnicamente, para evitar uma explosão da pilha, você pode 1) aumentar o limite de tamanho da pilha nos idiomas que a permitem, ou 2) consumir um fator constante de trabalho extra para virtualizar sua pilha (ick),


Exemplos mais complicados

Aqui listamos exemplos de interesse particular, que não são apenas problemas gerais de DP, mas distinguem de forma interessante memoização e tabulação. Por exemplo, uma formulação pode ser muito mais fácil que a outra, ou pode haver uma otimização que requer basicamente tabulação:

  • o algoritmo para calcular a distância de edição [ 4 ], interessante como um exemplo não trivial de um algoritmo bidimensional de preenchimento de tabela

3
@ coder000001: para exemplos em python, você pode pesquisar no google python memoization decorator; alguns idiomas permitem que você escreva uma macro ou código que encapsule o padrão de memorização. O padrão de memorização nada mais é do que "em vez de chamar a função, procure o valor em um cache (se o valor não estiver lá, calcule-o e adicione-o ao cache primeiro)".
Ninjagecko

16
Não vejo ninguém mencionando isso, mas acho que outra vantagem do Top down é que você criará apenas a tabela / cache de pesquisa esparsamente. (ou seja, você preenche os valores onde realmente precisa deles). Portanto, esses podem ser os profissionais, além da codificação fácil. Em outras palavras, de cima para baixo pode economizar o tempo de execução real, pois você não calcula tudo (você pode ter um tempo de execução tremendamente melhor, mas o mesmo tempo de execução assintótico). No entanto, ele requer memória adicional para manter os quadros de pilha adicionais (mais uma vez, o consumo de memória 'pode' (somente pode) de casal, mas asymptotically é o mesmo.
InformedA

2
Tenho a impressão de que abordagens descendentes que armazenam soluções de cache em subproblemas sobrepostos são uma técnica chamada memorização . Uma técnica de baixo para cima que preenche uma tabela e também evita recalcular subproblemas sobrepostos é chamada de tabulação . Essas técnicas podem ser empregadas ao usar a programação dinâmica , que se refere à solução de subproblemas para resolver um problema muito maior. Isso parece contraditório com esta resposta, onde essa resposta usa programação dinâmica em vez de tabulação em muitos lugares. Quem está correto?
Sammaron 7/09/15

1
@ Sammaron: hmm, você faz um bom argumento. Talvez eu devesse ter verificado minha fonte na Wikipedia, que não consigo encontrar. Ao verificar cstheory.stackexchange um pouco, agora concordo que "de baixo para cima" implicaria que o fundo era conhecido de antemão (tabulação) e "de cima para baixo" é que você assume a solução para subproblemas / subárvores. Na época, achei o termo ambíguo e interpretei as frases na visão dupla ("de baixo para cima" você assume a solução para subproblemas e memoriza, "de cima para baixo" você sabe quais são os subproblemas que você pode tabular). Vou tentar resolver isso em uma edição.
Ninjagecko 08/09/2015

1
@mgiuffrida: Às vezes, o espaço da pilha é tratado de maneira diferente, dependendo da linguagem de programação. Por exemplo, em python, tentar executar uma fib recursiva memorizada falhará por dizer fib(513). A terminologia sobrecarregada que sinto está atrapalhando aqui. 1) Você sempre pode jogar fora os subproblemas que não são mais necessários. 2) Você sempre pode evitar o cálculo de subproblemas desnecessários. 3) 1 e 2 pode ser muito mais difícil de codificar sem uma estrutura de dados explícita para armazenar subproblemas em OU, mais difícil se o fluxo de controle precisar se entrelaçar entre as chamadas de função (pode ser necessário estado ou continuações).
Ninjagecko

76

DP de cima para baixo e de baixo para cima são duas maneiras diferentes de resolver os mesmos problemas. Considere uma solução de programação memorizada (de cima para baixo) vs dinâmica (de baixo para cima) para calcular 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)

Pessoalmente, acho a memorização muito mais natural. Você pode pegar uma função recursiva e memorizá-la por um processo mecânico (primeira consulta de resposta no cache e, se possível, devolvê-la, caso contrário, calcule-a recursivamente e, em seguida, antes de retornar, salve o cálculo no cache para uso futuro), enquanto faz de baixo para cima a programação dinâmica exige que você codifique uma ordem na qual as soluções são calculadas, de modo que nenhum "grande problema" seja computado antes do problema menor do qual depende.


1
Ah, agora entendo o que significa "de cima para baixo" e "de baixo para cima"; na verdade, é apenas uma referência a memoization vs DP. E pensar que eu era a pessoa que editou a questão de mencionar DP no título ...
ninjagecko

qual é o tempo de execução da fib memoizada v / s fib recursiva normal?
Siddhartha

exponencial (2 ^ n) para coz normal é uma árvore de recursão, eu acho.
Siddhartha

1
Sim, é linear! Eu desenhei a árvore de recursão e vi quais chamadas poderiam ser evitadas e percebi que as chamadas memo_fib (n - 2) seriam todas evitadas após a primeira chamada e, portanto, todos os ramos corretos da árvore de recursão seriam cortados e reduzirá para linear.
Siddhartha

1
Como o DP envolve essencialmente a construção de uma tabela de resultados em que cada resultado é calculado no máximo uma vez, uma maneira simples de visualizar o tempo de execução de um algoritmo de DP é ver o tamanho da tabela. Nesse caso, é do tamanho n (um resultado por valor de entrada) e O (n). Em outros casos, poderia ser uma matriz n ^ 2, resultando em O (n ^ 2) etc.
Johnson Wong

22

Um recurso importante da programação dinâmica é a presença de subproblemas sobrepostos . Ou seja, o problema que você está tentando resolver pode ser dividido em subproblemas, e muitos desses subproblemas compartilham subproblemas. É como "Divida e conquiste", mas você acaba fazendo a mesma coisa muitas e muitas vezes. Um exemplo que eu uso desde 2003 ao ensinar ou explicar esses assuntos: você pode calcular os números de Fibonacci recursivamente.

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

Use seu idioma favorito e tente executá-lo fib(50). Vai demorar muito, muito tempo. Aproximadamente tanto tempo quanto fib(50)ele! No entanto, muito trabalho desnecessário está sendo feito. fib(50)chama fib(49)e fib(48), mas, em seguida, ambos acabam chamando fib(47), mesmo que o valor seja o mesmo. De fato, fib(47)será computado três vezes: por uma ligação direta de fib(49), por uma ligação direta de fib(48), e também por uma ligação direta de outra fib(48), aquela gerada pela computação de fib(49)... Então, veja bem, temos subproblemas sobrepostos .

Boas notícias: não há necessidade de calcular o mesmo valor muitas vezes. Depois de calculá-lo uma vez, armazene em cache o resultado e, da próxima vez, use o valor em cache! Essa é a essência da programação dinâmica. Você pode chamá-lo de "de cima para baixo", "memorização" ou qualquer outra coisa que desejar. Essa abordagem é muito intuitiva e muito fácil de implementar. Basta escrever uma solução recursiva primeiro, testá-la em pequenos testes, adicionar memorização (armazenamento em cache de valores já calculados) e --- bingo! --- você terminou.

Normalmente, você também pode escrever um programa iterativo equivalente que funcione de baixo para cima, sem recursão. Nesse caso, essa seria a abordagem mais natural: faça um loop de 1 a 50 calculando todos os números de Fibonacci à medida que avança.

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

Em qualquer cenário interessante, a solução de baixo para cima geralmente é mais difícil de entender. No entanto, uma vez que você o entenda, geralmente você terá uma visão muito mais clara de como o algoritmo funciona. Na prática, ao resolver problemas não triviais, recomendo primeiro escrever a abordagem de cima para baixo e testá-la em pequenos exemplos. Em seguida, escreva a solução de baixo para cima e compare as duas para garantir que você está recebendo a mesma coisa. Idealmente, compare as duas soluções automaticamente. Escreva uma rotina pequena que gere muitos testes, idealmente - todospequenos testes até certo tamanho - e validam que ambas as soluções oferecem o mesmo resultado. Depois disso, use a solução de baixo para cima na produção, mas mantenha o código de cima para baixo, comentado. Isso tornará mais fácil para outros desenvolvedores entender o que você está fazendo: o código de baixo para cima pode ser bastante incompreensível, até você o escreveu e mesmo se você sabe exatamente o que está fazendo.

Em muitas aplicações, a abordagem de baixo para cima é um pouco mais rápida devido à sobrecarga de chamadas recursivas. O estouro de pilha também pode ser um problema em alguns problemas e observe que isso pode depender muito dos dados de entrada. Em alguns casos, talvez você não consiga escrever um teste causando um estouro de pilha se não entender bem a programação dinâmica, mas algum dia isso ainda pode acontecer.

Agora, existem problemas em que a abordagem de cima para baixo é a única solução viável, porque o espaço do problema é tão grande que não é possível resolver todos os subproblemas. No entanto, o "armazenamento em cache" ainda funciona em tempo razoável, porque sua entrada precisa apenas de uma fração dos subproblemas para ser resolvida - mas é muito complicado definir explicitamente quais subproblemas você precisa resolver e, portanto, escrever um solução. Por outro lado, há situações em que você sabe que precisará resolver todos os subproblemas. Nesse caso, continue e use de baixo para cima.

Eu pessoalmente usaria de cima para baixo na otimização de parágrafos, também conhecido como problema de otimização de quebra de linha do Word (consulte os algoritmos de quebra de linha Knuth-Plass; pelo menos o TeX o usa e alguns softwares da Adobe Systems usam uma abordagem semelhante). Eu usaria de baixo para cima para a transformação rápida de Fourier .


Olá!!! Quero determinar se as seguintes proposições estão corretas. - Para um algoritmo de programação dinâmica, o cálculo de todos os valores com de baixo para cima é assintoticamente mais rápido que o uso de recursão e memorização. - O tempo de um algoritmo dinâmico é sempre Ο (Ρ) onde Ρ é o número de subproblemas. - Cada problema no NP pode ser resolvido em tempo exponencial.
23415 Mary Star

O que eu poderia dizer sobre as proposições acima? Você tem alguma ideia? @osa
Mary Star

@evinda, (1) está sempre errado. É o mesmo ou assintoticamente mais lento (quando você não precisa de todos os subproblemas, a recursão pode ser mais rápida). (2) só está correto se você puder resolver todos os subproblemas em O (1). (3) é meio certo. Cada problema no NP pode ser resolvido em tempo polinomial em uma máquina não determinística (como um computador quântico, que pode fazer várias coisas simultaneamente: comer seu bolo, comê-lo simultaneamente e rastrear os dois resultados). Portanto, de certa forma, cada problema no NP pode ser resolvido em tempo exponencial em um computador comum. Nota do lado: tudo em P também está em NP. Por exemplo, adicionando dois números inteiros
osa

19

Vamos tomar a série fibonacci como exemplo

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

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

Outra maneira de dizer isso,

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

No caso dos cinco primeiros números de fibonacci

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

Agora vamos dar uma olhada no algoritmo recursivo da série Fibonacci como um exemplo

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

Agora, se executarmos este programa com os seguintes comandos

rcursive(5);

se olharmos atentamente para o algoritmo, para gerar o quinto número, será necessário o terceiro e o quarto números. Então, minha recursão começa no topo (5) e depois vai até os números inferiores / inferiores. Essa abordagem é realmente de cima para baixo.

Para evitar fazer o mesmo cálculo várias vezes, usamos técnicas de programação dinâmica. Armazenamos o valor previamente calculado e o reutilizamos. Essa técnica é chamada de memorização. Há mais na programação dinâmica que não a memorização, que não é necessária para discutir o problema atual.

Careca

Vamos reescrever nosso algoritmo original e adicionar técnicas memorizadas.

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];
}

E executamos esse método como segue

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

Essa solução ainda está de cima para baixo, pois o algoritmo começa do valor máximo e desce para cada etapa para obter nosso valor máximo.

Debaixo para cima

Mas, a questão é: podemos começar do fundo, como do primeiro número de fibonacci, e depois subir nosso caminho. Vamos reescrevê-lo usando essas 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];
}

Agora, se olharmos para esse algoritmo, ele realmente começa com valores mais baixos e depois vai para o topo. Se eu precisar do quinto número de fibonacci, na verdade, estou calculando o primeiro, depois o segundo e o terceiro até o quinto número. Na verdade, essas técnicas são chamadas de técnicas ascendentes.

Os dois últimos algoritmos atendem aos requisitos de programação dinâmica. Mas um é de cima para baixo e outro de baixo para cima. O algoritmo possui complexidade semelhante de espaço e tempo.


Podemos dizer que a abordagem de baixo para cima é frequentemente implementada de forma não recursiva?
Lewis Chan

Não, você pode converter qualquer lógica loop para recursão
Ashvin Sharma

3

A programação dinâmica é freqüentemente chamada de memorização!

1.Memoization é a técnica de cima para baixo (comece a resolver o problema especificado dividindo-o) e a programação dinâmica é uma técnica de baixo para cima (comece a resolver a partir do subproblema trivial, até o problema em questão)

2.DP encontra a solução começando pelo (s) caso (s) de base e segue seu caminho para cima. O DP resolve todos os subproblemas, porque o faz de baixo para cima

Ao contrário do Memoization, que resolve apenas os subproblemas necessários

  1. O DP tem o potencial de transformar soluções de força bruta de tempo exponencial em algoritmos de tempo polinomial.

  2. O DP pode ser muito mais eficiente porque sua interação

Pelo contrário, a memorização deve pagar a sobrecarga (geralmente significativa) devido à recursão.

Para ser mais simples, o Memoization usa a abordagem de cima para baixo para resolver o problema, ou seja, começa com o problema principal (principal), depois o divide em subproblemas e os soluciona da mesma forma. Nesta abordagem, o mesmo subproblema pode ocorrer várias vezes e consumir mais ciclo da CPU, aumentando assim a complexidade do tempo. Enquanto na programação dinâmica, o mesmo subproblema não será resolvido várias vezes, mas o resultado anterior será usado para otimizar a solução.


4
isso não é verdade, a memorização usa um cache que ajuda a economizar a complexidade de tempo da mesma forma que o DP
InformedA:

3

Simplesmente dizer que a abordagem de cima para baixo usa a recursão para chamar problemas Sub repetidamente,
enquanto a abordagem de baixo para cima usa o single sem chamar ninguém e, portanto, é mais eficiente.


1

A seguir, é apresentada a solução baseada em DP para o problema de Editar distância, que é de cima para baixo. Espero que também ajude a entender o mundo da Programação 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];
}

Você pode pensar em sua implementação recursiva em sua casa. É muito bom e desafiador se você nunca resolveu algo assim antes.


1

Top-Down : Mantendo o controle do valor calculado até agora e retornando o resultado quando a condição básica for atendida.

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);
}

Bottom-Up : O resultado atual depende do resultado do seu subproblema.

int n = 5;
fibBottomUp(n);

private int fibBottomUp(int n) {
    if (n <= 1) return 1;
    return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.