Algoritmo de la Subseccuencia Creciente Más Larga (LIS)
La Subseccuencia Creciente Más Larga (LIS) es un clásico de la teoría de algoritmos y tiene aplicaciones en bioinformática, análisis de series temporales, planificación de tareas y mucho más. En este artículo descubrirás:
- Definición formal y ejemplos intuitivos.
- Implementación tradicional O(N²) basada en programación dinámica.
- Implementación avanzada O(N log N) usando la técnica de Patience Sorting.
- Cómo reconstruir la subsecuencia resultante.
- Comparativa de rendimiento, limitaciones y buenas prácticas.
1. Conceptos Básicos
Una subseccuencia de una secuencia A = [a₁, a₂, …, aₙ] es cualquier conjunto de elementos que conserva el orden original pero no necesariamente son contiguos. La subsecuencia es creciente si cada elemento es estrictamente mayor que el anterior.
Formalmente, buscamos la mayor longitud k tal que exista un índice i₁ < i₂ < … < iₖ con a_{i₁} < a_{i₂} < … < a_{iₖ}.
Ejemplo ilustrativo
Para la serie [10, 22, 9, 33, 21, 50, 41, 60, 80] una LIS válida es [10, 22, 33, 50, 60, 80] con longitud 6.
2. Implementación O(N²) – Programación Dinámica
def lis_dp(arr):
"""Devuelve la longitud de la LIS usando DP O(N²)."""
n = len(arr)
if n == 0:
return 0
# dp[i] = longitud de la LIS que termina en i
dp = [1] * n
for i in range(1, n):
for j in range(i):
if arr[j] < arr[i] and dp[j] + 1 > dp[i]:
dp[i] = dp[j] + 1
return max(dp)
# Ejemplo de uso
seq = [10, 22, 9, 33, 21, 50, 41, 60, 80]
print("LIS longitud:", lis_dp(seq))
Esta versión es fácil de entender y adecuada para n ≤ 10⁴. Su complejidad temporal es O(N²) y espacial O(N).
3. Implementación O(N log N) – Patience Sorting
La idea clave es mantener una lista tails donde tails[i] es el menor posible último elemento de una subsecuencia creciente de longitud i+1. Cada nuevo número se inserta con bisect_left, lo que garantiza O(log N) por iteración.
Complejidad
- Tiempo:
O(N log N) - Espacio:
O(N)(solotailsy opcionalmenteprev_idxpara reconstruir).
import bisect
def lis_patience(arr):
"""Devuelve la longitud y la subsecuencia real usando O(N log N)."""
if not arr:
return 0, []
# tails almacena los valores finales mínimos
tails = []
# parent[i] guarda el índice del elemento anterior en la LIS que termina en i
parent = [-1] * len(arr)
# pos[i] guarda la posición (longitud-1) donde arr[i] se inserta en tails
pos = [0] * len(arr)
for i, x in enumerate(arr):
# búsqueda binaria en tails
idx = bisect.bisect_left(tails, x)
if idx == len(tails):
tails.append(x)
else:
tails[idx] = x
pos[i] = idx
# actualizar puntero al anterior elemento de la subsecuencia
if idx > 0:
# buscar el último índice j < i con pos[j] == idx-1 y arr[j] < x
# se puede mantener una lista de índices por longitud para O(1)
# aquí usamos una búsqueda lineal simple (suficiente para demo)
for j in range(i-1, -1, -1):
if pos[j] == idx-1 and arr[j] < x:
parent[i] = j
break
# reconstruir la LIS
lis_len = len(tails)
lis = []
# encontrar el último índice con posición lis_len-1
last = max(i for i, p in enumerate(pos) if p == lis_len-1)
while last != -1:
lis.append(arr[last])
last = parent[last]
lis.reverse()
return lis_len, lis
# Demo
seq = [10, 22, 9, 33, 21, 50, 41, 60, 80]
length, subseq = lis_patience(seq)
print("Longitud:", length)
print("LIS:", subseq)
Esta versión escala a millones de elementos sin perder rendimiento.
4. Comparativa de Rendimiento
| Métrica | DP O(N²) | Patience O(N log N) |
|---|---|---|
| Complejidad Temporal | O(N²) | O(N log N) |
| Complejidad Espacial | O(N) | O(N) |
| Rango práctico de N | ≈10⁴ | ≥10⁶ |
| Facilidad de Implementación | Alta | Media (uso de bisect y reconstrucción) |
| Uso de memoria adicional | Solo dp | tails + parent + pos |
5. Buenas Prácticas y Trucos de Optimización
- Pre‑procesado de datos: si la secuencia contiene muchos valores duplicados, reduzca a pares
(valor, índice)y mantenga solo la primera aparición. - Uso de
array('I')onumpy.ndarray: para entradas extremadamente grandes, estos tipos reducen la sobrecarga de objetos Python. - Vectorización (NumPy): aunque la lógica binaria no se vectoriza fácilmente, la generación de la lista de entrada sí.
- Paralelismo: la fase O(N²) puede dividirse en bloques y combinarse con
multiprocessing, pero la versión O(N log N) ya es suficientemente rápida para la mayoría de los casos. - Gestión de memoria: reutilice la lista
tailsen lugar de crear nuevas en bucles anidados.
6. Casos de Uso Reales
Algunos escenarios donde la LIS es la herramienta adecuada:
- Bioinformática: alineamiento de secuencias genómicas para detectar regiones conservadas.
- Finanzas: identificar la mayor racha de precios crecientes en series temporales de acciones.
- Planificación de tareas: encontrar la mayor cadena de dependencias compatibles en sistemas de producción.
- Compresión de datos: algoritmos de codificación que aprovechan patrones crecientes para reducir redundancia.
© 2025 BlogTech – Todos los derechos reservados.
Algoritmo de la Subseccuencia Creciente Más Larga (LIS) en Python: Guía Completa y Ejemplos Prácticos