Pular para o conteúdo

Algoritmos e estruturas de dados · Técnicas · 7 min de leitura

Recursão e programação dinâmica

Resolver um problema a partir de versões menores dele — e, quando os subproblemas se repetem, guardar as respostas. Do Fibonacci exponencial ao linear, e o troco com menos moedas.

fib(5) → fib(4)1fib(4) → fib(3)2fib(4) → fib(2)3fib(5) → fib(3)4fib(3) → memofib(5)fib(5)fib(4)fib(4)fib(3) — vem do memofib(3)vem do memofib(3) — calcula: 2fib(3)calcula: 2fib(2) — vem do memofib(2)vem do memomemo — 2→1, 3→2, 4→3memo2→1, 3→2, 4→3

Arraste para o lado para ver o diagrama inteiro.

Sem memo, fib(5) chamaria fib(3) duas vezes e fib(2) três vezes — e a árvore dobra a cada nível. Com memo (passos 3 e 4), cada valor é calculado uma única vez.

Recursão

Uma função recursiva tem um caso base (a resposta direta, como fib(0) = 0 e fib(1) = 1) e um passo que chama a si mesma com uma entrada menor. Cada chamada ocupa um quadro na pilha: recursão muito profunda estoura a pilha — às vezes um laço é melhor.

Programação dinâmica

Quando os mesmos subproblemas se repetem e a resposta ótima se monta a partir das respostas ótimas dos menores, guarde-as. Duas formas:

  • De cima para baixo (memoização): a recursão de sempre, com um mapa de respostas já calculadas.
  • De baixo para cima (tabulação): um laço que preenche a tabela do menor para o maior, sem recursão.

Como reconhecer

Perguntas como "de quantas formas…", "qual o mínimo/máximo…" ou "é possível…" sobre sequências e escolhas costumam ser DP: troco com menos moedas, mochila, maior subsequência comum (o diff do Git), distância de edição (corretor ortográfico). Defina o estado, a transição e o caso base — o resto é preencher a tabela.

No código: antes e depois

✗ AntesSwift
// O(2ⁿ): recalcula os mesmos valores milhões de vezes
func fib(_ n: Int) -> Int {
    n < 2 ? n : fib(n - 1) + fib(n - 2)
}
✓ DepoisSwift
// O(n) de tempo, O(1) de espaço: tabulação guardando só os dois últimos
func fib(_ n: Int) -> Int {
    var (a, b) = (0, 1)
    for _ in 0..<n { (a, b) = (b, a + b) }
    return a
}

// Troco com menos moedas: menor[v] = 1 + min(menor[v - moeda])
func menosMoedas(_ valor: Int, _ moedas: [Int]) -> Int? {
    var menor = [0] + Array(repeating: Int.max, count: valor)
    for v in stride(from: 1, through: valor, by: 1) {
        for m in moedas where m <= v && menor[v - m] != Int.max {
            menor[v] = min(menor[v], menor[v - m] + 1)
        }
    }
    return menor[valor] == Int.max ? nil : menor[valor]
}

fib(40) ingênuo faz mais de 300 milhões de chamadas; a versão em laço faz 40 voltas. O troco preenche uma tabela de valor + 1 posições: O(valor × moedas).

Em resumo

Exercícios

Responda e confira na hora. Complexidades valem com ou sem espaços, como O(n log n).

  1. Com fib(0) = 0 e fib(1) = 1, quanto vale fib(10)?

    Ver a solução

    Resposta: 55. 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55 — o 11º termo, de índice 10.

  2. Qual a complexidade do Fibonacci recursivo ingênuo (sem memo)?

    Ver a solução

    Resposta: O(2ⁿ). Cada chamada gera duas outras: a árvore de chamadas dobra a cada nível (o valor exato é cerca de 1,618ⁿ, ainda exponencial).

  3. Com moedas de 1, 2 e 5, qual o menor número de moedas para formar 11?

    Ver a solução

    Resposta: 3. 5 + 5 + 1 = 11. Com 2 moedas o máximo é 10, então 3 é o mínimo.

Perguntar à IA

Monte um prompt pronto sobre Recursão e programação dinâmica na sua linguagem e no seu caso, e abra num chat gratuito.

ChatGPT Claude Perplexity

O prompt vai para o site que você escolher. Não cole senhas, tokens nem dados reais de clientes. Para Gemini e outros, copie e cole.

Referências

Continue aprendendo

Todos os temas de algoritmos e estruturas de dados →