0Pricing
DSA Interview Prep · Aula

Otimização de Espaço para DP 2D

Reduza o espaço de LCS e da distância de edição de O(mn) para O(min(m,n)) mantendo apenas as linhas atual e anterior da tabela DP.

Otimização de Espaço para DP 2D é 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.

Por que o espaço importa na DP 2D

Uma tabela de DP 2D para cadeias de comprimento 1000 requer 1000×1000 = 1.000.000 de células — aproximadamente 8 MB para inteiros de 64 bits. Para sequências mais longas (alinhamento de DNA, comparação de textos extensos), isso se torna impraticável. A observação principal é que a maioria das recorrências de DP 2D consulta apenas a linha atual e a anterior, portanto toda a tabela pode ser comprimida em um ou dois vetores unidimensionais. Esse é o princípio central da otimização de espaço em DP 2D.

# Full 2D DP: O(mn) space
# LCS for 1000-char strings
m, n = 1000, 1000
dp_2d_size = m * n * 8  # bytes (64-bit ints)
print(f'2D table: {dp_2d_size:,} bytes = {dp_2d_size//1024} KB')

# 1D rolling array: O(n) space
dp_1d_size = n * 8
print(f'1D array: {dp_1d_size:,} bytes = {dp_1d_size} bytes')
print(f'Space saving: {dp_2d_size // dp_1d_size}x')

Padrão de vetor deslizante

O padrão de vetor deslizante substitui a tabela 2D completa por um vetor unidimensional que representa a linha anterior. Ao calcular a linha i, atualize cada célula j usando o valor atual dp[j] (que ainda contém o dp[i-1][j] da linha anterior) e o dp[j-1] recém-atualizado (que corresponde a dp[i][j-1]). Uma variável diagonal captura dp[i-1][j-1] antes que ele seja sobrescrito. Esse padrão se aplica à LCS, à distância de edição e à maioria dos problemas de DP 2D.

# Rolling array template for 2D DP
# Before update: dp[j] holds dp[i-1][j] (previous row)
# After update: dp[j] holds dp[i][j] (current row)

def rolling_array_template(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * (n + 1)  # represents one row
    for i in range(1, m + 1):
        diag = 0  # stores dp[i-1][j-1] before overwrite
        for j in range(1, n + 1):
            temp = dp[j]  # save dp[i-1][j] before overwriting
            # compute dp[i][j] using dp[j] (above) and dp[j-1] (left) and diag
            dp[j] = diag + dp[j] + dp[j-1]  # placeholder logic
            diag = temp
    return dp[n]

LCS com espaço O(min m,n)

Para a LCS, certifique-se de que text1 seja a cadeia mais curta (para que n seja pequeno). Aloque um vetor unidimensional de tamanho n+1. Processe uma linha por vez. Em cada célula: salve temp = dp[j] (este é dp[i-1][j]). Em seguida: se os caracteres coincidirem, dp[j] = diag + 1; caso contrário, dp[j] = max(dp[j], dp[j-1]). Por fim, defina diag = temp. Depois de todas as linhas, dp[n] conterá o comprimento da LCS.

def lcs_space_opt(text1, text2):
    # Ensure text2 is the shorter one
    if len(text1) < len(text2):
        text1, text2 = text2, text1
    m, n = len(text1), len(text2)
    dp = [0] * (n + 1)
    for i in range(1, m + 1):
        diag = 0
        for j in range(1, n + 1):
            temp = dp[j]  # dp[i-1][j]
            if text1[i-1] == text2[j-1]:
                dp[j] = diag + 1
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp
    return dp[n]

print(lcs_space_opt('ABCBDAB', 'BDCABA'))  # 4
print(lcs_space_opt('AGGTAB', 'GXTXAYB')) # 4

Distância de edição com espaço O(n)

A distância de edição usa o mesmo padrão de vetor deslizante. O vetor unidimensional inicial representa a linha 0: dp[j] = j (inserindo j caracteres). Para cada linha i, defina dp[0] = i (excluindo i caracteres) e salve diag = dp[0] antes da atualização. No laço interno, salve temp = dp[j], calcule o novo valor a partir de inserir (dp[j-1]+1), excluir (dp[j]+1) e substituir (diag + cost), depois defina diag = temp.

def edit_dist_opt(s, t):
    m, n = len(s), len(t)
    dp = list(range(n + 1))   # row 0: dp[0][j] = j
    for i in range(1, m + 1):
        diag = dp[0]           # dp[i-1][0] before dp[0] update
        dp[0] = i              # dp[i][0] = i
        for j in range(1, n + 1):
            temp = dp[j]       # dp[i-1][j]
            cost = 0 if s[i-1] == t[j-1] else 1
            dp[j] = min(
                dp[j-1] + 1,  # insert
                dp[j] + 1,    # delete
                diag + cost   # replace or match
            )
            diag = temp
    return dp[n]

print(edit_dist_opt('horse', 'ros'))  # 3
print(edit_dist_opt('intention', 'execution'))  # 5

Soma mínima do caminho com espaço O(n)

Para a soma mínima do caminho em uma grade, o vetor unidimensional deslizante começa como as somas dos prefixos da primeira linha (há apenas uma maneira de chegar a cada célula da primeira linha). Para cada linha seguinte, atualize da esquerda para a direita: dp[j] antes da atualização é o valor da linha acima (dp[i-1][j]), e dp[j-1], recém-atualizado, vem da esquerda. Nenhuma diagonal é necessária aqui, pois a soma mínima do caminho não exige a célula diagonal.

def min_path_sum_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [float('inf')] * n
    dp[0] = 0
    for i in range(m):
        # Update first column (only from above)
        dp[0] += grid[i][0]
        for j in range(1, n):
            # min of above (dp[j] = old) and left (dp[j-1] = updated)
            dp[j] = grid[i][j] + min(dp[j], dp[j-1])
    return dp[n-1]

grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum_opt(grid))  # 7

Quando o acesso à diagonal é necessário

Nem todos os problemas de DP bidimensional podem ser compactados com um simples vetor deslizante, pois alguns precisam do elemento diagonal dp[i-1][j-1] depois que dp[j] foi sobrescrito. A solução é sempre a mesma: salve temp = dp[j] antes de atualizá-lo e use-o como diag no cálculo da próxima coluna. Essa antecipação de uma célula trata de forma simples todas as recorrências de três direções (LCS, distância de edição).

# Recap: the diagonal save pattern
# Without it: dp[j-1] updated (left) and dp[j] about to be overwritten
# With it:

def show_diagonal_pattern(s1, s2):
    n = len(s2)
    dp = [0] * (n + 1)
    for ch1 in s1:
        diag = 0  # was dp[i-1][0] = 0 for LCS
        for j, ch2 in enumerate(s2, 1):
            temp = dp[j]  # SAVE before overwrite
            if ch1 == ch2:
                dp[j] = diag + 1  # use saved diagonal
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp  # advance diagonal
    return dp[n]

print(show_diagonal_pattern('ABCBDAB', 'BDCABA'))  # 4

Otimização de espaço da mochila 2D

O problema da mochila 0/1 também se beneficia da otimização de espaço. A tabela 2D completa tem dimensões (número de itens + 1) × (capacidade + 1). O vetor deslizante reduz isso para O(capacidade). A diferença fundamental em relação a LCS e à distância de edição é percorrer a dimensão da capacidade em ordem reversa (do maior para o menor). Isso garante que cada item seja contado no máximo uma vez — percorrê-la para a frente permitiria selecionar um item várias vezes.

def knapsack_01(weights, values, capacity):
    dp = [0] * (capacity + 1)
    for w, v in zip(weights, values):
        # Reverse order: prevents using the same item twice
        for c in range(capacity, w - 1, -1):
            dp[c] = max(dp[c], dp[c - w] + v)
    return dp[capacity]

weights = [1, 3, 4, 5]
values  = [1, 4, 5, 7]
cap = 7
print(knapsack_01(weights, values, cap))  # 9 (items 3+4: weight 3+4=7, value 4+5=9)

Iteração para a frente versus reversa

Saber em qual direção percorrer o laço interno é essencial: reversa para a mochila 0/1 (cada item é usado no máximo uma vez — consultar estados anteriores evita reutilizá-lo). Para a frente para a mochila ilimitada (cada item pode ser reutilizado — consultar estados já atualizados permite vários usos). Escolher a direção errada altera silenciosamente uma mochila 0/1 para uma mochila ilimitada, ou vice-versa. Sempre confirme a restrição antes de escolher a direção.

# 0/1 Knapsack: each item used AT MOST ONCE → iterate reverse
def knapsack_01_demo(weights, values, cap):
    dp = [0] * (cap + 1)
    for w, v in zip(weights, values):
        for c in range(cap, w-1, -1):  # REVERSE
            dp[c] = max(dp[c], dp[c-w] + v)
    return dp[cap]

# Unbounded Knapsack: items can be reused → iterate forward
def knapsack_unbounded(weights, values, cap):
    dp = [0] * (cap + 1)
    for c in range(1, cap + 1):
        for w, v in zip(weights, values):
            if c >= w:
                dp[c] = max(dp[c], dp[c-w] + v)  # FORWARD
    return dp[cap]

print(knapsack_01_demo([2,3],[3,4],5))     # 7
print(knapsack_unbounded([2,3],[3,4],5))   # 8 (use weight-2 twice: 3+3=6? or 4+... )

Caminhos únicos com espaço O(n)

Para caminhos únicos, a tabela inteira pode ser substituída por uma única linha. Inicialize todas as células com 1 (a primeira linha). Para cada linha seguinte, atualize da esquerda para a direita: dp[j] += dp[j-1]. Nenhuma diagonal é necessária, pois a recorrência usa apenas a célula acima (dp[j], valor atual antes da atualização) e a célula à esquerda (dp[j-1], já atualizada). Esta é a forma mais simples de fazer a compressão de 2D para 1D.

def unique_paths_opt(m, n):
    dp = [1] * n  # first row: all 1s
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]  # above (dp[j]) + left (dp[j-1])
    return dp[n-1]

# With obstacles
def unique_paths_obstacles_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * n
    dp[0] = 1
    for i in range(m):
        if grid[i][0] == 1: dp[0] = 0  # blocked column
        for j in range(1, n):
            if grid[i][j] == 1: dp[j] = 0  # blocked
            else: dp[j] += dp[j-1]
    return dp[n-1]

print(unique_paths_opt(3, 7))  # 28
print(unique_paths_obstacles_opt([[0,0,0],[0,1,0],[0,0,0]]))  # 2

Estrutura temporária de duas linhas para recorrências complexas

Quando a recorrência precisa de células de duas ou mais linhas anteriores (por exemplo, em algumas variantes de DP de intervalos ou em reduções de DP 3D), utiliza-se uma estrutura temporária de duas linhas: mantenha uma matriz para a linha anterior e outra para a linha atual e troque-as depois de cada linha. Isso proporciona espaço O(2n) = O(n). Para recorrências que consultam k linhas anteriores, mantenha k matrizes em uma estrutura circular. Essa é uma generalização do padrão do vetor deslizante de uma linha.

def lcs_two_row_buffer(s1, s2):
    m, n = len(s1), len(s2)
    prev = [0] * (n + 1)  # dp[i-1]
    curr = [0] * (n + 1)  # dp[i]
    for i in range(1, m + 1):
        curr[0] = 0
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                curr[j] = prev[j-1] + 1
            else:
                curr[j] = max(prev[j], curr[j-1])
        prev, curr = curr, prev  # swap (curr becomes prev)
    return prev[n]  # after swap, prev holds the last computed row

print(lcs_two_row_buffer('ABCBDAB', 'BDCABA'))  # 4

Quando a otimização de espaço não é possível

A otimização de espaço não é sempre possível. Se for necessário reconstruir a solução ótima (e não apenas seu valor), geralmente será preciso manter a tabela completa para fazer o retrocesso. As alternativas incluem: (1) armazenar uma tabela de decisões separada, do mesmo tamanho; (2) usar o algoritmo de Hirschberg, que calcula LCS em tempo O(mn) e espaço O(min(m,n)), incluindo a reconstrução, ao dividir o problema recursivamente no ponto médio; (3) aceitar espaço O(mn) quando a reconstrução for necessária.

# When reconstruction needed: must keep full table or use Hirschberg
# Hirschberg's idea: compute LCS length in O(n) space at midpoint of s1,
# recurse on left and right halves. O(mn) time, O(n) space + reconstruction.

# For interview: mention the trade-off
# 'I can reduce to O(n) space if only the value is needed.
#  To also reconstruct the sequence, I need the full O(mn) table
#  or a more complex divide-and-conquer approach.'

print('Space opt: O(n) for length only')
print('Full table: O(mn) needed for reconstruction')

Verificação rápida

Teste seus conhecimentos sobre os 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: as tabelas de DP 2D podem ser compactadas para ocupar espaço O(n) usando um vetor 1D deslizante quando apenas a linha anterior é necessária, o padrão da variável diagonal (salvar a variável temporária antes de sobrescrevê-la) trata de recorrências que precisam de dp[i-1][j-1] e a mochila 0/1 percorre a capacidade em ordem reversa, enquanto a mochila ilimitada a percorre para a frente. Em seguida, estudaremos o modelo de retrocesso: Escolher, Explorar, Desfazer — a base dos algoritmos de busca exaustiva.

Perguntas Frequentes

A aula “Otimização de Espaço para DP 2D” é grátis?

Sim — o texto completo de “Otimização de Espaço para DP 2D” é 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 “Otimização de Espaço para DP 2D”?

Reduza o espaço de LCS e da distância de edição de O(mn) para O(min(m,n)) mantendo apenas as linhas atual e anterior da tabela DP. 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 “Otimização de Espaço para DP 2D”?

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. Caminhos Únicos e Soma Mínima de Caminho em Grades
  2. Maior Subse­quência Comum
  3. Distância de Edição (Levenshtein)
  4. Otimização de Espaço para DP 2D
← Voltar para DSA Interview Prep