Programação dinâmica é uma forma de resolver problemas salvando respostas de subproblemas menores e reutilizando essas respostas depois. Ela ajuda quando os mesmos subproblemas aparecem de novo e a resposta completa pode ser construída a partir dessas respostas menores.
Os dois estilos principais são memoização e tabulação. A memoização armazena respostas durante uma solução recursiva. A tabulação preenche respostas em ordem bottom-up, geralmente com um array ou tabela.
Quando a programação dinâmica funciona
A programação dinâmica é útil quando duas condições são verdadeiras.
Primeiro, os subproblemas se sobrepõem. Isso significa que o mesmo resultado intermediário é necessário mais de uma vez. Segundo, respostas maiores podem ser construídas a partir de respostas menores de forma consistente.
Se essas condições não forem verdadeiras, armazenar resultados antigos pode aumentar o custo de memória sem ajudar muito.
Memoização vs. tabulação
Memoização: armazenamento top-down
A memoização começa com a ideia recursiva. Quando o algoritmo resolve um subproblema pela primeira vez, ele armazena o resultado. Chamadas posteriores reutilizam o valor armazenado em vez de recalculá-lo.
Esse estilo costuma ser mais fácil quando a recorrência está clara, mas a ordem de todos os estados necessários não está.
Tabulação: construção bottom-up
A tabulação começa pelos casos base e preenche os estados em uma ordem que torna as dependências disponíveis quando necessário.
Esse estilo costuma ser mais fácil quando você já conhece uma ordem de iteração clara e quer controle explícito sobre a tabela.
Exemplo de programação dinâmica: Fibonacci
A sequência de Fibonacci é definida por
e, para ,
Uma solução recursiva simples repete trabalho. Por exemplo, calcular exige e , e então calcular exige novamente. Essa chamada repetida de é o tipo de desperdício que a programação dinâmica elimina.
Memoização em Fibonacci
Com memoização, a recorrência continua a mesma, mas cada valor é armazenado depois da primeira vez em que é calculado.
Para , a sequência útil é:
- calcular e armazenar
- calcular e armazenar
- calcular e armazenar
- calcular e armazenar
- calcular e armazenar
- calcular e armazenar
O ponto principal é que cada estado é calculado uma vez. Pedidos posteriores leem o valor armazenado.
Tabulação em Fibonacci
Com tabulação, você constrói a partir dos casos base para cima:
Essa versão deixa a ordem das dependências óbvia: cada novo valor usa entradas que já são conhecidas.
Por que a programação dinâmica pode ser mais rápida
Se um problema tem apenas estados distintos que importam, a programação dinâmica tenta resolver cada um desses estados uma única vez. Isso pode transformar uma grande árvore recursiva em um cálculo muito menor.
O ganho exato de velocidade depende do problema. Programação dinâmica não é automaticamente rápida. Ela ajuda quando o trabalho repetido é uma parte real da solução original.
Erros comuns em programação dinâmica
Usar quando os subproblemas não se sobrepõem
Nem todo problema recursivo é um problema de programação dinâmica. Se quase todo subproblema for diferente, armazenar resultados pode não economizar trabalho o suficiente para fazer diferença.
Definir o estado errado
O estado precisa incluir todas as informações necessárias para o próximo passo. Se faltar informação importante, a recorrência pode parecer válida, mas produzir respostas erradas.
Errar os casos base
Os casos base sustentam todo o método. Se os valores iniciais estiverem errados, todo estado posterior construído a partir deles também estará errado.
Preencher a tabela na ordem errada
A tabulação só funciona quando cada estado é preenchido depois dos estados dos quais ele depende. Uma recorrência correta ainda pode falhar se a ordem de preenchimento estiver errada.
Supor que programação dinâmica sempre significa otimização
Muitos exemplos famosos maximizam ou minimizam alguma coisa, mas a programação dinâmica também é usada para problemas de contagem e decisão. O verdadeiro sinal são subproblemas reutilizáveis, não a palavra "melhor".
Onde a programação dinâmica é usada
A programação dinâmica aparece em problemas como distância de edição, subsequência comum mais longa, contagem de caminhos, otimização no estilo mochila e alguns contextos de caminho mínimo.
Quando você estiver decidindo se deve tentar usá-la, faça duas perguntas:
- os subproblemas menores se repetem?
- a resposta maior pode ser construída a partir dessas respostas menores?
Se as duas respostas forem sim, a programação dinâmica é uma forte candidata.
Tente um problema parecido
Tente sua própria versão com o número de maneiras de subir degraus se cada movimento puder ser de degrau ou degraus. A recorrência é simples, os subproblemas repetidos são fáceis de identificar, e esse é um bom próximo exemplo depois de Fibonacci.
Precisa de ajuda com um problema?
Envie sua pergunta e receba uma solução verificada, passo a passo, em segundos.
Abrir GPAI Solver →