0Pricing
Coding Interview Prep · Aula

DP de Cima para Baixo com Memoização

Adicione um dicionário de memoização a uma solução recursiva para eliminar chamadas duplicadas e use @lru_cache com o mínimo de código.

DP de Cima para Baixo com Memoização é uma aula grátis de Coding Interview Prep no CoddyKit. Esta é a aula 2 de 4. Você pode ler a aula completa abaixo gratuitamente — depois pratica ao vivo no navegador com um editor de código integrado e um tutor de IA 24/7. Faz parte do caminho de aprendizado de Coding Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de Coding Interview Prep inclui 4 aulas no total.

DP de cima para baixo: a ideia da memoização

DP de cima para baixo começa com a solução recursiva original e adiciona memoização: um cache que armazena o resultado de cada subproblema na primeira vez em que ele é calculado. Em chamadas posteriores com os mesmos argumentos, o resultado armazenado em cache é retornado imediatamente, sem recursão. Isso transforma uma recursão ingênua de O(2^n) em O(n) com mudanças mínimas no código — muitas vezes basta adicionar 2–3 linhas a uma solução recursiva existente.

# Top-down approach:
# 1. Write the recursive solution (natural but slow)
# 2. Add a memo dict to cache results
# 3. Before recursing, check if the result is cached
# 4. Before returning, store the result in the cache

# This is also called 'memoization' (US spelling)
# 'memoize' means 'to remember', not 'memorize'

# The cache key is the function arguments
# For fib: key is n
# For 2D DP: key is (i, j)
# For 3D DP: key is (i, j, k)
print('Top-down = recursion + memo cache')

Fibonacci com memoização

Adicionar um dicionário de memoização à recursão ingênua de Fibonacci reduz o tempo de O(2^n) para O(n). A primeira chamada a fib(k) calcula e armazena o resultado. Todas as chamadas posteriores para o mesmo k retornam o valor armazenado em cache instantaneamente. A complexidade de espaço é O(n) para o dicionário de memoização, mais O(n) para a pilha de chamadas. Compare a quantidade de chamadas: sem memoização, fib(30) faz cerca de 2 milhões de chamadas; com memoização, exatamente 30 chamadas.

def fib_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]  # return cached result
    if n <= 1:
        return n
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

# Verify speed improvement:
print(fib_memo(30))   # fast!
print(fib_memo(50))   # still fast
print(fib_memo(100))  # no problem

# Without memo, fib_naive(50) would take minutes
# With memo: each of the 50 sub-problems computed once

Usando @functools.lru_cache

O decorador do Python @functools.lru_cache(maxsize=None) (ou o apelido @cache no Python 3.9 ou posterior) aplica memoização automaticamente a uma função com base em seus argumentos. Essa é a forma mais limpa de adicionar DP de cima para baixo em entrevistas — escreva a solução recursiva, acrescente o decorador e pronto. O decorador armazena todos os resultados em um dicionário indexado pelos argumentos da função, que precisam ser hasháveis (não use listas — use tuplas).

import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print(fib(50))   # 12586269025
print(fib(100))  # works instantly

# Clear cache between tests if needed:
fib.cache_clear()

# Python 3.9+ shorthand:
# from functools import cache
# @cache
# def fib(n): ...

print(fib.cache_info())  # shows hits, misses, maxsize, currsize

Troca de moedas de cima para baixo

Troca de moedas (LeetCode #322): dadas as denominações das moedas e um valor-alvo, encontre o número mínimo de moedas necessário. A formulação recursiva é: para cada moeda, use-a e resolva o valor restante; depois escolha o mínimo. Faça a memoização com base no valor para evitar recomputações. Caso-base: amount=0 requer 0 moedas; um valor impossível retorna infinito (ou -1 depois da recursão).

import functools

def coin_change_top_down(coins, amount):
    @functools.lru_cache(maxsize=None)
    def dp(remaining):
        if remaining == 0:
            return 0  # no coins needed
        if remaining < 0:
            return float('inf')  # impossible
        # Try each coin and take the minimum
        return 1 + min(dp(remaining - c) for c in coins)

    result = dp(amount)
    return result if result != float('inf') else -1

print(coin_change_top_down([1, 5, 6, 9], 11))  # 2: (5+6) or (2*5+1?no: 9+2?no) 5+6=11 YES
print(coin_change_top_down([2], 3))             # -1: impossible
print(coin_change_top_down([1, 2, 5], 11))      # 3: 5+5+1

Subida de escadas de cima para baixo com K degraus

Generalize a subida de escadas para permitir de 1 a k degraus. O estado é o degrau atual e, a partir do degrau i, você pode alcançar os degraus i+1, i+2, ..., i+k. A recorrência é: dp(i) = sum of dp(i-j) for j in 1..k if i-j >= 0. A memoização torna isso O(n*k), em vez de O(k^n). Essa generalização aparece em problemas como “custo mínimo para chegar ao último degrau” e “contagem de maneiras de preencher uma grade”.

import functools

def climb_k_steps(n, k):
    @functools.lru_cache(maxsize=None)
    def dp(i):
        if i == 0:
            return 1  # base: one way to stay at ground
        if i < 0:
            return 0  # impossible
        # From stair i, you could have come from i-1, i-2, ..., i-k
        return sum(dp(i - j) for j in range(1, k+1) if i - j >= 0)

    return dp(n)

# k=2 (original): should match fib-like sequence
print([climb_k_steps(n, 2) for n in range(7)])  # [1,1,2,3,5,8,13]
# k=3: more options
print([climb_k_steps(n, 3) for n in range(7)])  # [1,1,2,4,7,13,24]

LCS de cima para baixo: memoização 2D

A maior subsequência comum (LCS) exige um estado 2D: dp(i, j) = comprimento da LCS de s1[:i] e s2[:j]. Se s1[i-1] == s2[j-1], os caracteres coincidem: dp(i,j) = 1 + dp(i-1, j-1). Caso contrário: dp(i,j) = max(dp(i-1,j), dp(i,j-1)) — ignore um caractere de uma das duas cadeias. Fazer memoização por (i, j) resulta em O(mn), em vez de O(2^(m+n)).

import functools

def lcs_top_down(s1, s2):
    m, n = len(s1), len(s2)

    @functools.lru_cache(maxsize=None)
    def dp(i, j):
        if i == 0 or j == 0:
            return 0  # empty prefix has LCS of 0
        if s1[i-1] == s2[j-1]:
            return 1 + dp(i-1, j-1)  # characters match
        return max(dp(i-1, j), dp(i, j-1))  # skip one

    return dp(m, n)

print(lcs_top_down('abcde', 'ace'))   # 3: 'ace'
print(lcs_top_down('abc', 'abc'))     # 3: 'abc'
print(lcs_top_down('abc', 'def'))     # 0: no common chars

Dicionário de memoização versus lru_cache: quando escolher

Use @lru_cache quando os argumentos da função forem tipos primitivos hasháveis (int, str, tuple). Use um dicionário manual de memoização quando: você precisar passar estado mutável (listas, dicionários), convertendo-o em tuplas; precisar rastrear quais chaves foram calculadas; ou estiver em um método de classe no qual self não deve ser armazenado em cache. O dicionário manual de memoização é mais explícito e evita problemas sutis de fechamento em funções auxiliares recursivas.

# @lru_cache: clean, automatic, O(1) overhead
# Use when: arguments are simple (int, str, tuple)
import functools
@functools.lru_cache(maxsize=None)
def simple_dp(n):
    if n <= 1: return n
    return simple_dp(n-1) + simple_dp(n-2)

# Manual memo dict: explicit, flexible
# Use when: complex state, need to inspect memo, class methods
def manual_memo_dp(s1, s2):
    memo = {}
    def dp(i, j):
        if (i,j) in memo: return memo[(i,j)]
        if i == 0 or j == 0:
            return 0
        if s1[i-1] == s2[j-1]:
            memo[(i,j)] = 1 + dp(i-1, j-1)
        else:
            memo[(i,j)] = max(dp(i-1,j), dp(i,j-1))
        return memo[(i,j)]
    return dp(len(s1), len(s2))

print(manual_memo_dp('abcde', 'ace'))  # 3

Soma-alvo de cima para baixo

Soma-alvo (LeetCode #494): atribua + ou - a cada número e conte as atribuições que produzem uma soma-alvo. Estado: dp(index, current_sum). Em cada índice, tente somar (+) e subtrair (-) o número atual. A memoização por (index, current_sum) transforma a força bruta O(2^n) em O(n * sum_range). O intervalo da soma é limitado pela soma total de todos os números, resultando em O(n * S) estados no total.

import functools

def find_target_sum_ways(nums, target):
    @functools.lru_cache(maxsize=None)
    def dp(index, current_sum):
        if index == len(nums):
            return 1 if current_sum == target else 0
        # Try adding the number
        add = dp(index + 1, current_sum + nums[index])
        # Try subtracting the number
        subtract = dp(index + 1, current_sum - nums[index])
        return add + subtract

    return dp(0, 0)

print(find_target_sum_ways([1,1,1,1,1], 3))  # 5
print(find_target_sum_ways([1], 1))            # 1
print(find_target_sum_ways([1], -1))           # 1

De cima para baixo versus de baixo para cima: vantagens e desvantagens

De cima para baixo (memoização) — vantagens: é natural de escrever, pois começa com a solução recursiva; calcula apenas os subproblemas realmente necessários (sob demanda); e permite adicionar o cache de forma incremental. De baixo para cima (tabulação) — vantagens: não há sobrecarga da pilha de chamadas (nem limite de recursão do Python), o acesso à memória tem melhor localidade de cache e é mais fácil otimizar o espaço. As duas abordagens têm a mesma complexidade assintótica. Em entrevistas, comece de cima para baixo para verificar a correção e depois converta para baixo para cima se pedirem melhor uso do espaço.

# Top-down advantages:
# + Natural: write recursive, add @cache
# + Lazy: only computes needed sub-problems
# + Easy to reason about correctness
# - Uses call stack (recursion limit in Python)
# - Higher constant factor (function call overhead)

# Bottom-up advantages:
# + No recursion limit
# + Better cache performance (sequential memory)
# + Easier to space-optimise (rolling array)
# - Must compute all sub-problems in order
# - Less intuitive for complex 2D/3D problems

# Interview strategy:
# Start with top-down to verify recurrence,
# convert to bottom-up only if asked.
print('Top-down: easy to write | Bottom-up: efficient for large n')

Quebra de palavras com DP de cima para baixo

Quebra de palavras (LeetCode #139) pergunta se uma cadeia de caracteres s pode ser segmentada em palavras de um dicionário. Estado: dp(i) = indica se s[i:] pode ser segmentada. A partir do índice i, tente todas as palavras: se s[i:i+len(w)] == w, faça a recursão sobre o sufixo restante. A memoização no índice inicial converte a força bruta O(2^n) em O(n^2) (ou O(n * max_word_len)) com a verificação de pertencimento ao conjunto.

import functools

def word_break(s, word_dict):
    word_set = set(word_dict)

    @functools.lru_cache(maxsize=None)
    def dp(start):
        if start == len(s):
            return True  # successfully segmented entire string
        for end in range(start + 1, len(s) + 1):
            if s[start:end] in word_set and dp(end):
                return True
        return False

    return dp(0)

print(word_break('leetcode', ['leet', 'code']))       # True
print(word_break('applepenapple', ['apple', 'pen'])) # True
print(word_break('catsandog', ['cats', 'dog', 'and', 'cat', 'san', 'andog'])) # False

Limite de recursão e ferramentas de iteração

O limite padrão de recursão do Python é 1000 (definido por sys.getrecursionlimit()). Em problemas de DP com entradas grandes (n = 10.000+), a memoização de cima para baixo atingirá esse limite. Opções: aumente o limite com sys.setrecursionlimit(100000) ou converta para DP de baixo para cima. Na programação competitiva, aumentar o limite é comum; em código de produção, prefira sempre soluções de baixo para cima ou iterativas para garantir confiabilidade.

import sys

print('Default recursion limit:', sys.getrecursionlimit())  # 1000

# For large DP problems, increase if needed:
# sys.setrecursionlimit(100000)

# Better: convert to bottom-up DP for large n
def fib_bottom_up(n):
    if n <= 1: return n
    a, b = 0, 1
    for _ in range(2, n+1):
        a, b = b, a + b
    return b

# No recursion limit issue:
print(fib_bottom_up(10000))  # works fine, no recursion

Verificação rápida

Teste sua compreensão dos conceitos de Estruturas de Dados & Algoritmos — Preparação para Entrevistas de Programação desta lição.

Recapitulação da lição

Nesta lição, você aprendeu: DP de cima para baixo com um dicionário de memoização e o decorador @lru_cache, soluções com memoização para Fibonacci, troca de moedas, LCS, soma-alvo e quebra de palavras e quando escolher a abordagem de cima para baixo em vez da de baixo para cima. A seguir, implementaremos DP de baixo para cima com tabulação e otimização de espaço.

Perguntas Frequentes

A aula “DP de Cima para Baixo com Memoização” é grátis?

Sim — o texto completo de “DP de Cima para Baixo com Memoização” é grátis para ler aqui na web. Para praticá-la interativamente (um editor de código integrado e um tutor de IA 24/7) e desbloquear o restante do curso de Coding Interview Prep, atualize para CoddyKit PRO. O curso de Coding Interview Prep inclui 4 aulas no total.

O que vou aprender em “DP de Cima para Baixo com Memoização”?

Adicione um dicionário de memoização a uma solução recursiva para eliminar chamadas duplicadas e use @lru_cache com o mínimo de código. Você pratica Coding Interview Prep com código prático que executa diretamente no navegador, e um tutor de IA 24/7 responde suas dúvidas enquanto trabalha na aula.

Preciso ter experiência prévia para começar Coding Interview Prep?

Nenhuma experiência prévia é necessária. Coding Interview Prep no CoddyKit é estruturado para alunos iniciantes até avançados, então você pode começar aqui ou desde o início e aprender no seu ritmo. Esta é a aula 2 de 4.

Quanto tempo leva a aula “DP de Cima para Baixo com Memoização”?

A maioria das aulas CoddyKit leva cerca de 5–10 minutos. Cada uma é compacta e interativa, então você faz progresso constante e retoma exatamente de onde parou entre web e app.

Posso escrever e executar código nesta aula de Coding Interview Prep?

Sim. Cada aula de Coding Interview Prep inclui um editor de código integrado, então você escreve e executa código real direto no navegador e recebe feedback de IA instantaneamente — nenhuma configuração local necessária.

Todas as aulas deste curso

  1. Reconhecendo DP: Subproblemas Sobrepostos
  2. DP de Cima para Baixo com Memoização
  3. DP de Baixo para Cima com Tabulação
  4. Troca de Moedas e Escada de Custo Mínimo
← Voltar para Coding Interview Prep