Capítulo 9 — Programação dinâmica 🎒
Ideia central
Programação dinâmica (DP) resolve um problema difícil quebrando-o em subproblemas menores e combinando suas soluções, sem recalcular o que já foi resolvido. Funciona quando os subproblemas se sobrepõem. O exemplo clássico é o problema da mochila (knapsack).
Analogia
Você monta uma tabela linha a linha. Cada célula usa o valor de células já preenchidas (subproblemas menores já resolvidos). No fim, a última célula tem a resposta ótima — e você nunca refez uma conta.
O problema da mochila
Você tem uma mochila que aguenta um peso máximo e vários itens, cada um com peso e valor. Quais itens levar para maximizar o valor sem estourar o peso? A força bruta testaria todos os subconjuntos (O(2ⁿ)); a DP resolve com uma tabela.
A receita da DP
- Crie uma tabela: linhas = itens, colunas = capacidades de 1 até o máximo.
- Para cada célula, a melhor escolha é o máximo entre:
- não levar o item atual (valor da linha de cima, mesma coluna);
- levar o item: valor do item + melhor valor do peso restante (célula da
linha de cima na coluna
capacidade - peso_do_item).
Implementação em Python
def mochila(pesos, valores, capacidade):
n = len(pesos)
# tabela (n+1) x (capacidade+1) preenchida com zeros
tabela = [[0] * (capacidade + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for c in range(1, capacidade + 1):
if pesos[i - 1] <= c: # o item cabe?
levar = valores[i - 1] + tabela[i - 1][c - pesos[i - 1]]
nao_levar = tabela[i - 1][c]
tabela[i][c] = max(levar, nao_levar)
else:
tabela[i][c] = tabela[i - 1][c] # não cabe: ignora o item
return tabela[n][capacidade]
pesos = [1, 3, 4] # ex.: violão(1), aparelho(3), notebook(4)
valores = [1500, 2000, 3000]
print(mochila(pesos, valores, 4)) # 3500
Outro exemplo: maior subsequência comum (LCS)
DP também resolve a maior subsequência comum entre duas strings (útil para "diff" e busca aproximada):
def lcs(a, b):
tab = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
for i in range(1, len(a) + 1):
for j in range(1, len(b) + 1):
if a[i - 1] == b[j - 1]:
tab[i][j] = tab[i - 1][j - 1] + 1 # letras iguais
else:
tab[i][j] = max(tab[i - 1][j], tab[i][j - 1])
return tab[-1][-1]
print(lcs("fish", "fosh")) # 3 (f, s, h)
Complexidade (Big-O)
- Mochila: O(n × capacidade) — uma célula por item × capacidade.
- LCS: O(m × n) — tamanhos das duas strings.
Muito melhor que a força bruta O(2ⁿ).
Dúvidas comuns
Quando usar programação dinâmica?
Quando o problema tem subproblemas sobrepostos e você precisa da solução ótima. Sinais: "maximizar/minimizar dada uma restrição", grade/tabela natural, escolhas discretas (levar ou não levar).
Qual a diferença entre DP e memoização?
Memoização = guardar resultados de chamadas (top-down, recursivo). DP por tabela = preencher de baixo para cima (bottom-up). As duas evitam recalcular.
DP × guloso?
Guloso faz escolhas locais e aceita aproximação. DP explora as combinações via tabela e dá a solução ótima exata. Veja o cap. 8.
Exercícios
9.1 — Por que a mochila não é resolvida de forma gulosa (pegar o mais valioso)?
Porque o item mais valioso pode ocupar peso demais e impedir uma combinação melhor. A DP avalia as combinações via tabela.
9.2 — Big-O da mochila por DP?
O(n × capacidade).
9.3 — O que cada célula da tabela representa?
O maior valor possível usando os primeiros i itens com capacidade c.
Checklist de domínio
- Entendo a ideia de subproblemas sobrepostos.
- Consigo montar a tabela da mochila e explicar cada célula.
- Sei a diferença entre DP, memoização e guloso.
- Sei o Big-O da mochila e da LCS.