Una subsecuencia común más larga ( LCS ) es la subsecuencia común más larga a todas las secuencias en un conjunto de secuencias (a menudo solo dos secuencias). Se diferencia de la subcadena común más larga : a diferencia de las subcadenas, no se requiere que las subsecuencias ocupen posiciones consecutivas dentro de las secuencias originales. El problema de calcular las subsecuencias comunes más largas es un problema clásico de la informática , la base de los programas de comparación de datos como la diff
utilidad , y tiene aplicaciones en lingüística computacional y bioinformática . También es ampliamente utilizado por sistemas de control de revisión como Git para conciliar múltiples cambios realizados en una colección de archivos controlada por revisión.
Por ejemplo, considere las secuencias (ABCD) y (ACBAD). Tienen cinco subsecuencias comunes de longitud 2: (AB), (AC), (AD), (BD) y (CD); dos subsecuencias comunes de longitud 3: (ABD) y (ACD); y ya no tienen subsecuencias comunes. Por lo tanto, (ABD) y (ACD) son sus subsecuencias comunes más largas.
Para el caso general de un número arbitrario de secuencias de entrada, el problema es NP-duro . [1] Cuando el número de secuencias es constante, el problema se puede resolver en tiempo polinomial mediante programación dinámica .
Dadas secuencias de longitudes , una búsqueda ingenua probaría cada una de las subsecuencias de la primera secuencia para determinar si también son subsecuencias de las secuencias restantes; cada subsecuencia puede probarse en un tiempo lineal en las longitudes de las secuencias restantes, por lo que el tiempo para este algoritmo sería
Para el caso de dos secuencias de n y m elementos, el tiempo de ejecución del enfoque de programación dinámica es O ( n × m ). [2] Para un número arbitrario de secuencias de entrada, el enfoque de programación dinámica proporciona una solución en
Existen métodos con menor complejidad, [3] que a menudo dependen de la longitud del LCS, del tamaño del alfabeto o de ambos.
La LCS no es necesariamente única; en el peor de los casos, el número de subsecuencias comunes es exponencial en las longitudes de las entradas, por lo que la complejidad algorítmica debe ser al menos exponencial. [4]
El problema LCS tiene una subestructura óptima : el problema se puede dividir en subproblemas más pequeños y simples, que a su vez se pueden dividir en subproblemas más simples, y así sucesivamente, hasta que, finalmente, la solución se vuelve trivial. LCS en particular tiene subproblemas superpuestos : las soluciones a subproblemas de alto nivel a menudo reutilizan soluciones a subproblemas de nivel inferior. Los problemas con estas dos propiedades son susceptibles de enfoques de programación dinámica , en los que las soluciones de los subproblemas se memorizan , es decir, las soluciones de los subproblemas se guardan para su reutilización.
El prefijo S n de S se define como los primeros n caracteres de S . [5] Por ejemplo, los prefijos de S = (AGCA) son
Sea LCS ( X , Y ) una función que calcula una subsecuencia más larga común a X e Y . Esta función tiene dos propiedades interesantes.
LCS ( X ^ A , Y ^ A ) = LCS ( X , Y )^ A , para todas las cadenas X , Y y todos los símbolos A , donde ^ denota concatenación de cadenas. Esto permite simplificar el cálculo de LCS para dos secuencias que terminan en el mismo símbolo. Por ejemplo, LCS ("BANANA", "ATANA") = LCS ("BANAN", "ATAN")^"A", y para los demás símbolos comunes, LCS ("BANANA", "ATANA") = LCS ("BAN", "AT")^"ANA".
Si A y B son símbolos distintos ( A ≠ B ), entonces LCS (X^A,Y^B) es una de las cadenas de longitud máxima en el conjunto { LCS ( X ^ A , Y ), LCS ( X , Y ^ B ) }, para todas las cadenas X , Y .
Por ejemplo, LCS ("ABCDEFG", "BCDGK") es la cadena más larga entre LCS ("ABCDEFG", "BCDG") y LCS ("ABCDEF", "BCDGK"); si ambas tuvieran la misma longitud, se podría elegir una de ellas arbitrariamente.
Para realizar la propiedad se distinguen dos casos:
Sean dos sucesiones definidas de la siguiente manera: y . Los prefijos de son ; los prefijos de son . Sea el conjunto de la subsecuencia común más larga de los prefijos y . Este conjunto de sucesiones está dado por lo siguiente.
Para hallar el MCS de y , compare y . Si son iguales, entonces la secuencia se extiende con ese elemento, . Si no son iguales, entonces se conserva la más larga de las dos secuencias, , y . (Si tienen la misma longitud, pero no son idénticas, entonces se conservan ambas). El caso base, cuando o está vacío, es la cadena vacía , .
Se encontrará la subsecuencia más larga común a R = (GAC) y C = (AGCAT). Debido a que la función LCS utiliza un elemento "cero", es conveniente definir prefijos cero que estén vacíos para estas secuencias: R 0 = ε; y C 0 = ε. Todos los prefijos se colocan en una tabla con C en la primera fila (lo que la convierte en un encabezado de columna ) y R en la primera columna (lo que la convierte en un encabezado de fila ).
Esta tabla se utiliza para almacenar la secuencia LCS para cada paso del cálculo. La segunda columna y la segunda fila se han rellenado con ε, porque cuando se compara una secuencia vacía con una secuencia no vacía, la subsecuencia común más larga siempre es una secuencia vacía.
LCS ( R 1 , C 1 ) se determina comparando los primeros elementos de cada secuencia. G y A no son iguales, por lo que esta LCS obtiene (usando la "segunda propiedad") la más larga de las dos secuencias, LCS ( R 1 , C 0 ) y LCS ( R 0 , C 1 ). Según la tabla, ambas están vacías, por lo que LCS ( R 1 , C 1 ) también está vacía, como se muestra en la tabla siguiente. Las flechas indican que la secuencia proviene tanto de la celda de arriba, LCS ( R 0 , C 1 ) como de la celda de la izquierda, LCS ( R 1 , C 0 ).
LCS ( R 1 , C 2 ) se determina comparando G y G. Coinciden, por lo que G se añade a la secuencia superior izquierda, LCS ( R 0 , C 1 ), que es (ε), dando (εG), que es (G).
Para LCS ( R 1 , C 3 ), G y C no coinciden. La secuencia anterior está vacía; la de la izquierda contiene un elemento, G. Si seleccionamos la más larga de ellas, LCS ( R 1 , C 3 ) es (G). La flecha apunta hacia la izquierda, ya que es la más larga de las dos secuencias.
LCS ( R 1 , C 4 ), asimismo, es (G).
LCS ( R 1 , C 5 ), asimismo, es (G).
Para LCS ( R 2 , C 1 ), A se compara con A. Los dos elementos coinciden, por lo que A se agrega a ε, dando (A).
Para LCS ( R 2 , C 2 ), A y G no coinciden, por lo que se utiliza la más larga de LCS ( R 1 , C 2 ), que es (G), y LCS ( R 2 , C 1 ), que es (A). En este caso, cada una contiene un elemento, por lo que a esta LCS se le asignan dos subsecuencias: (A) y (G).
Para LCS ( R 2 , C 3 ), A no coincide con C. LCS ( R 2 , C 2 ) contiene las secuencias (A) y (G); LCS( R 1 , C 3 ) es (G), que ya está contenida en LCS ( R 2 , C 2 ). El resultado es que LCS ( R 2 , C 3 ) también contiene las dos subsecuencias, (A) y (G).
Para LCS ( R 2 , C 4 ), A coincide con A, que se agrega a la celda superior izquierda, dando (GA).
Para LCS ( R 2 , C 5 ), A no coincide con T. Comparando las dos secuencias, (GA) y (G), la más larga es (GA), por lo que LCS ( R 2 , C 5 ) es (GA).
Para LCS ( R 3 , C 1 ), C y A no coinciden, por lo que LCS ( R 3 , C 1 ) obtiene la más larga de las dos secuencias, (A).
En el caso de LCS ( R 3 , C 2 ), C y G no coinciden. Tanto LCS ( R 3 , C 1 ) como LCS ( R 2 , C 2 ) tienen un elemento. El resultado es que LCS ( R 3 , C 2 ) contiene las dos subsecuencias (A) y (G).
Para LCS ( R 3 , C 3 ), C y C coinciden, por lo que C se agrega a LCS ( R 2 , C 2 ), que contiene las dos subsecuencias, (A) y (G), dando (AC) y (GC).
Para LCS ( R 3 , C 4 ), C y A no coinciden. Al combinar LCS ( R 3 , C 3 ), que contiene (AC) y (GC), y LCS ( R 2 , C 4 ), que contiene (GA), se obtienen un total de tres secuencias: (AC), (GC) y (GA).
Finalmente, para LCS ( R 3 , C 5 ), C y T no coinciden. El resultado es que LCS ( R 3 , C 5 ) también contiene las tres secuencias, (AC), (GC) y (GA).
El resultado final es que la última celda contiene todas las subsecuencias comunes más largas de (AGCAT) y (GAC); estas son (AC), (GC) y (GA). La tabla también muestra las subsecuencias comunes más largas para cada par posible de prefijos. Por ejemplo, para (AGC) y (GA), las subsecuencias comunes más largas son (A) y (G).
Para calcular el LCS de una fila de la tabla LCS solo se necesitan las soluciones de la fila actual y de la fila anterior. Sin embargo, en el caso de secuencias largas, estas pueden ser numerosas y extensas, lo que requiere mucho espacio de almacenamiento. Se puede ahorrar espacio de almacenamiento guardando no las subsecuencias reales, sino la longitud de la subsecuencia y la dirección de las flechas, como se muestra en la tabla siguiente.
Las subsecuencias reales se deducen en un procedimiento de "rastreo" que sigue las flechas hacia atrás, comenzando desde la última celda de la tabla. Cuando la longitud disminuye, las secuencias deben haber tenido un elemento común. Son posibles varios caminos cuando se muestran dos flechas en una celda. A continuación se muestra la tabla para dicho análisis, con números coloreados en las celdas donde la longitud está a punto de disminuir. Los números en negrita trazan la secuencia, (GA). [6]
Para dos cadenas y , la longitud de la supersecuencia común más corta está relacionada con la longitud del LCS por [3]
La distancia de edición cuando solo se permite la inserción y eliminación (sin sustitución), o cuando el costo de la sustitución es el doble del costo de una inserción o eliminación, es:
La función siguiente toma como entrada las secuencias X[1..m]
y Y[1..n]
, calcula el LCS entre X[1..i]
y Y[1..j]
para todos 1 ≤ i ≤ m
y 1 ≤ j ≤ n
, y lo almacena en C[i,j]
. C[m,n]
contendrá la longitud del LCS de X
y Y
. [7]
función LCSLength(X[1..m], Y[1..n]) C = matriz(0..m, 0..n) para i := 0..m C[i,0] = 0 para j := 0..n C[0,j] = 0 para i := 1..m para j := 1..n si X[i] = Y[j] C[i,j] := C[i-1,j-1] + 1 demás C[i,j] := máx(C[i,j-1], C[i-1,j]) devuelve C[m,n]
Alternativamente, se podría utilizar la memorización .
La siguiente función retrocede en el tiempo a las opciones elegidas al calcular la C
tabla. Si los últimos caracteres de los prefijos son iguales, deben estar en un LCS. Si no es así, verifique cuál dio el LCS más grande de mantener y y haga la misma elección. Simplemente elija uno si tuvieran la misma longitud. Llame a la función con y .i=m
j=n
función backtrack(C[0..m,0..n], X[1..m], Y[1..n], i, j) si i = 0 o j = 0 devuelve "" si X[i] = Y[j] devuelve backtrack(C, X, Y, i-1, j-1) + X[i] si C[i,j-1] > C[i-1,j] devuelve backtrack(C, X, Y, i, j-1) devuelve backtrack(C, X, Y, i-1, j)
Si al elegir y se obtiene un resultado igualmente largo, se leen ambas subsecuencias resultantes. Esta función devuelve esto como un conjunto. Tenga en cuenta que esta función no es polinómica, ya que podría ramificarse en casi todos los pasos si las cadenas son similares.
función backtrackAll(C[0..m,0..n], X[1..m], Y[1..n], i, j) si i = 0 o j = 0 devuelve {""} si X[i] = Y[j] devuelve {Z + X[i] para todos los Z en backtrackAll(C, X, Y, i-1, j-1)} R := {} si C[i,j-1] ≥ C[i-1,j] R := retrocederTodo(C, X, Y, i, j-1) si C[i-1,j] ≥ C[i,j-1] R := R ∪ retrocederTodo(C, X, Y, i-1, j) volver R
Esta función retrocederá a través de la matriz C e imprimirá la diferencia entre las dos secuencias. Tenga en cuenta que obtendrá una respuesta diferente si intercambia ≥
y <
con >
y ≤
a continuación.
función printDiff(C[0..m,0..n], X[1..m], Y[1..n], i, j) si i >= 0 y j >= 0 y X[i] = Y[j] imprimirDiff(C, X, Y, i-1, j-1) imprimir " " + X[i] de lo contrario si j > 0 y (i = 0 o C[i,j-1] ≥ C[i-1,j]) imprimirDiff(C, X, Y, i, j-1) imprimir "+ " + Y[j] de lo contrario si i > 0 y (j = 0 o C[i,j-1] < C[i-1,j]) imprimirDiff(C, X, Y, i-1, j) imprimir "- " + X[i] demás imprimir ""
Sea “ ” y sea “ ”. La subsecuencia común más larga entre y es “ ”. La tabla que se muestra a continuación, que se genera mediante la función , muestra las longitudes de las subsecuencias comunes más largas entre los prefijos de y . La fila y la columna n.° muestran la longitud de la subsecuencia común más larga entre y .XMJYAUZ
MZJAWXU
MJAU
C
LCSLength
Los números resaltados muestran la ruta backtrack
que seguiría la función desde la esquina inferior derecha hasta la esquina superior izquierda, al leer un LCS. Si los símbolos actuales en y son iguales, son parte del LCS, y vamos hacia arriba y hacia la izquierda (mostrados en negrita ). Si no, vamos hacia arriba o hacia la izquierda, dependiendo de qué celda tenga un número más alto. Esto corresponde a tomar el LCS entre y , o y .
Se pueden realizar varias optimizaciones al algoritmo anterior para acelerarlo en casos del mundo real.
La matriz C en el algoritmo ingenuo crece cuadráticamente con las longitudes de las secuencias. Para dos secuencias de 100 elementos, se necesitaría una matriz de 10 000 elementos y se tendrían que realizar 10 000 comparaciones. En la mayoría de los casos del mundo real, especialmente en las diferencias y parches de código fuente, los comienzos y finales de los archivos rara vez cambian, y casi con certeza no ambos al mismo tiempo. Si solo han cambiado unos pocos elementos en el medio de la secuencia, se pueden eliminar el comienzo y el final. Esto reduce no solo los requisitos de memoria para la matriz, sino también la cantidad de comparaciones que se deben realizar.
función MCS(X[1..m], Y[1..n]) inicio := 1 m_fin := m n_fin := n Recortar los elementos coincidentes al principio mientras start ≤ m_end y start ≤ n_end y X[start] = Y[start] inicio := inicio + 1 Recortar los elementos coincidentes al final mientras start ≤ m_end y start ≤ n_end y X[m_end] = Y[n_end] m_fin := m_fin - 1 n_fin := n_fin - 1 C = matriz(inicio-1..m_fin, inicio-1..n_fin) solo recorre los elementos que han cambiado para i := start..m_end para j := start..n_end el algoritmo continúa como antes...
En el mejor de los casos, una secuencia sin cambios, esta optimización eliminaría la necesidad de la matriz C. En el peor de los casos, un cambio en el primer y último elemento de la secuencia, solo se realizan dos comparaciones adicionales.
La mayor parte del tiempo que emplea el algoritmo ingenuo se dedica a realizar comparaciones entre elementos de las secuencias. En el caso de secuencias de texto, como el código fuente, conviene ver las líneas como elementos de la secuencia en lugar de caracteres individuales. Esto puede implicar comparaciones de cadenas relativamente largas para cada paso del algoritmo. Se pueden realizar dos optimizaciones que pueden ayudar a reducir el tiempo que consumen estas comparaciones.
Se puede utilizar una función hash o suma de comprobación para reducir el tamaño de las cadenas en las secuencias. Es decir, para el código fuente donde la línea promedio tiene 60 o más caracteres de longitud, el hash o la suma de comprobación para esa línea podría tener solo entre 8 y 40 caracteres de longitud. Además, la naturaleza aleatoria de los hashes y las sumas de comprobación garantizaría que las comparaciones se acortaran más rápido, ya que las líneas de código fuente rara vez se modificarán al principio.
Esta optimización tiene tres inconvenientes principales. En primer lugar, es necesario dedicar una cierta cantidad de tiempo de antemano para calcular previamente los hashes de las dos secuencias. En segundo lugar, es necesario asignar memoria adicional para las nuevas secuencias con hashes. Sin embargo, en comparación con el algoritmo ingenuo utilizado aquí, ambos inconvenientes son relativamente mínimos.
El tercer inconveniente es el de las colisiones . Dado que no se garantiza que la suma de comprobación o el hash sean únicos, existe una pequeña posibilidad de que dos elementos diferentes se reduzcan al mismo hash. Esto es poco probable en el código fuente, pero es posible. Por lo tanto, un hash criptográfico sería mucho más adecuado para esta optimización, ya que su entropía será significativamente mayor que la de una suma de comprobación simple. Sin embargo, los beneficios pueden no valer la pena los requisitos de configuración y cálculo de un hash criptográfico para longitudes de secuencia pequeñas.
Si sólo se requiere la longitud de la LCS, la matriz se puede reducir a una matriz, o a un vector, ya que el enfoque de programación dinámica requiere sólo las columnas actual y anterior de la matriz. El algoritmo de Hirschberg permite la construcción de la secuencia óptima en sí misma en los mismos límites de tiempo cuadrático y espacio lineal. [8]
Chowdhury y Ramachandran idearon un algoritmo de espacio lineal de tiempo cuadrático [9] [10] para encontrar la longitud de LCS junto con una secuencia óptima que se ejecuta más rápido que el algoritmo de Hirschberg en la práctica debido a su rendimiento de caché superior. [9] El algoritmo tiene una complejidad de caché asintóticamente óptima bajo el modelo de caché ideal . [11] Curiosamente, el algoritmo en sí mismo no tiene en cuenta la caché [11], lo que significa que no toma ninguna decisión basada en los parámetros de caché (por ejemplo, tamaño de caché y tamaño de línea de caché) de la máquina.
Existen varios algoritmos que se ejecutan más rápido que el enfoque de programación dinámica presentado. Uno de ellos es el algoritmo Hunt–Szymanski , que normalmente se ejecuta en tiempo (para ), donde es el número de coincidencias entre las dos secuencias. [12] Para problemas con un tamaño de alfabeto acotado, se puede utilizar el Método de los Cuatro Rusos para reducir el tiempo de ejecución del algoritmo de programación dinámica por un factor logarítmico. [13]
Comenzando con Chvátal & Sankoff (1975), [14] varios investigadores han investigado el comportamiento de la longitud de la subsecuencia común más larga cuando las dos cadenas dadas se extraen aleatoriamente del mismo alfabeto. Cuando el tamaño del alfabeto es constante, la longitud esperada de la LCS es proporcional a la longitud de las dos cadenas, y las constantes de proporcionalidad (dependiendo del tamaño del alfabeto) se conocen como las constantes de Chvátal-Sankoff . Sus valores exactos no se conocen, pero se han demostrado límites superiores e inferiores en sus valores, [15] y se sabe que crecen inversamente proporcionalmente a la raíz cuadrada del tamaño del alfabeto. [16] Se ha demostrado que los modelos matemáticos simplificados del problema de la subsecuencia común más larga están controlados por la distribución de Tracy-Widom . [17]
{{cite book}}
: CS1 maint: multiple names: authors list (link)