Skip to Content

Encontrar repeticiones

Se da un string ss de longitud nn.

Una repetición es dos ocurrencias de un string seguidas. En otras palabras, una repetición se puede describir por un par de índices i<ji < j tales que la subcadena s[ij]s[i \dots j] consiste en dos strings idénticos escritos uno después del otro.

El desafío es encontrar todas las repeticiones en un string dado ss. 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:

acababaeeacababaee

El string contiene las siguientes tres repeticiones:

  • s[25]=ababs[2 \dots 5] = abab
  • s[36]=babas[3 \dots 6] = baba
  • s[78]=ees[7 \dots 8] = ee

Otro ejemplo:

abaabaabaaba

Aquí hay solo dos repeticiones

  • s[05]=abaabas[0 \dots 5] = abaaba
  • s[23]=aas[2 \dots 3] = aa

Número de repeticiones

En general puede haber hasta O(n2)O(n^2) repeticiones en un string de longitud nn. Un ejemplo obvio es un string que consiste en nn 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 O(nlogn)O(n \log n), 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 O(nlogn)O(n \log n).

  • Si codificamos las repeticiones con tuplas de números (llamadas triples de Crochemore) (i, p, r)(i,~ p,~ r) (donde ii es la posición del comienzo, pp la longitud de la subcadena que se repite, y rr el número de repeticiones), entonces todas las repeticiones se pueden describir con O(nlogn)O(n \log n) de tales triples.

  • Los strings de Fibonacci, definidos como

    t0=a,t1=b,ti=ti1+ti2,t0amp;=a,t1amp;=b,tiamp;=ti1+ti2,\begin{align} t_0 &amp;= a, \\ t_1 &amp;= b, \\ t_i &amp;= t_{i-1} + t_{i-2}, \end{align}

    son “fuertemente” periódicos. El número de repeticiones en el string de Fibonacci fif_i, incluso comprimido con triples de Crochemore, es O(fnlogfn)O(f_n \log f_n). El número de repeticiones primitivas también es O(fnlogfn)O(f_n \log f_n).

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 O(nlogn)O(n \log n) si podemos computar las repeticiones cruzadas en tiempo O(n)O(n).

Búsqueda de repeticiones cruzadas

Así, queremos encontrar todas las repeticiones que empiezan en la primera mitad del string, llamémosla uu, y terminan en la segunda mitad, llamémosla vv:

s=u+vs = u + v

Sus longitudes son aproximadamente iguales a la longitud de ss 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 s[ij]s[i \dots j], entonces el carácter del medio es (i+j+1)/2(i + j + 1) / 2.

Llamamos a una repetición izquierda o derecha según en qué string se ubica este carácter: en el string uu o en el string vv. En otras palabras, un string se llama izquierdo si la mayoría de él yace en uu; 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 2l2l (es decir, cada mitad de la repetición tiene longitud ll). Consideremos el primer carácter de la repetición que cae en el string vv (está en la posición u|u| del string ss). Coincide con el carácter ll posiciones antes, denotemos esta posición cntrcntr.

Fijaremos esta posición cntrcntr, y buscaremos todas las repeticiones en esta posición cntrcntr.

Por ejemplo:

c acntr c  a d ac ~ \underset{cntr}{a} ~ c ~ | ~ a ~ d ~ a

Las líneas verticales dividen las dos mitades. Aquí fijamos la posición cntr=1cntr = 1, y en esta posición encontramos la repetición cacacaca.

Está claro que si fijamos la posición cntrcntr, simultáneamente fijamos la longitud de las repeticiones posibles: l=ucntrl = |u| - cntr. Una vez que sepamos cómo encontrar estas repeticiones, iteraremos sobre todos los valores posibles de cntrcntr de 00 a u1|u|-1, y encontraremos todas las repeticiones cruzadas izquierdas de longitud l=u, u1, ,1l = |u|,~ |u|-1,~ \dots, 1.

Criterio para repeticiones cruzadas izquierdas

Ahora, ¿cómo podemos encontrar todas esas repeticiones para un cntrcntr 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 abcabcabcabc:

al1 bcntr cl2 al1  b cl2\overbrace{a}^{l_1} ~ \overbrace{\underset{cntr}{b} ~ c}^{l_2} ~ \overbrace{a}^{l_1} ~ | ~ \overbrace{b ~ c}^{l_2}

Aquí denotamos las longitudes de las dos piezas de la repetición con l1l_1 y l2l_2: l1l_1 es la longitud de la repetición hasta la posición cntr1cntr-1, y l2l_2 es la longitud de la repetición desde cntrcntr hasta el final de la mitad de la repetición. Tenemos 2l=l1+l2+l1+l22l = l_1 + l_2 + l_1 + l_2 como la longitud total de la repetición.

Generemos condiciones necesarias y suficientes para tal repetición en la posición cntrcntr de longitud 2l=2(l1+l2)=2(ucntr)2l = 2(l_1 + l_2) = 2(|u| - cntr):

  • Sea k1k_1 el mayor número tal que los primeros k1k_1 caracteres antes de la posición cntrcntr coinciden con los últimos k1k_1 caracteres del string uu:

u[cntrk1cntr1]=u[uk1u1] u[cntr - k_1 \dots cntr - 1] = u[|u| - k_1 \dots |u| - 1]

  • Sea k2k_2 el mayor número tal que los k2k_2 caracteres que empiezan en la posición cntrcntr coinciden con los primeros k2k_2 caracteres del string vv:

u[cntrcntr+k21]=v[0k21]
u[cntr \dots cntr + k_2 - 1] = v[0 \dots k_2 - 1]

  • Entonces tenemos una repetición exactamente para cualquier par (l1, l2)(l_1,~ l_2) con

l1k1,l2k2. l1amp;k1,l2amp;k2.\begin{align} l_1 &amp;\le k_1, \\ l_2 &amp;\le k_2. \\ \end{align}

Para resumir:

  • Fijamos una posición específica cntrcntr.
  • Todas las repeticiones que encontraremos ahora tienen longitud 2l=2(ucntr)2l = 2(|u| - cntr). Puede haber múltiples de tales repeticiones; dependen de las longitudes l1l_1 y l2=ll1l_2 = l - l_1.
  • Encontramos k1k_1 y k2k_2 como se describió arriba.
  • Entonces todas las repeticiones adecuadas son aquellas para las que las longitudes de las piezas l1l_1 y l2l_2 satisfacen las condiciones:

l1+l2=l=ucntrl1k1,l2k2. l1+l2amp;=l=ucntrl1amp;k1,l2amp;k2.\begin{align} l_1 + l_2 &amp;= l = |u| - cntr \\ l_1 &amp;\le k_1, \\ l_2 &amp;\le k_2. \\ \end{align}

Por lo tanto la única parte que queda es cómo podemos computar los valores k1k_1 y k2k_2 rápidamente para cada posición cntrcntr. Afortunadamente podemos computarlos en O(1)O(1) usando la función Z:

  • Podemos encontrar el valor k1k_1 para cada posición calculando la función Z del string u\overline{u} (es decir, el string uu invertido). Entonces el valor k1k_1 para un cntrcntr particular será igual al valor correspondiente del arreglo de la función Z.
  • Para precomputar todos los valores k2k_2, calculamos la función Z del string v+#+uv + # + u (es decir, el string uu concatenado con el carácter separador ## y el string vv). De nuevo solo necesitamos consultar el valor correspondiente en la función Z para obtener el valor k2k_2.

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 cntrcntr como el carácter correspondiente al último carácter del string uu.

Entonces la longitud k1k_1 se definirá como el mayor número de caracteres antes de la posición cntrcntr (inclusive) que coinciden con los últimos caracteres del string uu. Y la longitud k2k_2 se definirá como el mayor número de caracteres que empiezan en cntr+1cntr + 1 que coinciden con los caracteres del string vv.

Así podemos encontrar los valores k1k_1 y k2k_2 computando la función Z de los strings u+#+v\overline{u} + # + \overline{v} y vv.

Después de eso podemos encontrar las repeticiones mirando todas las posiciones cntrcntr, 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: (cntr, l, k1, k2)(cntr,~ l,~ k_1,~ k_2) en tiempo O(nlogn)O(n \log n). 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 O(nlogn)O(n \log n).

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á O(n2)O(n^2) (recordemos que puede haber O(n2)O(n^2) 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); } }