¿Qué es la LCS?
La Subsequencia Común Más Larga (LCS) es la secuencia más larga que aparece en el mismo orden (no necesariamente contiguo) en dos cadenas de caracteres o en dos secuencias de elementos. Formalmente, dadas dos secuencias X = x₁…xₘ y Y = y₁…yₙ, la LCS es una secuencia Z tal que:
Zes una subsecuencia deXy deY.- La longitud de
Zes máxima entre todas las subsecuencias comunes.
Ejemplo clásico: para "AGGTAB" y "GXTXAYB" la LCS es "GTAB" (longitud 4).
Aplicaciones Reales
- Comparación de archivos y detección de diferencias (diff tools).
- Bioinformática: alineamiento de secuencias de ADN/ARN.
- Versionado de código fuente y sistemas de control de versiones.
- Reconstrucción de texto a partir de entradas parciales (autocompletar).
- Plagio y detección de similitud de documentos.
Enfoques Algorítmicos
1. Solución Recursiva (exponencial)
def lcs_recursive(X, Y, i, j):
if i == 0 or j == 0:
return ""
if X[i-1] == Y[j-1]:
return lcs_recursive(X, Y, i-1, j-1) + X[i-1]
else:
a = lcs_recursive(X, Y, i-1, j)
b = lcs_recursive(X, Y, i, j-1)
return a if len(a) > len(b) else b
Esta versión explora todas las combinaciones y tiene complejidad O(2^{min(m,n)}). Es útil solo para entender la relación de sub‑problemas.
2. Programación Dinámica (tabular)
Almacena los resultados parciales en una tabla dp[m+1][n+1]. Cada celda representa la longitud de la LCS de los prefijos X[0..i) y Y[0..j).
def lcs_dp(X, Y):
m, n = len(X), len(Y)
dp = [[0] * (n + 1) for _ in range(m + 1)]
for i in range(1, m + 1):
for j in range(1, n + 1):
if X[i-1] == Y[j-1]:
dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
# Reconstruir la LCS
i, j = m, n
lcs = []
while i > 0 and j > 0:
if X[i-1] == Y[j-1]:
lcs.append(X[i-1])
i -= 1; j -= 1
elif dp[i-1][j] >= dp[i][j-1]:
i -= 1
else:
j -= 1
return ''.join(reversed(lcs))
Complejidad temporal O(m·n) y espacial O(m·n).
Optimización de Espacio
Para secuencias muy largas (p.ej., m,n > 10⁶) la tabla completa puede ser prohibitiva. Observando que cada fila depende solo de la fila anterior, podemos reducir el uso de memoria a O(min(m,n)):
def lcs_space_optimized(X, Y):
if len(Y) > len(X):
X, Y = Y, X # aseguramos que X es la más larga
previous = [0] * (len(Y) + 1)
for i in range(1, len(X) + 1):
current = [0]
for j in range(1, len(Y) + 1):
if X[i-1] == Y[j-1]:
current.append(previous[j-1] + 1)
else:
current.append(max(previous[j], current[-1]))
previous = current
# Reconstruir la LCS requiere una pasada extra o almacenar índices; aquí devolvemos solo la longitud
return previous[-1]
Este enfoque es ideal para servicios de micro‑servicios o contenedores con memoria limitada.
Comparativa con Algoritmos Relacionados
Longest Increasing Subsequence (LIS)
Mientras LCS busca coincidencias entre dos secuencias, LIS trabaja sobre una sola secuencia y busca la subsecuencia estrictamente creciente. Ambas usan DP, pero LIS puede resolverse en O(n log n) con búsqueda binaria.
Edit Distance (Levenshtein)
La distancia de edición mide cuántas inserciones, borrados y sustituciones se requieren para transformar una cadena en otra. Comparten la tabla DP, pero la fórmula de actualización difiere: dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]).
Pruebas Unitarias y Buenas Prácticas
import unittest
class TestLCS(unittest.TestCase):
def test_basic(self):
self.assertEqual(lcs_dp('AGGTAB', 'GXTXAYB'), 'GTAB')
self.assertEqual(lcs_dp('', 'ABC'), '')
self.assertEqual(lcs_dp('ABC', ''), '')
self.assertEqual(lcs_dp('ABC', 'ABC'), 'ABC')
def test_space_optimized(self):
self.assertEqual(lcs_space_optimized('AGGTAB', 'GXTXAYB'), 4)
self.assertEqual(lcs_space_optimized('ABCDEF', 'FBDAMN'), 2) # "BD"
if __name__ == '__main__':
unittest.main()
Ejecuta python -m unittest nombre_del_archivo.py para validar. Mantén los tests en un directorio tests/ y usa CI/CD para asegurar que futuros cambios no rompan la lógica.
Consideraciones de Rendimiento y Escalabilidad
- Paralelismo: La tabla DP es inherentemente secuencial, pero se pueden paralelizar las diagonalas usando
numpyonumbapara GPUs. - Streaming: En flujos de datos (p. ej., logs en tiempo real) se puede aplicar la variante online LCS que mantiene sólo la última fila y actualiza incrementalmente.
- Contenedores: Cuando despliegas en Docker/Podman, define
--memoryy--cpusadecuados; la versión optimizada ocupa~2·min(m,n)·4bytesde RAM. - Cache: Usa
functools.lru_cacheen la versión recursiva para evitar recalcular sub‑problemas cuando el dominio es pequeño.
Resolución de Problemas Comunes (Troubleshooting)
- Resultado vacío inesperado: Verifica que los caracteres coinciden exactamente (mayúsculas/minúsculas). Usa
.lower()si la comparación debe ser case‑insensitive. - Exceso de memoria: Cambia a la versión
lcs_space_optimizedo implementa la variante de Hirschberg que reduce la memoria aO(min(m,n))y permite reconstruir la cadena. - Desbordamiento de tiempo en grandes textos: Considera usar algoritmos de alineamiento aproximado (p. ej., Smith‑Waterman) o limitar la longitud máxima con recortes heurísticos.
- Incompatibilidad de tipos: Asegúrate de pasar
stro listas de caracteres; si trabajas con bytes, decodifica primero.
Conclusión
El algoritmo de la Subsequencia Común Más Larga es una pieza fundamental en la comparación de secuencias y tiene aplicaciones que van desde la bioinformática hasta el control de versiones. Con Python puedes implementarlo de forma clara y, según tus restricciones de memoria y tiempo, elegir entre la versión recursiva, la tabla completa o la variante optimizada. Incorporar pruebas automatizadas, monitorizar el uso de recursos y aplicar buenas prácticas de contenedorización garantizan una solución robusta y escalable.