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?
dynamic-programming
difference
memoization
Convidado
fonte
fonte
Respostas:
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:
(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.
fib(100)
, basta chamar isso, e chamariafib(100)=fib(99)+fib(98)
, o que chamariafib(99)=fib(98)+fib(97)
... etc ..., o que chamariafib(2)=fib(1)+fib(0)=1+0=1
. Finalmentefib(3)=fib(2)+fib(1)
, ele resolveria , mas não precisará recalcularfib(2)
, porque o armazenamos em cache.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.
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).(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
fib
funçã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:
fonte
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)".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).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.
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.
fonte
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.
Use seu idioma favorito e tente executá-lo
fib(50)
. Vai demorar muito, muito tempo. Aproximadamente tanto tempo quantofib(50)
ele! No entanto, muito trabalho desnecessário está sendo feito.fib(50)
chamafib(49)
efib(48)
, mas, em seguida, ambos acabam chamandofib(47)
, mesmo que o valor seja o mesmo. De fato,fib(47)
será computado três vezes: por uma ligação direta defib(49)
, por uma ligação direta defib(48)
, e também por uma ligação direta de outrafib(48)
, aquela gerada pela computação defib(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.
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 .
fonte
Vamos tomar a série fibonacci como exemplo
Outra maneira de dizer isso,
No caso dos cinco primeiros números de fibonacci
Agora vamos dar uma olhada no algoritmo recursivo da série Fibonacci como um exemplo
Agora, se executarmos este programa com os seguintes comandos
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.
E executamos esse método como segue
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,
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.
fonte
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
O DP tem o potencial de transformar soluções de força bruta de tempo exponencial em algoritmos de tempo polinomial.
O DP pode ser muito mais eficiente porque sua interaçã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.
fonte
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.
fonte
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:
Você pode pensar em sua implementação recursiva em sua casa. É muito bom e desafiador se você nunca resolveu algo assim antes.
fonte
Top-Down : Mantendo o controle do valor calculado até agora e retornando o resultado quando a condição básica for atendida.
Bottom-Up : O resultado atual depende do resultado do seu subproblema.
fonte