Skip to Content

Función prefijo. Algoritmo de Knuth–Morris–Pratt

Definición de la función prefijo

Se nos da un string ss de longitud nn. La función prefijo (prefix function) de este string se define como un arreglo π\pi de longitud nn, donde π[i]\pi[i] es la longitud del prefijo propio más largo de la subcadena s[0i]s[0 \dots i] que también es un sufijo de esa subcadena. Un prefijo propio de un string es un prefijo que no es igual al string mismo. Por definición, π[0]=0\pi[0] = 0.

Matemáticamente, la definición de la función prefijo se puede escribir así:

π[i]=maxk=0i{k:s[0k1]=s[i(k1)i]}\pi[i] = \max_ {k = 0 \dots i} {k : s[0 \dots k-1] = s[i-(k-1) \dots i] }

Por ejemplo, la función prefijo del string “abcabcd” es [0,0,0,1,2,3,0][0, 0, 0, 1, 2, 3, 0], y la función prefijo del string “aabaaab” es [0,1,0,1,2,2,3][0, 1, 0, 1, 2, 2, 3].

Algoritmo trivial

Un algoritmo que sigue exactamente la definición de la función prefijo es el siguiente:

vector<int> prefix_function(string s) { int n = (int)s.length(); vector<int> pi(n); for (int i = 0; i < n; i++) for (int k = 0; k <= i; k++) if (s.substr(0, k) == s.substr(i-k+1, k)) pi[i] = k; return pi; }

Es fácil ver que su complejidad es O(n3)O(n^3), así que hay margen de mejora.

Algoritmo eficiente

Este algoritmo fue propuesto por Knuth y Pratt y, de forma independiente, por Morris en 1977. Se usó como la función principal de un algoritmo de búsqueda de subcadenas.

Primera optimización

La primera observación importante es que los valores de la función prefijo solo pueden aumentar a lo sumo en uno.

En efecto, de lo contrario, si π[i+1]>π[i]+1\pi[i + 1] \gt \pi[i] + 1, podemos tomar este sufijo que termina en la posición i+1i + 1 con longitud π[i+1]\pi[i + 1] y quitarle el último carácter. Terminamos con un sufijo que termina en la posición ii con longitud π[i+1]1\pi[i + 1] - 1, que es mejor que π[i]\pi[i], es decir, obtenemos una contradicción.

La siguiente ilustración muestra esta contradicción. El sufijo propio más largo en la posición ii que también es un prefijo tiene longitud 22, y en la posición i+1i+1 tiene longitud 44. Por lo tanto el string s0 s1 s2 s3s_0 ~ s_1 ~ s_2 ~ s_3 es igual al string si2 si1 si si+1s_{i-2} ~ s_{i-1} ~ s_i ~ s_{i+1}, lo que significa que también los strings s0 s1 s2s_0 ~ s_1 ~ s_2 y si2 si1 sis_{i-2} ~ s_{i-1} ~ s_i son iguales; por lo tanto π[i]\pi[i] tiene que ser 33.

s0 s1π[i]=2 s2 s3π[i+1]=4  si2 si1 siπ[i]=2 si+1π[i+1]=4\underbrace{\overbrace{s_0 ~ s_1}^{\pi[i] = 2} ~ s_2 ~ s_3}{\pi[i+1] = 4} ~ \dots ~ \underbrace{s{i-2} ~ \overbrace{s_{i-1} ~ s_{i}}^{\pi[i] = 2} ~ s_{i+1}}_{\pi[i+1] = 4}

Así, al pasar a la siguiente posición, el valor de la función prefijo puede aumentar en uno, permanecer igual o disminuir en cierta cantidad. Este hecho ya nos permite reducir la complejidad del algoritmo a O(n2)O(n^2), porque en un paso la función prefijo puede crecer a lo sumo en uno. En total la función puede crecer a lo sumo nn pasos, y por lo tanto también solo puede disminuir un total de nn pasos. Esto significa que solo tenemos que realizar O(n)O(n) comparaciones de strings, y alcanzamos la complejidad O(n2)O(n^2).

Segunda optimización

Vayamos más lejos: queremos deshacernos de las comparaciones de strings. Para lograrlo, tenemos que usar toda la información calculada en los pasos anteriores.

Calculemos entonces el valor de la función prefijo π\pi para i+1i + 1. Si s[i+1]=s[π[i]]s[i+1] = s[\pi[i]], entonces podemos afirmar con certeza que π[i+1]=π[i]+1\pi[i+1] = \pi[i] + 1, ya que ya sabemos que el sufijo en la posición ii de longitud π[i]\pi[i] es igual al prefijo de longitud π[i]\pi[i]. Esto se ilustra de nuevo con un ejemplo.

s0 s1 s2π[i] s3s3=si+1π[i+1]=π[i]+1  si2 si1 siπ[i] si+1s3=si+1π[i+1]=π[i]+1\underbrace{\overbrace{s_0 ~ s_1 ~ s_2}^{\pi[i]} ~ \overbrace{s_3}^{s_3 = s_{i+1}}}{\pi[i+1] = \pi[i] + 1} ~ \dots ~ \underbrace{\overbrace{s{i-2} ~ s_{i-1} ~ s_{i}}^{\pi[i]} ~ \overbrace{s_{i+1}}^{s_3 = s_{i + 1}}}_{\pi[i+1] = \pi[i] + 1}

Si no es el caso, s[i+1]s[π[i]]s[i+1] \neq s[\pi[i]], entonces tenemos que probar un string más corto. Para acelerar las cosas, nos gustaría pasar de inmediato a la mayor longitud j<π[i]j \lt \pi[i] tal que la propiedad de prefijo se cumple en la posición ii, es decir, s[0j1]=s[ij+1i]s[0 \dots j-1] = s[i-j+1 \dots i]:

s0 s1j s2 s3π[i]  si3 si2 si1 sijπ[i] si+1\overbrace{\underbrace{s_0 ~ s_1}j ~ s_2 ~ s_3}^{\pi[i]} ~ \dots ~ \overbrace{s{i-3} ~ s_{i-2} ~ \underbrace{s_{i-1} ~ s_{i}}j}^{\pi[i]} ~ s{i+1}

En efecto, si encontramos tal longitud jj, entonces de nuevo solo necesitamos comparar los caracteres s[i+1]s[i+1] y s[j]s[j]. Si son iguales, podemos asignar π[i+1]=j+1\pi[i+1] = j + 1. En caso contrario, tendremos que encontrar el mayor valor menor que jj para el cual se cumple la propiedad de prefijo, y así sucesivamente. Puede ocurrir que esto continúe hasta j=0j = 0. Si entonces s[i+1]=s[0]s[i+1] = s[0], asignamos π[i+1]=1\pi[i+1] = 1, y π[i+1]=0\pi[i+1] = 0 en caso contrario.

Así que ya tenemos un esquema general del algoritmo. La única pregunta que queda es cómo encontrar de forma efectiva las longitudes para jj. Recapitulemos: para la longitud actual jj en la posición ii para la cual se cumple la propiedad de prefijo, es decir, s[0j1]=s[ij+1i]s[0 \dots j-1] = s[i-j+1 \dots i], queremos encontrar el mayor k<jk \lt j para el cual se cumple la propiedad de prefijo.

s0 s1k s2 s3j  si3 si2 si1 sikj si+1\overbrace{\underbrace{s_0 ~ s_1}k ~ s_2 ~ s_3}^j ~ \dots ~ \overbrace{s{i-3} ~ s_{i-2} ~ \underbrace{s_{i-1} ~ s_{i}}k}^j ~s{i+1}

La ilustración muestra que este tiene que ser el valor de π[j1]\pi[j-1], que ya calculamos antes.

Algoritmo final

Por fin podemos construir un algoritmo que no realiza ninguna comparación de strings y solo realiza O(n)O(n) acciones.

Este es el procedimiento final:

  • Calculamos los valores de prefijo π[i]\pi[i] en un bucle iterando desde i=1i = 1 hasta i=n1i = n-1 (a π[0]\pi[0] simplemente se le asigna 00).
  • Para calcular el valor actual π[i]\pi[i] fijamos la variable jj que denota la longitud del mejor sufijo para i1i-1. Inicialmente j=π[i1]j = \pi[i-1].
  • Comprobamos si el sufijo de longitud j+1j+1 también es un prefijo comparando s[j]s[j] y s[i]s[i]. Si son iguales, asignamos π[i]=j+1\pi[i] = j + 1; en caso contrario reducimos jj a π[j1]\pi[j-1] y repetimos este paso.
  • Si llegamos a la longitud j=0j = 0 y todavía no hay coincidencia, entonces asignamos π[i]=0\pi[i] = 0 y pasamos al siguiente índice i+1i + 1.

Implementación

La implementación termina siendo sorprendentemente corta y expresiva.

vector<int> prefix_function(string s) { int n = (int)s.length(); vector<int> pi(n); for (int i = 1; i < n; i++) { int j = pi[i-1]; while (j > 0 && s[i] != s[j]) j = pi[j-1]; if (s[i] == s[j]) j++; pi[i] = j; } return pi; }

Este es un algoritmo online, es decir, procesa los datos a medida que llegan: por ejemplo, se pueden leer los caracteres del string uno por uno y procesarlos de inmediato, hallando el valor de la función prefijo para cada siguiente carácter. El algoritmo aún requiere almacenar el string mismo y los valores de la función prefijo calculados previamente, pero si conocemos de antemano el valor máximo MM que puede tomar la función prefijo sobre el string, podemos almacenar solo los primeros M+1M+1 caracteres del string y la misma cantidad de valores de la función prefijo.

Aplicaciones

Búsqueda de una subcadena en un string. El algoritmo de Knuth-Morris-Pratt

Esta tarea es la aplicación clásica de la función prefijo.

Dados un texto tt y un string ss, queremos encontrar y mostrar las posiciones de todas las ocurrencias del string ss en el texto tt.

Por comodidad denotamos con nn la longitud del string ss y con mm la longitud del texto tt.

Generamos el string s+#+ts + # + t, donde ## es un separador que no aparece ni en ss ni en tt. Calculemos la función prefijo de este string. Ahora pensemos en el significado de los valores de la función prefijo, excepto las primeras n+1n + 1 entradas (que pertenecen al string ss y al separador). Por definición, el valor π[i]\pi[i] muestra la mayor longitud de una subcadena que termina en la posición ii y que coincide con el prefijo. Pero en nuestro caso esto no es más que el bloque más grande que coincide con ss y termina en la posición ii. Esta longitud no puede ser mayor que nn debido al separador. Pero si se alcanza la igualdad π[i]=n\pi[i] = n, entonces significa que el string ss aparece por completo en esta posición, es decir, termina en la posición ii. Solo no hay que olvidar que las posiciones están indexadas en el string s+#+ts + # + t.

Así, si en alguna posición ii tenemos π[i]=n\pi[i] = n, entonces en la posición i(n+1)n+1=i2ni - (n + 1) - n + 1 = i - 2n del string tt aparece el string ss.

Como ya se mencionó en la descripción del cálculo de la función prefijo, si sabemos que los valores de prefijo nunca superan cierto valor, entonces no necesitamos almacenar el string entero ni la función entera, sino solo su comienzo. En nuestro caso esto significa que solo necesitamos almacenar el string s+#s + # y los valores de la función prefijo para él. Podemos leer de a un carácter del string tt y calcular el valor actual de la función prefijo.

Así, el algoritmo de Knuth-Morris-Pratt resuelve el problema en tiempo O(n+m)O(n + m) y memoria O(n)O(n).

Contar el número de ocurrencias de cada prefijo

Aquí discutimos dos problemas a la vez. Dado un string ss de longitud nn. En la primera variante del problema queremos contar el número de apariciones de cada prefijo s[0i]s[0 \dots i] en el mismo string. En la segunda variante se da otro string tt y queremos contar el número de apariciones de cada prefijo s[0i]s[0 \dots i] en tt.

Primero resolvemos el primer problema. Consideremos el valor de la función prefijo π[i]\pi[i] en una posición ii. Por definición, significa que el prefijo de longitud π[i]\pi[i] del string ss ocurre y termina en la posición ii, y no hay un prefijo más largo que cumpla esta definición. Al mismo tiempo, prefijos más cortos pueden terminar en esta posición. No es difícil ver que tenemos la misma pregunta que ya respondimos cuando calculamos la función prefijo misma: Dado un prefijo de longitud jj que es un sufijo que termina en la posición ii, ¿cuál es el siguiente prefijo más pequeño <j\lt j que también es un sufijo que termina en la posición ii? Así, en la posición ii termina el prefijo de longitud π[i]\pi[i], el prefijo de longitud π[π[i]1]\pi[\pi[i] - 1], el prefijo π[π[π[i]1]1]\pi[\pi[\pi[i] - 1] - 1], y así sucesivamente, hasta que el índice se vuelve cero. Así podemos calcular la respuesta de la siguiente forma.

vector<int> ans(n + 1); for (int i = 0; i < n; i++) ans[pi[i]]++; for (int i = n-1; i > 0; i--) ans[pi[i-1]] += ans[i]; for (int i = 0; i <= n; i++) ans[i]++;

Aquí, para cada valor de la función prefijo primero contamos cuántas veces ocurre en el arreglo π\pi, y luego calculamos las respuestas finales: si sabemos que el prefijo de longitud ii aparece exactamente ans[i]\text{ans}[i] veces, entonces este número hay que sumarlo al número de ocurrencias de su sufijo más largo que también es un prefijo. Al final hay que sumar 11 a cada resultado, ya que también hay que contar los prefijos originales.

Ahora consideremos el segundo problema. Aplicamos el truco de Knuth-Morris-Pratt: creamos el string s+#+ts + # + t y calculamos su función prefijo. Las únicas diferencias con la primera tarea son que solo nos interesan los valores de prefijo que se relacionan con el string tt, es decir, π[i]\pi[i] para in+1i \ge n + 1. Con esos valores podemos realizar exactamente los mismos cálculos que en la primera tarea.

El número de subcadenas distintas en un string

Dado un string ss de longitud nn. Queremos calcular el número de subcadenas distintas que aparecen en él.

Resolveremos este problema de forma iterativa. A saber, aprenderemos, conociendo el número actual de subcadenas distintas, cómo recalcular este conteo al agregar un carácter al final.

Sea kk el número actual de subcadenas distintas en ss, y agregamos el carácter cc al final de ss. Obviamente aparecerán algunas subcadenas nuevas que terminan en cc. Queremos contar estas subcadenas nuevas que no aparecieron antes.

Tomamos el string t=s+ct = s + c y lo invertimos. Ahora la tarea se transforma en calcular cuántos prefijos hay que no aparecen en ningún otro lugar. Si calculamos el valor máximo de la función prefijo πmax\pi_{\text{max}} del string invertido tt, entonces el prefijo más largo que aparece en ss tiene longitud πmax\pi_{\text{max}}. Claramente, también aparecen todos los prefijos de menor longitud.

Por lo tanto, el número de subcadenas nuevas que aparecen cuando agregamos un nuevo carácter cc es s+1πmax|s| + 1 - \pi_{\text{max}}.

Así, por cada carácter agregado podemos calcular el número de subcadenas nuevas en tiempo O(n)O(n), lo que da una complejidad temporal de O(n2)O(n^2) en total.

Vale la pena notar que también podemos calcular el número de subcadenas distintas agregando los caracteres al principio, o eliminando caracteres del principio o del final.

Comprimir un string

Dado un string ss de longitud nn. Queremos encontrar la representación “comprimida” más corta del string, es decir, queremos encontrar un string tt de menor longitud tal que ss se pueda representar como concatenación de una o más copias de tt.

Está claro que solo necesitamos encontrar la longitud de tt. Conociendo la longitud, la respuesta al problema será el prefijo de ss con esa longitud.

Calculemos la función prefijo de ss. Usando su último valor definimos el valor k=nπ[n1]k = n - \pi[n - 1]. Mostraremos que si kk divide a nn, entonces kk será la respuesta; en caso contrario no hay una compresión efectiva y la respuesta es nn.

Sea nn divisible por kk. Entonces el string se puede particionar en bloques de longitud kk. Por definición de la función prefijo, el prefijo de longitud nkn - k será igual a su sufijo. Pero esto significa que el último bloque es igual al bloque anterior. Y el bloque anterior tiene que ser igual al bloque que lo precede. Y así sucesivamente. Como resultado, resulta que todos los bloques son iguales, por lo tanto podemos comprimir el string ss a longitud kk.

Por supuesto, todavía hay que mostrar que esto es realmente el óptimo. En efecto, si hubiera una compresión menor que kk, entonces la función prefijo al final sería mayor que nkn - k. Por lo tanto kk es realmente la respuesta.

Ahora supongamos que nn no es divisible por kk. Mostramos que esto implica que la longitud de la respuesta es nn. Lo demostramos por contradicción. Suponiendo que existe una respuesta, y que la compresión tiene longitud pp (pp divide a nn). Entonces el último valor de la función prefijo tiene que ser mayor que npn - p, es decir, el sufijo cubrirá parcialmente el primer bloque. Ahora consideremos el segundo bloque del string. Como el prefijo es igual al sufijo, y tanto el prefijo como el sufijo cubren este bloque y su desplazamiento relativo kk no divide la longitud del bloque pp (de lo contrario kk dividiría a nn), entonces todos los caracteres del bloque tienen que ser idénticos. Pero entonces el string consiste en un solo carácter repetido una y otra vez, por lo tanto podemos comprimirlo a un string de tamaño 11, lo que da k=1k = 1, y kk divide a nn. Contradicción.

s0 s1 s2 s3p s4 s5 s6 s7p\overbrace{s_0 ~ s_1 ~ s_2 ~ s_3}^p ~ \overbrace{s_4 ~ s_5 ~ s_6 ~ s_7}^p

s0 s1 s2 s3 s4 s5 s6p s7π[7]=5s_0 ~ s_1 ~ s_2 ~ \underbrace{\overbrace{s_3 ~ s_4 ~ s_5 ~ s_6}^p ~ s_7}_{\pi[7] = 5}

s4=s3, s5=s4, s6=s5, s7=s6  s0=s1=s2=s3s_4 = s_3, ~ s_5 = s_4, ~ s_6 = s_5, ~ s_7 = s_6 ~ \Rightarrow ~ s_0 = s_1 = s_2 = s_3

Construir un autómata según la función prefijo

Volvamos a la concatenación de los dos strings a través de un separador, es decir, para los strings ss y tt calculamos la función prefijo del string s+#+ts + # + t. Obviamente, como ## es un separador, el valor de la función prefijo nunca superará s|s|. Se sigue que basta con almacenar solo el string s+#s + # y los valores de la función prefijo para él, y podemos calcular la función prefijo para todo carácter posterior sobre la marcha:

s0 s1  sn1 #need to store t0 t1  tm1do not need to store\underbrace{s_0 ~ s_1 ~ \dots ~ s_{n-1} ~ #}{\text{need to store}} ~ \underbrace{t_0 ~ t_1 ~ \dots ~ t{m-1}}_{\text{do not need to store}}

En efecto, en tal situación, conocer el siguiente carácter ctc \in t y el valor de la función prefijo de la posición anterior es información suficiente para calcular el siguiente valor de la función prefijo, sin usar ningún carácter anterior del string tt ni el valor de la función prefijo en ellos.

En otras palabras, podemos construir un autómata (una máquina de estados finitos): el estado en él es el valor actual de la función prefijo, y la transición de un estado a otro se realizará mediante el siguiente carácter.

Así, incluso sin tener el string tt, podemos construir una tabla de transiciones (oldπ,c)newπ(\text{old}\pi, c) \rightarrow \text{new}\pi usando el mismo algoritmo que para calcular la tabla de transiciones:

void compute_automaton(string s, vector<vector<int>>& aut) { s += '#'; int n = s.size(); vector<int> pi = prefix_function(s); aut.assign(n, vector<int>(26)); for (int i = 0; i < n; i++) { for (int c = 0; c < 26; c++) { int j = i; while (j > 0 && 'a' + c != s[j]) j = pi[j-1]; if ('a' + c == s[j]) j++; aut[i][c] = j; } } }

Sin embargo, en esta forma el algoritmo corre en tiempo O(n226)O(n^2 26) para las letras minúsculas del alfabeto. Nótese que podemos aplicar programación dinámica y usar las partes de la tabla ya calculadas. Cada vez que vamos del valor jj al valor π[j1]\pi[j-1], en realidad queremos decir que la transición (j,c)(j, c) lleva al mismo estado que la transición (π[j1],c)(\pi[j-1], c), y esta respuesta ya está calculada con precisión.

void compute_automaton(string s, vector<vector<int>>& aut) { s += '#'; int n = s.size(); vector<int> pi = prefix_function(s); aut.assign(n, vector<int>(26)); for (int i = 0; i < n; i++) { for (int c = 0; c < 26; c++) { if (i > 0 && 'a' + c != s[i]) aut[i][c] = aut[pi[i-1]][c]; else aut[i][c] = i + ('a' + c == s[i]); } } }

Como resultado construimos el autómata en tiempo O(26n)O(26 n).

¿Cuándo es útil un autómata así? Para empezar, recordemos que usamos la función prefijo del string s+#+ts + # + t y sus valores principalmente para un solo propósito: encontrar todas las ocurrencias del string ss en el string tt.

Por lo tanto, el beneficio más obvio de este autómata es la aceleración del cálculo de la función prefijo para el string s+#+ts + # + t. Al construir el autómata para s+#s + #, ya no necesitamos almacenar el string ss ni los valores de la función prefijo en él. Todas las transiciones ya están calculadas en la tabla.

Pero hay una segunda aplicación, menos obvia. Podemos usar el autómata cuando el string tt es un string gigantesco construido usando algunas reglas. Esto puede ser, por ejemplo, los strings de Gray, o un string formado por una combinación recursiva de varios strings cortos de la entrada.

Para completar resolveremos un problema de este tipo: dados un número k105k \le 10^5 y un string ss de longitud 105\le 10^5. Tenemos que calcular el número de ocurrencias de ss en el kk-ésimo string de Gray. Recordemos que los strings de Gray se definen de la siguiente forma:

g1=“a”g2=“aba”g3=“abacaba”g4=“abacabadabacaba”\begin{align} g_1 &amp;= \text{&quot;a&quot;}\ g_2 &amp;= \text{&quot;aba&quot;}\ g_3 &amp;= \text{&quot;abacaba&quot;}\ g_4 &amp;= \text{&quot;abacabadabacaba&quot;} \end{align}

En tales casos, incluso construir el string tt será imposible, por su longitud astronómica. El kk-ésimo string de Gray tiene 2k12^k-1 caracteres de longitud. Sin embargo, podemos calcular el valor de la función prefijo al final del string de forma efectiva, conociendo solo el valor de la función prefijo al inicio.

Además del autómata mismo, también calculamos valores G[i][j]G[i][j]: el valor del autómata después de procesar el string gig_i partiendo del estado jj. Y adicionalmente calculamos valores K[i][j]K[i][j]: el número de ocurrencias de ss en gig_i durante el procesamiento de gig_i partiendo del estado jj. En realidad K[i][j]K[i][j] es el número de veces que la función prefijo tomó el valor s|s| al realizar las operaciones. La respuesta al problema será entonces K[k][0]K[k][0].

¿Cómo podemos calcular estos valores? Primero, los valores básicos son G[0][j]=jG[0][j] = j y K[0][j]=0K[0][j] = 0. Y todos los valores posteriores se pueden calcular a partir de los valores anteriores y usando el autómata. Para calcular el valor para algún ii recordamos que el string gig_i consiste en gi1g_{i-1}, el ii-ésimo carácter del alfabeto, y gi1g_{i-1}. Así el autómata irá al estado:

mid=aut[G[i1][j]][i]\text{mid} = \text{aut}[G[i-1][j]][i]

G[i][j]=G[i1][mid]G[i][j] = G[i-1][\text{mid}]

Los valores de K[i][j]K[i][j] también se pueden contar fácilmente.

K[i][j]=K[i1][j]+(mid==s)+K[i1][mid]K[i][j] = K[i-1][j] + (\text{mid} == |s|) + K[i-1][\text{mid}]

Así podemos resolver el problema para strings de Gray, y de forma similar también una enorme cantidad de otros problemas parecidos. Por ejemplo, el mismo método también resuelve el siguiente problema: se nos da un string ss y algunos patrones tit_i, cada uno de los cuales se especifica así: es un string de caracteres ordinarios, y puede haber algunas inserciones recursivas de los strings anteriores de la forma tkcntt_k^{\text{cnt}}, lo que significa que en ese lugar hay que insertar el string tkt_k cnt\text{cnt} veces. Un ejemplo de tales patrones:

t1=“abdeca”t2=“abc”+t130+“abd”t3=t250+t1100t4=t210+t3100\begin{align} t_1 &amp;= \text{&quot;abdeca&quot;}\ t_2 &amp;= \text{&quot;abc&quot;} + t_1^{30} + \text{&quot;abd&quot;}\ t_3 &amp;= t_2^{50} + t_1^{100}\ t_4 &amp;= t_2^{10} + t_3^{100} \end{align}

Las sustituciones recursivas inflan el string, de modo que sus longitudes pueden alcanzar el orden de 100100100^{100}.

Tenemos que encontrar el número de veces que el string ss aparece en cada uno de los strings.

El problema se puede resolver de la misma forma construyendo el autómata de la función prefijo, y luego calculamos las transiciones para cada patrón usando los resultados anteriores.

Problemas de práctica