A programação dinâmica é uma técnica par ao desenvolvimento de algoritmos assintoticamente eficientes. A ideia central da programação dinâmica é, assim como na técnica de Dividir para conquistar, decompor o problema original em subproblemas e combinar suas soluções. A principal diferença entre essa técnica e dividir para conquistar reside no fato de que na programação dinâmica os subproblemas não são disjuntos, ou seja: diferentes subproblemas podem conter os mesmos subproblemas repetidas vezes. Tendo isso em vista, a estratégia de programação dinâmica consiste em resolver cada subproblema apenas uma única vez e armazenar esse resultado para cálculos futuros de outros subproblemas.
Duas abordagens em particular podem ser utilizadas na programação dinâmica:
- Top-down: partir do problema inicial e calcular os subproblemas recursivamente utilizando memorização, ou seja, alguma estrutura de dados para armazenar os resultados dos subproblemas afim de utilizá-los no cálculo de outros subproblemas.
- Bottom-up: reverter o problema e resolvê-lo a partir dos menores subproblemas, compondo seus resultados (usando ou não memorização) até obter a solução do problema original.
Sequência de Fibonacci
Um conhecido exemplo de algoritmo que pode ser significativamente otimizado através do uso de programação dinâmica é o da geração de números da sequência de Fibonacci.
Dada a definição recursiva da sequência de Fibonacci:
Podemos implementar uma função que calcula os primeiros números da sequência de Fibonacci em OCaml da seguinte forma:
Analisando a implementação, é possível perceber que há muitas computações repetidas, pois está contido em , portando toda a árvore de recursão de será computada duas vezes. Isso ocorre para quase todos os cálculos, e é a principal razão para a ineficiência dessa implementação, que tem complexidade .
Podemos utilizar a técnica de programação dinâmica para projetar algoritmos mais eficientes para a sequência de Fibonacci através da memorização de resultados de subproblemas.
Uma abordagem top-down para o algoritmo que gera números da sequência de Fibonacci pode ser implementada através de um array para armazenar os resultados dos subproblemas da seguinte forma:
Da mesma forma, uma abordagem bottom-up para o algoritmo pode ser implementada utilizando um loop iterativo ou mesmo utilizando tail call recursion, como é exemplificado a seguir:
Em ambas as soluções cada subproblema é calculado apenas uma vez. No caso da abordagem top-down os valores são armazenados em um array e as chamadas recursivas podem aproveitar os resultados já calculados. Já no caso da abordagem bottom-up o resultado final é construído através da combinação dos subproblemas, que são resolvidos diretamente e compostos para obter os resultados de subproblemas maiores.