Encontrar repeticiones
Se da un string de longitud .
Una repetición es dos ocurrencias de un string seguidas. En otras palabras, una repetición se puede describir por un par de índices tales que la subcadena consiste en dos strings idénticos escritos uno después del otro.
El desafío es encontrar todas las repeticiones en un string dado . O una tarea simplificada: encontrar alguna repetición o encontrar la repetición más larga.
El algoritmo descrito aquí fue publicado en 1982 por Main y Lorentz.
Ejemplo
Consideremos las repeticiones en el siguiente string de ejemplo:
El string contiene las siguientes tres repeticiones:
Otro ejemplo:
Aquí hay solo dos repeticiones
Número de repeticiones
En general puede haber hasta repeticiones en un string de longitud . Un ejemplo obvio es un string que consiste en veces la misma letra; en este caso cualquier subcadena de longitud par es una repetición. En general cualquier string periódico con un período corto contendrá muchas repeticiones.
Por otro lado, este hecho no impide computar el número de repeticiones en tiempo , porque el algoritmo puede dar las repeticiones en forma comprimida, en grupos de varias piezas a la vez.
Existe incluso el concepto que describe grupos de subcadenas periódicas con tuplas de tamaño cuatro. Se ha demostrado que el número de tales grupos es a lo sumo lineal respecto de la longitud del string.
Además, aquí hay algunos resultados más interesantes relacionados con el número de repeticiones:
-
El número de repeticiones primitivas (aquellas cuyas mitades no son repeticiones) es a lo sumo .
-
Si codificamos las repeticiones con tuplas de números (llamadas triples de Crochemore) (donde es la posición del comienzo, la longitud de la subcadena que se repite, y el número de repeticiones), entonces todas las repeticiones se pueden describir con de tales triples.
-
Los strings de Fibonacci, definidos como
son “fuertemente” periódicos. El número de repeticiones en el string de Fibonacci , incluso comprimido con triples de Crochemore, es . El número de repeticiones primitivas también es .
Algoritmo de Main-Lorentz
La idea detrás del algoritmo de Main-Lorentz es divide y vencerás.
Parte el string inicial en mitades, y computa el número de repeticiones que yacen por completo en cada mitad mediante dos llamadas recursivas. Luego viene la parte difícil. El algoritmo encuentra todas las repeticiones que empiezan en la primera mitad y terminan en la segunda mitad (que llamaremos repeticiones cruzadas). Esta es la parte esencial del algoritmo de Main-Lorentz, y la discutiremos en detalle aquí.
La complejidad de los algoritmos de divide y vencerás está bien investigada. El teorema maestro dice que terminaremos con un algoritmo si podemos computar las repeticiones cruzadas en tiempo .
Búsqueda de repeticiones cruzadas
Así, queremos encontrar todas las repeticiones que empiezan en la primera mitad del string, llamémosla , y terminan en la segunda mitad, llamémosla :
Sus longitudes son aproximadamente iguales a la longitud de dividida por dos.
Consideremos una repetición arbitraria y miremos el carácter del medio (más precisamente el primer carácter de la segunda mitad de la repetición). Es decir, si la repetición es una subcadena , entonces el carácter del medio es .
Llamamos a una repetición izquierda o derecha según en qué string se ubica este carácter: en el string o en el string . En otras palabras, un string se llama izquierdo si la mayoría de él yace en ; de lo contrario lo llamamos derecho.
Discutiremos ahora cómo encontrar todas las repeticiones izquierdas. Encontrar todas las repeticiones derechas se puede hacer de la misma manera.
Denotemos la longitud de la repetición izquierda por (es decir, cada mitad de la repetición tiene longitud ). Consideremos el primer carácter de la repetición que cae en el string (está en la posición del string ). Coincide con el carácter posiciones antes, denotemos esta posición .
Fijaremos esta posición , y buscaremos todas las repeticiones en esta posición .
Por ejemplo:
Las líneas verticales dividen las dos mitades. Aquí fijamos la posición , y en esta posición encontramos la repetición .
Está claro que si fijamos la posición , simultáneamente fijamos la longitud de las repeticiones posibles: . Una vez que sepamos cómo encontrar estas repeticiones, iteraremos sobre todos los valores posibles de de a , y encontraremos todas las repeticiones cruzadas izquierdas de longitud .
Criterio para repeticiones cruzadas izquierdas
Ahora, ¿cómo podemos encontrar todas esas repeticiones para un fijado? Hay que tener en cuenta que todavía puede haber múltiples de tales repeticiones.
Miremos de nuevo una visualización, esta vez para la repetición :
Aquí denotamos las longitudes de las dos piezas de la repetición con y : es la longitud de la repetición hasta la posición , y es la longitud de la repetición desde hasta el final de la mitad de la repetición. Tenemos como la longitud total de la repetición.
Generemos condiciones necesarias y suficientes para tal repetición en la posición de longitud :
- Sea el mayor número tal que los primeros caracteres antes de la posición coinciden con los últimos caracteres del string :
- Sea el mayor número tal que los caracteres que empiezan en la posición coinciden con los primeros caracteres del string :
u[cntr \dots cntr + k_2 - 1] = v[0 \dots k_2 - 1]
- Entonces tenemos una repetición exactamente para cualquier par con
Para resumir:
- Fijamos una posición específica .
- Todas las repeticiones que encontraremos ahora tienen longitud . Puede haber múltiples de tales repeticiones; dependen de las longitudes y .
- Encontramos y como se describió arriba.
- Entonces todas las repeticiones adecuadas son aquellas para las que las longitudes de las piezas y satisfacen las condiciones:
Por lo tanto la única parte que queda es cómo podemos computar los valores y rápidamente para cada posición . Afortunadamente podemos computarlos en usando la función Z:
- Podemos encontrar el valor para cada posición calculando la función Z del string (es decir, el string invertido). Entonces el valor para un particular será igual al valor correspondiente del arreglo de la función Z.
- Para precomputar todos los valores , calculamos la función Z del string (es decir, el string concatenado con el carácter separador y el string ). De nuevo solo necesitamos consultar el valor correspondiente en la función Z para obtener el valor .
Así esto es suficiente para encontrar todas las repeticiones cruzadas izquierdas.
Repeticiones cruzadas derechas
Para computar las repeticiones cruzadas derechas actuamos de forma similar: definimos el centro como el carácter correspondiente al último carácter del string .
Entonces la longitud se definirá como el mayor número de caracteres antes de la posición (inclusive) que coinciden con los últimos caracteres del string . Y la longitud se definirá como el mayor número de caracteres que empiezan en que coinciden con los caracteres del string .
Así podemos encontrar los valores y computando la función Z de los strings y .
Después de eso podemos encontrar las repeticiones mirando todas las posiciones , y usando el mismo criterio que teníamos para las repeticiones cruzadas izquierdas.
Implementación
La implementación del algoritmo de Main-Lorentz encuentra todas las repeticiones en forma de tuplas peculiares de tamaño cuatro: en tiempo . Si solo se quiere encontrar el número de repeticiones en un string, o solo se quiere encontrar la repetición más larga en un string, esta información es suficiente y el tiempo de ejecución seguirá siendo .
Nótese que si se quiere expandir estas tuplas para obtener la posición de inicio y de fin de cada repetición, entonces el tiempo de ejecución será (recordemos que puede haber repeticiones). En esta implementación lo haremos, y almacenaremos todas las repeticiones encontradas en un vector de pares de índices de inicio y fin.
vector<int> z_function(string const& s) {
int n = s.size();
vector<int> z(n);
for (int i = 1, l = 0, r = 0; i < n; i++) {
if (i <= r)
z[i] = min(r-i+1, z[i-l]);
while (i + z[i] < n && s[z[i]] == s[i+z[i]])
z[i]++;
if (i + z[i] - 1 > r) {
l = i;
r = i + z[i] - 1;
}
}
return z;
}
int get_z(vector<int> const& z, int i) {
if (0 <= i && i < (int)z.size())
return z[i];
else
return 0;
}
vector<pair<int, int>> repetitions;
void convert_to_repetitions(int shift, bool left, int cntr, int l, int k1, int k2) {
for (int l1 = max(1, l - k2); l1 <= min(l, k1); l1++) {
if (left && l1 == l) break;
int l2 = l - l1;
int pos = shift + (left ? cntr - l1 : cntr - l - l1 + 1);
repetitions.emplace_back(pos, pos + 2*l - 1);
}
}
void find_repetitions(string s, int shift = 0) {
int n = s.size();
if (n == 1)
return;
int nu = n / 2;
int nv = n - nu;
string u = s.substr(0, nu);
string v = s.substr(nu);
string ru(u.rbegin(), u.rend());
string rv(v.rbegin(), v.rend());
find_repetitions(u, shift);
find_repetitions(v, shift + nu);
vector<int> z1 = z_function(ru);
vector<int> z2 = z_function(v + '#' + u);
vector<int> z3 = z_function(ru + '#' + rv);
vector<int> z4 = z_function(v);
for (int cntr = 0; cntr < n; cntr++) {
int l, k1, k2;
if (cntr < nu) {
l = nu - cntr;
k1 = get_z(z1, nu - cntr);
k2 = get_z(z2, nv + 1 + cntr);
} else {
l = cntr - nu + 1;
k1 = get_z(z3, nu + 1 + nv - 1 - (cntr - nu));
k2 = get_z(z4, (cntr - nu) + 1);
}
if (k1 + k2 >= l)
convert_to_repetitions(shift, cntr < nu, cntr, l, k1, k2);
}
}