WhatsApp

  

Algoritmo de la Subseccuencia Creciente Más Larga (LIS) en Python: Guía Completa y Ejemplos Prácticos

Aprende a resolver el problema de la Subseccuencia Creciente Más Larga (LIS) con Python. Explicación teórica, implementaciones O(N²) y O(N log N), casos de uso, comparativas y buenas prácticas.

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) (solo tails y opcionalmente prev_idx para 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 TemporalO(N²)O(N log N)
Complejidad EspacialO(N)O(N)
Rango práctico de N≈10⁴≥10⁶
Facilidad de ImplementaciónAltaMedia (uso de bisect y reconstrucción)
Uso de memoria adicionalSolo dptails + 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') o numpy.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 tails en lugar de crear nuevas en bucles anidados.

6. Casos de Uso Reales

Algunos escenarios donde la LIS es la herramienta adecuada:

  1. Bioinformática: alineamiento de secuencias genómicas para detectar regiones conservadas.
  2. Finanzas: identificar la mayor racha de precios crecientes en series temporales de acciones.
  3. Planificación de tareas: encontrar la mayor cadena de dependencias compatibles en sistemas de producción.
  4. 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
ASIMOV Ingeniería S. de R.L. de C.V., Emiliano Nava 15 noviembre, 2025
Compartir
Iniciar sesión dejar un comentario

  
Algoritmo de la Subsequencia Común Más Larga (LCS) en Python: Guía Completa y Ejemplos Prácticos
Aprende a implementar, optimizar y aplicar el algoritmo de la Subsequencia Común Más Larga (LCS) en Python. Incluye ejemplos, comparativas, buenas prácticas y casos de uso reales.