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')) # 4Distâ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')) # 5Soma 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)) # 7Quando 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')) # 4Otimizaçã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]])) # 2Estrutura 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')) # 4Quando 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
- Caminhos Únicos e Soma Mínima de Caminho em Grades
- Maior Subsequência Comum
- Distância de Edição (Levenshtein)
- Otimização de Espaço para DP 2D