Optimización espacial para DP 2D
Reduzca el espacio de LCS y de la distancia de edición de O(mn) a O(min(m,n)) conservando solo las filas actual y anterior de la tabla de DP.
Optimización espacial para DP 2D es una lección gratuita de DSA Interview Prep en CoddyKit. Esta es la lección 4 de 4. Puedes leer la lección completa abajo gratuitamente — luego la practicas en el navegador con un editor de código integrado y un tutor de IA 24/7. Forma parte de la ruta de aprendizaje de DSA Interview Prep, y tu progreso se sincroniza en la web y la app de CoddyKit. El curso de DSA Interview Prep incluye 4 lecciones en total.
Por qué importa el espacio en la DP bidimensional
Una tabla de DP bidimensional para cadenas de longitud 1000 requiere 1000×1000 = 1,000,000 celdas, aproximadamente 8 MB para enteros de 64 bits. Para secuencias más largas (alineación de ADN, diferencias de texto grandes), esto se vuelve poco práctico. La observación clave es que la mayoría de las recurrencias de DP bidimensional solo consultan la fila actual y la anterior, por lo que toda la tabla puede comprimirse en uno o dos arreglos unidimensionales. Este es el fundamento de la optimización del espacio en DP bidimensional.
# 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')Patrón del arreglo rodante
El patrón de arreglo rodante reemplaza la tabla bidimensional completa por un arreglo unidimensional que representa la fila anterior. Al calcular la fila i, actualice cada celda j usando el valor actual dp[j] (que todavía contiene el dp[i-1][j] de la fila anterior) y el dp[j-1] recién actualizado (que corresponde a dp[i][j-1]). Una variable diagonal captura dp[i-1][j-1] antes de que se sobrescriba. Este patrón se aplica a la LCS, la distancia de edición y la mayoría de los problemas de DP bidimensional.
# 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 con espacio O(min m,n)
Para la LCS, asegúrese de que text1 sea la cadena más corta (para que n sea pequeño). Reserve un arreglo unidimensional de tamaño n+1. Procese la tabla fila por fila. En cada celda: guarde temp = dp[j] (esto es dp[i-1][j]). Después: si los caracteres coinciden, dp[j] = diag + 1; de lo contrario, dp[j] = max(dp[j], dp[j-1]). Finalmente, establezca diag = temp. Después de procesar todas las filas, dp[n] contiene la longitud de la 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')) # 4Distancia de edición con espacio O(n)
La distancia de edición utiliza el mismo patrón rodante. El arreglo unidimensional inicial representa la fila 0: dp[j] = j (insertar j caracteres). Para cada fila i, establezca dp[0] = i (eliminar i caracteres) y guarde diag = dp[0] antes de la actualización. En el bucle interno, guarde temp = dp[j], calcule el nuevo valor a partir de la inserción (dp[j-1]+1), la eliminación (dp[j]+1) y el reemplazo (diag + cost), y después establezca 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')) # 5Suma mínima de caminos con espacio O(n)
Para la suma mínima de caminos en una cuadrícula, el arreglo unidimensional de actualización comienza con las sumas prefijas de la primera fila (solo hay una forma de llegar a cada celda de la primera fila). Para cada fila posterior, actualícelo de izquierda a derecha: dp[j] antes de actualizarlo contiene el valor de la fila superior (dp[i-1][j]), y dp[j-1], que acaba de actualizarse, contiene el valor de la izquierda. Aquí no se necesita la diagonal porque la suma mínima de caminos no requiere la celda 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)) # 7Cuándo se necesita acceso a la diagonal
No todos los problemas de DP 2D pueden comprimirse con un arreglo de actualización simple, porque algunos necesitan el elemento diagonal dp[i-1][j-1] después de que dp[j] se haya sobrescrito. La solución es siempre la misma: guarde temp = dp[j] antes de actualizarlo y úselo como diag para el cálculo de la siguiente columna. Esta anticipación de una celda gestiona de forma clara todas las recurrencias de tres direcciones (LCS, distancia de edición).
# 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')) # 4Optimización de espacio para la mochila 2D
El problema de la mochila 0/1 también se beneficia de la optimización de espacio. La tabla 2D completa tiene dimensiones (n_items+1) × (capacity+1). El arreglo de actualización la reduce a O(capacity). La diferencia fundamental con LCS y la distancia de edición es que se debe recorrer la dimensión de capacidad en orden inverso (de mayor a menor). Esto garantiza que cada elemento se cuente como máximo una vez; recorrerla hacia delante permitiría seleccionar un elemento varias veces.
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)Iteración hacia delante frente a iteración inversa
Saber en qué dirección recorrer el bucle interno es fundamental: inversa para la mochila 0/1 (cada elemento se usa como máximo una vez; consultar los estados anteriores evita reutilizarlo). Hacia delante para la mochila ilimitada (cada elemento se puede reutilizar; consultar los estados ya actualizados permite usarlo varias veces). Equivocarse cambia silenciosamente un problema de mochila 0/1 por uno de mochila ilimitada, o viceversa. Confirme siempre la restricción antes de elegir la dirección.
# 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+... )Caminos únicos con espacio O(n)
Para los caminos únicos, toda la tabla puede sustituirse por una sola fila. Inicialice todas las celdas a 1 (la primera fila). Para cada fila posterior, actualice de izquierda a derecha: dp[j] += dp[j-1]. No se necesita la diagonal porque la recurrencia solo utiliza la celda superior (dp[j], cuyo valor actual se conserva antes de la actualización) y la celda de la izquierda (dp[j-1], ya actualizada). Esta es la compresión 2D→1D más sencilla.
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]])) # 2Búfer de dos filas para recurrencias complejas
Cuando la recurrencia necesita celdas de dos o más filas anteriores (por ejemplo, en algunas variantes de DP de intervalos o reducciones de DP 3D), se utiliza un búfer de dos filas: mantenga los arreglos prev y curr e intercámbielos después de cada fila. Esto proporciona un espacio O(2n) = O(n). Para las recurrencias que retroceden k filas, mantenga k arreglos como un búfer circular. Esto generaliza el patrón del arreglo de actualización de una sola fila.
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')) # 4Cuándo no es posible optimizar el espacio
La optimización de espacio no siempre es posible. Si necesita reconstruir la solución óptima (no solo su valor), por lo general necesitará la tabla completa para hacer el backtracking. Algunas alternativas son: (1) almacenar una tabla de decisiones independiente del mismo tamaño; (2) utilizar el algoritmo de Hirschberg, que calcula LCS en tiempo O(mn) y espacio O(min(m,n)), incluida la reconstrucción, al dividir el problema recursivamente por el punto medio; (3) aceptar un espacio O(mn) cuando se requiere la reconstrucción.
# 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')Comprobación rápida
Ponga a prueba su comprensión de los conceptos de Data Structures & Algorithms — Coding Interview Prep de esta lección.
Resumen de la lección
En esta lección aprendió que: las tablas de DP 2D pueden comprimirse a un espacio O(n) mediante un arreglo 1D de actualización cuando solo se necesita la fila anterior; el patrón de la variable diagonal (guardar temp antes de sobrescribir) gestiona las recurrencias que necesitan dp[i-1][j-1]; y la mochila 0/1 recorre la capacidad en orden inverso, mientras que la mochila ilimitada la recorre hacia delante. A continuación estudiaremos la plantilla de backtracking: Elegir, Explorar y Deshacer, la base de los algoritmos de búsqueda exhaustiva.
Preguntas frecuentes
¿La lección «Optimización espacial para DP 2D» es gratis?
Sí — el texto completo de «Optimización espacial para DP 2D» es gratis para leer aquí en la web. Para practicarla de forma interactiva (editor de código integrado y tutor de IA 24/7) y desbloquear el resto del curso de DSA Interview Prep, actualiza a CoddyKit PRO. El curso de DSA Interview Prep incluye 4 lecciones en total.
¿Qué aprenderé en «Optimización espacial para DP 2D»?
Reduzca el espacio de LCS y de la distancia de edición de O(mn) a O(min(m,n)) conservando solo las filas actual y anterior de la tabla de DP. Practicas DSA Interview Prep con código real que ejecutas directamente en el navegador, y un tutor de IA 24/7 responde tus preguntas mientras trabajas en la lección.
¿Necesito experiencia previa para empezar DSA Interview Prep?
No se requiere experiencia previa. DSA Interview Prep en CoddyKit está estructurado para principiantes hasta estudiantes avanzados, así que puedes empezar aquí o desde el inicio y avanzar a tu ritmo. Esta es la lección 4 de 4.
¿Cuánto tiempo toma la lección «Optimización espacial para DP 2D»?
La mayoría de las lecciones de CoddyKit toman alrededor de 5–10 minutos. Cada una es compacta e interactiva, así que avanzas constantemente y retomas exactamente por donde dejaste en la web y la app.
¿Puedo escribir y ejecutar código en esta lección de DSA Interview Prep?
Sí. Cada lección de DSA Interview Prep incluye un editor de código integrado, así que escribes y ejecutas código real directamente en tu navegador y obtienes retroalimentación instantánea de IA — sin configuración local necesaria.
Todas las lecciones de este curso
- Rutas únicas y suma mínima de rutas en cuadrículas
- Subsecuencia común más larga
- Distancia de edición (Levenshtein)
- Optimización espacial para DP 2D