Programação dinâmica e semelhanças de dividir e conquistar
Pelo que vejo por enquanto, posso dizer que a programação dinâmica é uma extensão do paradigma de dividir e conquistar .
Eu não os trataria como algo completamente diferente. Como ambos trabalham dividindo recursivamente um problema em dois ou mais subproblemas do mesmo tipo ou relacionados, até que se tornem simples o suficiente para serem resolvidos diretamente. As soluções para os subproblemas são então combinadas para fornecer uma solução para o problema original.
Então, por que ainda temos nomes de paradigmas diferentes e por que chamei a programação dinâmica de extensão? Isso ocorre porque a abordagem de programação dinâmica pode ser aplicada ao problema apenas se o problema tiver certas restrições ou pré-requisitos . Depois disso, a programação dinâmica amplia a abordagem de dividir e conquistar com a técnica de memorização ou tabulação .
Vamos passo a passo ...
Pré-requisitos / restrições de programação dinâmica
Como acabamos de descobrir, existem dois atributos principais que os problemas de divisão e conquista devem ter para que a programação dinâmica seja aplicável:
Subestrutura ideal - a solução ideal pode ser construída a partir de soluções ideais de seus subproblemas
Subproblemas sobrepostos - o problema pode ser dividido em subproblemas que são reutilizados várias vezes ou um algoritmo recursivo para o problema resolve o mesmo subproblema repetidamente, em vez de sempre gerar novos subproblemas
Uma vez atendidas essas duas condições, podemos dizer que esse problema de divisão e conquista pode ser resolvido usando a abordagem de programação dinâmica.
Extensão de programação dinâmica para dividir e conquistar
A abordagem de programação dinâmica estende a abordagem de dividir e conquistar com duas técnicas ( memorização e tabulação ), ambas com o objetivo de armazenar e reutilizar soluções de subproblemas que podem melhorar drasticamente o desempenho. Por exemplo, a implementação recursiva ingênua da função Fibonacci possui complexidade de tempo em O(2^n)
que a solução DP faz o mesmo com apenas O(n)
tempo.
Memoização (preenchimento de cache de cima para baixo) refere-se à técnica de armazenamento em cache e reutilização de resultados calculados anteriormente. A fib
função memorizada ficaria assim:
memFib(n) {
if (mem[n] is undefined)
if (n < 2) result = n
else result = memFib(n-2) + memFib(n-1)
mem[n] = result
return mem[n]
}
A tabulação (preenchimento de cache de baixo para cima) é semelhante, mas se concentra no preenchimento das entradas do cache. A computação dos valores no cache é mais fácil de forma iterativa. A versão de tabulação fib
seria assim:
tabFib(n) {
mem[0] = 0
mem[1] = 1
for i = 2...n
mem[i] = mem[i-2] + mem[i-1]
return mem[n]
}
Você pode ler mais sobre memorização e comparação de tabulação aqui .
A idéia principal que você deve entender aqui é que, como nosso problema de divisão e conquista tem subproblemas sobrepostos, o armazenamento em cache de soluções de subproblemas se torna possível e, assim, a memorização / tabulação entra em cena.
Então qual é a diferença entre DP e DC?
Como agora estamos familiarizados com os pré-requisitos de DP e suas metodologias, estamos prontos para colocar tudo o que foi mencionado acima em uma imagem.
Se você quiser ver exemplos de código, pode dar uma olhada em explicações mais detalhadas aqui, onde encontrará dois exemplos de algoritmos: Pesquisa binária e distância mínima de edição (distância de Levenshtein) que ilustram a diferença entre DP e DC.