0Pricing
DSA Interview Prep · Aula

Memoização: Armazenando Resultados Recursivos em Cache

Aplique @functools.lru_cache e dicionários de memoização manuais a Fibonacci e climbing-stairs para eliminar recomputações exponenciais.

Memoização: Armazenando Resultados Recursivos em Cache é uma aula grátis de DSA Interview Prep no CoddyKit. Esta é a aula 4 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 DSA Interview Prep, e seu progresso é sincronizado entre a web e o app CoddyKit. O curso de DSA Interview Prep inclui 4 aulas no total.

O Problema da Recursão Redundante

O Fibonacci recursivo ingênuo calcula os mesmos valores repetidamente. fib(5) chama fib(4) e fib(3); fib(4) chama fib(3) e fib(2) — portanto, fib(3) é calculado duas vezes. Essa redundância cresce exponencialmente: fib(40) faz mais de um bilhão de chamadas de função. A memoização resolve isso armazenando cada resultado na primeira vez em que ele é calculado, para que as chamadas subsequentes o recuperem em O(1), em vez de recalculá-lo.

# Count calls without memoisation
call_count = [0]

def fib_plain(n):
    call_count[0] += 1
    if n <= 1: return n
    return fib_plain(n-1) + fib_plain(n-2)

fib_plain(20)
print(f'fib(20) without memo: {call_count[0]:,} calls')
# ~21,891 calls for n=20; ~1 billion for n=40

Memoização Manual com um Dicionário

Adicione um dicionário memo como parâmetro (ou use um fechamento). Antes de calcular, verifique se a resposta já está em memo. Se estiver, retorne-a imediatamente. Caso contrário, calcule-a, armazene-a em memo e retorne-a. Agora, cada subproblema distinto é calculado exatamente uma vez, transformando O(2^n) em tempo O(n) e espaço O(n) para o dicionário memo, além de O(n) de espaço para a pilha.

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

print(fib_memo(10))   # 55
print(fib_memo(50))   # 12586269025
print(fib_memo(100))  # huge number — still fast!

Decorador functools.lru_cache

Python fornece @functools.lru_cache(maxsize=None) (também disponível como @functools.cache no Python 3.9 ou posterior) para automatizar a memoização. Adicionar esse decorador acima de uma função armazena todas as chamadas com base em seus argumentos. maxsize=None significa que o tamanho do armazenamento temporário é ilimitado — cada combinação distinta de argumentos é armazenada. Isso transforma qualquer função recursiva em uma versão com memoização usando uma única linha de código.

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))  # 354224848179261915075
print(fib.cache_info())  # CacheInfo(hits=..., misses=..., maxsize=None, currsize=...)

Subida de Escadas (LeetCode 70)

LeetCode 70, 'Subida de Escadas': você pode subir 1 ou 2 degraus por vez. Quantas maneiras existem de chegar ao degrau n? Isso é Fibonacci disfarçado: ways(n) = ways(n-1) + ways(n-2). Casos-base: ways(0) = 1 (uma maneira de permanecer no chão), ways(1) = 1. Com memoização, o tempo é O(n) e o espaço é O(n).

import functools

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

for i in range(1, 8):
    print(f'climbStairs({i}) = {climbStairs(i)}')
# 1,2,3,5,8,13,21

Troco (LeetCode 322)

LeetCode 322, 'Troco': dadas as denominações e um valor-alvo, encontre o número mínimo de moedas. Recursão de cima para baixo com memoização: dp(amount) = 1 + min(dp(amount - coin)) para cada moeda válida. O caso-base é: dp(0) = 0. Armazene temporariamente cada subvalor. Se um subvalor for impossível, retorne infinito. A memoização transforma a força bruta exponencial em tempo O(valor-alvo × quantidade de moedas).

import functools

def coinChange(coins, amount):
    @functools.lru_cache(maxsize=None)
    def dp(rem):
        if rem == 0:
            return 0
        if rem < 0:
            return float('inf')
        return 1 + min(dp(rem - c) for c in coins)

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

print(coinChange([1, 5, 11], 15))  # 3 (5+5+5)
print(coinChange([1, 2, 5], 11))   # 3 (5+5+1)
print(coinChange([2], 3))          # -1

Divisão de Palavras (LeetCode 139) com Memoização

LeetCode 139, 'Divisão de Palavras': determine se uma cadeia de caracteres pode ser segmentada em palavras do dicionário. A recursão de cima para baixo: can_break(s, start) tenta cada prefixo s[start:end]; se ele estiver no dicionário e can_break(s, end) for verdadeiro, retorne verdadeiro. Sem memoização, isso é O(2^n); com memoização (armazenando temporariamente cada índice inicial), torna-se O(n² × L), em que L é o comprimento máximo de uma palavra.

import functools

def wordBreak(s, wordDict):
    word_set = set(wordDict)

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

    return can_break(0)

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

Memoização versus Tabulação

Memoização (de cima para baixo) começa com o problema original e armazena as respostas à medida que elas são descobertas recursivamente. Ela resolve apenas os subproblemas realmente necessários. A tabulação (de baixo para cima) preenche previamente uma tabela, partindo de subproblemas pequenos até chegar aos grandes, e resolve todos os subproblemas. A memoização é mais fácil de derivar de uma solução recursiva; a tabulação evita limitações de profundidade da recursão e a sobrecarga de chamadas de função.

# Memoisation (top-down)
import functools
@functools.lru_cache(maxsize=None)
def fib_td(n):
    if n <= 1: return n
    return fib_td(n-1) + fib_td(n-2)

# Tabulation (bottom-up)
def fib_bu(n):
    if n <= 1: return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i-1] + dp[i-2]
    return dp[n]

print(fib_td(20), fib_bu(20))   # 6765 6765
# Both O(n) time; fib_bu avoids recursion limit

Otimização de Espaço: Variáveis Deslizantes

Muitos problemas de DP que a recursão com memoização resolve usando espaço O(n) podem ser ainda mais otimizados para espaço O(1) quando apenas um número fixo de respostas de subproblemas anteriores é necessário. Para Fibonacci, somente os dois últimos valores importam. Para a subida de escadas, ocorre o mesmo. Duas variáveis deslizantes substituem todo o dicionário ou a tabela de memoização.

# Fibonacci with O(1) space
def fib_o1(n):
    if n <= 1:
        return n
    prev2, prev1 = 0, 1
    for _ in range(2, n + 1):
        prev2, prev1 = prev1, prev2 + prev1
    return prev1

for i in range(8):
    print(f'fib({i})={fib_o1(i)}', end='  ')
print()

# Climbing stairs O(1) space
def climbStairs_o1(n):
    if n <= 1: return 1
    a, b = 1, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b
print(climbStairs_o1(10))  # 89

lru_cache versus Fechamento versus Dicionário Global

Há três maneiras de implementar a memoização manualmente. Um dicionário global é simples, mas polui o escopo do módulo. Um fechamento encapsula o armazenamento temporário dentro da função, evitando vazamentos, mas exige uma função envoltória. @lru_cache é a opção mais limpa — um único decorador substitui todo o código repetitivo. Em uma entrevista, comece com @lru_cache, a menos que o entrevistador peça especificamente uma implementação manual.

import functools

# 1. Global dict (messy)
memo_global = {}
def fib_global(n):
    if n in memo_global: return memo_global[n]
    if n <= 1: return n
    memo_global[n] = fib_global(n-1) + fib_global(n-2)
    return memo_global[n]

# 2. Closure (cleaner scope)
def make_fib():
    cache = {}
    def fib(n):
        if n in cache: return cache[n]
        if n <= 1: return n
        cache[n] = fib(n-1) + fib(n-2)
        return cache[n]
    return fib
fib_closure = make_fib()

# 3. lru_cache (best)
@functools.lru_cache(maxsize=None)
def fib_cached(n):
    if n <= 1: return n
    return fib_cached(n-1) + fib_cached(n-2)

print(fib_global(30), fib_closure(30), fib_cached(30))  # all 832040

Quando a Memoização Não Ajuda

A memoização só acelera problemas com subproblemas sobrepostos — casos em que o mesmo subproblema é calculado várias vezes. Se cada subproblema for único (como em um percurso simples de árvore, em que cada nó é visitado exatamente uma vez), a memoização adicionará sobrecarga sem benefício. Além disso, a memoização não pode corrigir problemas em que a árvore recursiva é exponencial no número de subproblemas distintos, e não no reaproveitamento — esses problemas exigem um algoritmo completamente diferente.

# Memoisation DOES help: overlapping sub-problems (Fibonacci)
# fib(n) reuses fib(n-2), fib(n-3), etc.

# Memoisation does NOT help: distinct sub-problems (permutations)
# Each unique (remaining_elements, target) pair is truly distinct
# The exponential complexity comes from the state space itself

print('Memoisation: useful when SAME sub-problem recurs multiple times')
print('Not useful: when every sub-problem is unique to one recursive path')

Resumo: Lista de Verificação da Memoização

Aplique a memoização quando: você tiver uma solução recursiva correta, mas lenta devido a recálculos redundantes; a função tiver um número pequeno de combinações distintas de argumentos; e o valor retornado depender apenas dos argumentos (função pura — sem efeitos colaterais e sem estado global). Verifique o espaço de estados dos subproblemas: se houver no máximo O(n) ou O(n²) estados distintos, a memoização transforma o tempo exponencial em polinomial.

Verificação Rápida

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

Recapitulação da Lição

Nesta lição, você aprendeu: a memoização armazena os resultados dos subproblemas para evitar recálculos, transformando a recursão exponencial em tempo polinomial, @functools.lru_cache é a ferramenta idiomática do Python e requer apenas uma linha e a memoização (de cima para baixo) e a tabulação (de baixo para cima) são os dois estilos de DP — a memoização é mais fácil de derivar, enquanto a tabulação evita problemas de profundidade da pilha. Parabéns — você concluiu os módulos de recursão e mapas de dispersão!

Perguntas Frequentes

A aula “Memoização: Armazenando Resultados Recursivos em Cache” é grátis?

Sim — o texto completo de “Memoização: Armazenando Resultados Recursivos em Cache” é 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 DSA Interview Prep, atualize para CoddyKit PRO. O curso de DSA Interview Prep inclui 4 aulas no total.

O que vou aprender em “Memoização: Armazenando Resultados Recursivos em Cache”?

Aplique @functools.lru_cache e dicionários de memoização manuais a Fibonacci e climbing-stairs para eliminar recomputações exponenciais. Você pratica DSA 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 DSA Interview Prep?

Nenhuma experiência prévia é necessária. DSA 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 4 de 4.

Quanto tempo leva a aula “Memoização: Armazenando Resultados Recursivos em Cache”?

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 DSA Interview Prep?

Sim. Cada aula de DSA 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. Estrutura da Recursão: Caso Base, Confiança, Construção
  2. Visualizando a Pilha de Chamadas
  3. Compromissos entre Recursão e Iteração
  4. Memoização: Armazenando Resultados Recursivos em Cache
← Voltar para DSA Interview Prep