Autómata de sufijos
Un autómata de sufijos (suffix automaton) es una estructura de datos poderosa que permite resolver muchos problemas relacionados con strings.
Por ejemplo, se puede buscar todas las ocurrencias de un string en otro, o contar la cantidad de subcadenas distintas de un string dado. Ambas tareas se pueden resolver en tiempo lineal con ayuda de un autómata de sufijos.
Intuitivamente, un autómata de sufijos se puede entender como una forma comprimida de todas las subcadenas de un string dado. Un hecho impresionante es que el autómata de sufijos contiene toda esta información en una forma altamente comprimida. Para un string de longitud solo requiere memoria . Además, también se puede construir en tiempo (si consideramos el tamaño del alfabeto como una constante); en caso contrario tanto la memoria como la complejidad temporal serán .
La linealidad del tamaño del autómata de sufijos fue descubierta primero en 1983 por Blumer et al., y en 1985 se presentaron los primeros algoritmos lineales de construcción por Crochemore y Blumer.
Definición de un autómata de sufijos
Un autómata de sufijos para un string dado es un DFA mínimo (autómata finito determinista / máquina de estados finitos determinista) que acepta todos los sufijos del string .
En otras palabras:
- Un autómata de sufijos es un grafo acíclico orientado. Los vértices se llaman estados, y las aristas se llaman transiciones entre estados.
- Uno de los estados es el estado inicial, y debe ser la fuente del grafo (todos los demás estados son alcanzables desde ).
- Cada transición está etiquetada con algún carácter. Todas las transiciones que salen de un estado deben tener etiquetas distintas.
- Uno o varios estados se marcan como estados terminales. Si empezamos desde el estado inicial y nos movemos a lo largo de transiciones hasta un estado terminal, entonces las etiquetas de las transiciones recorridas deben deletrear uno de los sufijos del string . Cada uno de los sufijos de debe poder deletrearse usando un camino de a un estado terminal.
- El autómata de sufijos contiene el mínimo número de vértices entre todos los autómatas que satisfacen las condiciones descritas arriba.
Propiedad de las subcadenas
La propiedad más simple e importante de un autómata de sufijos es que contiene información sobre todas las subcadenas del string . Cualquier camino que empieza en el estado inicial , si escribimos las etiquetas de las transiciones, forma una subcadena de . Y recíprocamente, cada subcadena de corresponde a un cierto camino que empieza en .
Para simplificar las explicaciones, diremos que la subcadena corresponde a ese camino (empezando en y las etiquetas deletrean la subcadena). Y recíprocamente decimos que cualquier camino corresponde al string deletreado por sus etiquetas.
Uno o varios caminos pueden llevar a un estado. Así, diremos que un estado corresponde al conjunto de strings que corresponden a estos caminos.
Ejemplos de autómatas de sufijos construidos
Aquí mostraremos algunos ejemplos de autómatas de sufijos para varios strings simples.
Denotaremos el estado inicial con azul y los estados terminales con verde.
Para el string :

Para el string :

Para el string :

Para el string :

Para el string :

Para el string :

Para el string :

Construcción en tiempo lineal
Antes de describir el algoritmo para construir un autómata de sufijos en tiempo lineal, necesitamos introducir varios conceptos nuevos y demostraciones simples, que serán muy importantes para entender la construcción.
Posiciones finales {data-toc-label=“Posiciones finales”}
Consideremos cualquier subcadena no vacía del string . Denotaremos con el conjunto de todas las posiciones en el string en las que terminan las ocurrencias de . Por ejemplo, tenemos para el string .
Llamaremos a dos subcadenas y -equivalentes si sus conjuntos de finales coinciden: . Así, todas las subcadenas no vacías del string se pueden descomponer en varias clases de equivalencia según sus conjuntos .
Resulta que en una máquina de sufijos las subcadenas -equivalentes corresponden al mismo estado. En otras palabras, el número de estados en un autómata de sufijos es igual al número de clases de equivalencia entre todas las subcadenas, más el estado inicial. Cada estado de un autómata de sufijos corresponde a una o más subcadenas que tienen el mismo valor .
Más adelante describiremos el algoritmo de construcción usando esta hipótesis. Luego veremos que se cumplen todas las propiedades requeridas de un autómata de sufijos, excepto la minimalidad. Y la minimalidad se sigue del teorema de Nerode (que no se demostrará en este artículo).
Podemos hacer algunas observaciones importantes respecto de los valores :
Lema 1: Dos subcadenas no vacías y (con ) son -equivalentes si y solo si el string ocurre en solo en forma de sufijo de .
La demostración es obvia. Si y tienen los mismos valores , entonces es un sufijo de y aparece solo en forma de sufijo de en . Y si es un sufijo de y aparece solo en forma de sufijo en , entonces los valores son iguales por definición.
Lema 2: Consideremos dos subcadenas no vacías y (con ). Entonces sus conjuntos o bien no se intersectan en absoluto, o es un subconjunto de . Y depende de si es un sufijo de o no.
Demostración: Si los conjuntos y tienen al menos un elemento en común, entonces los strings y ambos terminan en esa posición, es decir, es un sufijo de . Pero entonces en cada ocurrencia de también aparece la subcadena , lo que significa que es un subconjunto de .
Lema 3: Consideremos una clase de -equivalencia. Ordenemos todas las subcadenas de esta clase por longitud decreciente. Entonces en la secuencia resultante cada subcadena será una más corta que la anterior, y al mismo tiempo será un sufijo de la anterior. En otras palabras, en una misma clase de equivalencia, las subcadenas más cortas son de hecho sufijos de las subcadenas más largas, y toman todas las longitudes posibles en un cierto intervalo .
Demostración: Fijemos alguna clase de -equivalencia. Si solo contiene un string, entonces el lema es obviamente cierto. Ahora digamos que el número de strings en la clase es mayor que uno.
Según el Lema 1, dos strings -equivalentes distintos están siempre de tal forma que el más corto es un sufijo propio del más largo. En consecuencia, no puede haber dos strings de la misma longitud en la clase de equivalencia.
Denotemos por el más largo, y por el string más corto de la clase de equivalencia. Según el Lema 1, el string es un sufijo propio del string . Consideremos ahora cualquier sufijo de con una longitud en el intervalo . Es fácil ver que este sufijo también está contenido en la misma clase de equivalencia. Porque este sufijo solo puede aparecer en forma de sufijo de en el string (ya que también el sufijo más corto ocurre en solo en forma de sufijo de ). En consecuencia, según el Lema 1, este sufijo es -equivalente al string .
Enlaces de sufijo {data-toc-label=“Enlaces de sufijo”}
Consideremos algún estado en el autómata. Como sabemos, el estado corresponde a la clase de strings con los mismos valores . Y si denotamos por el más largo de estos strings, entonces todos los demás strings son sufijos de .
También sabemos que los primeros sufijos de un string (si consideramos sufijos en orden descendente de su longitud) están todos contenidos en esta clase de equivalencia, y todos los demás sufijos (al menos uno más: el sufijo vacío) están en algunas otras clases. Denotamos por el mayor de esos sufijos, y hacemos un enlace de sufijo hacia él.
En otras palabras, un enlace de sufijo (suffix link) lleva al estado que corresponde al sufijo más largo de que está en otra clase de -equivalencia.
Aquí asumimos que el estado inicial corresponde a su propia clase de equivalencia (que contiene solo el string vacío), y por conveniencia ponemos .
Lema 4: Los enlaces de sufijo forman un árbol con raíz .
Demostración: Consideremos un estado arbitrario . Un enlace de sufijo lleva a un estado que corresponde a strings con longitud estrictamente menor (esto se sigue de la definición de los enlaces de sufijo y del Lema 3). Por lo tanto, al movernos a lo largo de los enlaces de sufijo, tarde o temprano llegaremos al estado inicial , que corresponde al string vacío.
Lema 5: Si construimos un árbol usando los conjuntos (por la regla de que el conjunto de un nodo padre contiene los conjuntos de todos los hijos como subconjuntos), entonces la estructura coincidirá con el árbol de enlaces de sufijo.
Demostración: El hecho de que podemos construir un árbol usando los conjuntos se sigue directamente del Lema 2 (que cualesquiera dos conjuntos o bien no se intersectan o uno está contenido en el otro).
Consideremos ahora un estado arbitrario , y su enlace de sufijo . De la definición del enlace de sufijo y del Lema 2 se sigue que
lo que junto con el lema anterior demuestra la afirmación: el árbol de enlaces de sufijo es esencialmente un árbol de conjuntos .
Aquí hay un ejemplo de un árbol de enlaces de sufijo en el autómata de sufijos construido para el string . Los nodos están etiquetados con la subcadena más larga de la clase de equivalencia correspondiente.

Recapitulación
Antes de proceder al algoritmo en sí, recapitulamos el conocimiento acumulado e introducimos unas notaciones auxiliares.
- Las subcadenas del string se pueden descomponer en clases de equivalencia según sus posiciones finales .
- El autómata de sufijos consiste del estado inicial , así como de un estado por cada clase de -equivalencia.
- Para cada estado coinciden una o varias subcadenas. Denotamos por el string más largo de ese tipo, y por su longitud. Denotamos por la subcadena más corta de ese tipo, y su longitud con . Entonces todos los strings correspondientes a este estado son sufijos distintos del string y tienen todas las longitudes posibles en el intervalo .
- Para cada estado se define un enlace de sufijo como un enlace que lleva a un estado que corresponde al sufijo del string de longitud . Los enlaces de sufijo forman un árbol con raíz en , y al mismo tiempo este árbol forma una relación de inclusión entre los conjuntos .
- Podemos expresar para usando el enlace de sufijo como:
- Si empezamos desde un estado arbitrario y seguimos los enlaces de sufijo, entonces tarde o temprano llegaremos al estado inicial . En este caso obtenemos una secuencia de intervalos disjuntos , que en unión forma el intervalo continuo .
Algoritmo
Ahora podemos proceder al algoritmo en sí. El algoritmo será online, es decir, agregaremos los caracteres del string uno por uno, y modificaremos el autómata en consecuencia en cada paso.
Para alcanzar consumo de memoria lineal, solo guardaremos los valores , y una lista de transiciones en cada estado. No etiquetaremos estados terminales (pero más adelante mostraremos cómo disponer estas etiquetas después de construir el autómata de sufijos).
Inicialmente el autómata consiste de un solo estado , que será el índice (los estados restantes recibirán los índices ). Le asignamos y por conveniencia ( será un estado ficticio, inexistente).
Ahora toda la tarea se reduce a implementar el proceso de agregar un carácter al final del string actual. Describamos este proceso:
-
Sea el estado correspondiente a todo el string antes de agregar el carácter . (Inicialmente ponemos , y cambiaremos en el último paso del algoritmo en consecuencia.)
-
Creamos un estado nuevo , y le asignamos . El valor no se conoce en ese momento.
-
Ahora hacemos el siguiente procedimiento: Empezamos en el estado . Mientras no haya una transición por la letra , agregaremos una transición al estado , y seguiremos el enlace de sufijo. Si en algún punto ya existe una transición por la letra , entonces nos detendremos y denotaremos este estado con .
-
Si no encontramos tal estado , entonces llegamos al estado ficticio , entonces podemos simplemente asignar y salir.
-
Supongamos ahora que encontramos un estado , desde el cual existe una transición por la letra . Denotaremos el estado al que lleva la transición con .
-
Ahora tenemos dos casos. O bien , o no.
-
Si , entonces podemos simplemente asignar y salir.
-
En caso contrario es un poco más complicado. Es necesario clonar el estado : creamos un estado nuevo , copiamos todos los datos de (enlace de sufijo y transición) excepto el valor . Asignaremos .
Después de clonar dirigimos el enlace de sufijo de a , y también de a clone.
Finalmente necesitamos caminar desde el estado hacia atrás usando enlaces de sufijo mientras haya una transición por al estado , y redirigir todas esas al estado .
-
En cualquiera de los tres casos, después de completar el procedimiento, actualizamos el valor con el estado .
Si también queremos saber qué estados son terminales y cuáles no, podemos encontrar todos los estados terminales después de construir el autómata de sufijos completo para todo el string . Para ello, tomamos el estado correspondiente a todo el string (guardado en la variable ), y seguimos sus enlaces de sufijo hasta que llegamos al estado inicial. Marcaremos todos los estados visitados como terminales. Es fácil entender que al hacer eso marcaremos exactamente los estados correspondientes a todos los sufijos del string , que son exactamente los estados terminales.
En la siguiente sección veremos en detalle cada paso y mostraremos su corrección.
Aquí solo notamos que, como solo creamos uno o dos estados nuevos por cada carácter de , el autómata de sufijos contiene un número lineal de estados.
La linealidad del número de transiciones, y en general la linealidad del tiempo de ejecución del algoritmo es menos clara, y se demostrarán después de que hayamos demostrado la corrección.
Corrección
-
Llamaremos a una transición continua si . En caso contrario, es decir, cuando , la transición se llamará no continua.
Como podemos ver de la descripción del algoritmo, las transiciones continuas y no continuas llevarán a distintos casos del algoritmo. Las transiciones continuas están fijas, y nunca cambiarán otra vez. En contraste, una transición no continua puede cambiar cuando se agregan letras nuevas al string (el extremo de la arista de transición puede cambiar).
-
Para evitar ambigüedad denotaremos el string para el cual se construyó el autómata de sufijos antes de agregar el carácter actual con .
-
El algoritmo empieza creando un estado nuevo , que corresponderá a todo el string . Está claro por qué tenemos que crear un estado nuevo. Junto con el carácter nuevo se crea una clase de equivalencia nueva.
-
Después de crear un estado nuevo recorremos por enlaces de sufijo empezando desde el estado correspondiente a todo el string . Para cada estado intentamos agregar una transición con el carácter al estado nuevo . Así agregamos a cada sufijo de el carácter . Sin embargo solo podemos agregar estas transiciones nuevas si no entran en conflicto con una ya existente. Por lo tanto, en cuanto encontramos una transición ya existente con tenemos que detenernos.
-
En el caso más simple llegamos al estado ficticio . Esto significa que agregamos la transición con a todos los sufijos de . Esto también significa que el carácter no había formado parte del string antes. Por lo tanto el enlace de sufijo de tiene que llevar al estado .
-
En el segundo caso nos encontramos con una transición existente . Esto significa que intentamos agregar un string (donde es un sufijo de ) a la máquina que ya existe en la máquina (el string ya aparece como subcadena de ). Como asumimos que el autómata para el string está construido correctamente, no deberíamos agregar una transición nueva aquí.
Sin embargo hay una dificultad. ¿A qué estado debería llevar el enlace de sufijo del estado ? Tenemos que hacer un enlace de sufijo a un estado en el que el string más largo es exactamente , es decir, el de este estado debería ser . Sin embargo es posible que tal estado aún no exista, es decir, . En este caso tenemos que crear tal estado, partiendo el estado .
-
Si la transición resulta ser continua, entonces . En este caso todo es simple. Dirigimos el enlace de sufijo de al estado .
-
En caso contrario la transición es no continua, es decir, . Esto significa que el estado corresponde no solo al sufijo de con longitud , sino también a subcadenas más largas de . No podemos hacer otra cosa que partir el estado en dos subestados, de modo que el primero tenga longitud .
¿Cómo podemos partir un estado? Clonamos el estado , lo que nos da el estado , y ponemos . Copiamos todas las transiciones de a , porque no queremos cambiar los caminos que recorren . También ponemos el enlace de sufijo de al destino del enlace de sufijo de , y ponemos el enlace de sufijo de a .
Y después de partir el estado, ponemos el enlace de sufijo de a .
En el último paso cambiamos algunas de las transiciones a , las redirigimos a . ¿Qué transiciones tenemos que cambiar? Basta con redirigir solo las transiciones correspondientes a todos los sufijos del string (donde es el string más largo de ), es decir, necesitamos continuar moviéndonos a lo largo de los enlaces de sufijo, empezando desde el vértice hasta que llegamos al estado ficticio o a una transición que lleva a un estado distinto de .
Número lineal de operaciones
Primero hacemos inmediatamente la hipótesis de que el tamaño del alfabeto es constante. Si no es el caso, entonces no será posible hablar de la complejidad temporal lineal. La lista de transiciones desde un vértice se guardará en un árbol balanceado, que permite realizar rápido operaciones de búsqueda de clave y agregar claves. Por lo tanto, si denotamos con el tamaño del alfabeto, entonces el comportamiento asintótico del algoritmo será con memoria . Sin embargo, si el alfabeto es lo bastante pequeño, entonces se puede sacrificar memoria evitando árboles balanceados, y guardar las transiciones en cada vértice como un arreglo de longitud (para búsqueda rápida por clave) y una lista dinámica (para recorrer rápido todas las claves disponibles). Así alcanzamos la complejidad temporal para el algoritmo, pero a costa de complejidad de memoria .
Así que consideraremos el tamaño del alfabeto constante, es decir, cada operación de buscar una transición por un carácter, agregar una transición, buscar la siguiente transición — todas estas operaciones se pueden hacer en .
Si consideramos todas las partes del algoritmo, entonces contiene tres lugares en el algoritmo en los que la complejidad lineal no es obvia:
- El primer lugar es el recorrido a través de los enlaces de sufijo desde el estado , agregando transiciones con el carácter .
- El segundo lugar es la copia de transiciones cuando el estado se clona en un estado nuevo .
- El tercer lugar es cambiar la transición que lleva a , redirigiéndolas a .
Usamos el hecho de que el tamaño del autómata de sufijos (tanto en el número de estados como en el número de transiciones) es lineal. (La demostración de la linealidad del número de estados es el algoritmo mismo, y la demostración de linealidad del número de estados se da abajo, después de la implementación del algoritmo).
Así, la complejidad total del primer y segundo lugar es obvia, después de todo cada operación agrega solo una transición nueva amortizada al autómata.
Resta estimar la complejidad total del tercer lugar, en el que redirigimos transiciones que originalmente apuntaban a , a . Denotamos . Este es un sufijo del string , y con cada iteración su longitud disminuye — y por tanto la posición como sufijo del string aumenta de forma monótona con cada iteración. En este caso, si antes de la primera iteración del bucle, el string correspondiente estaba a profundidad () de (contando la profundidad como el número de enlaces de sufijo), entonces después de la última iteración el string será un 2-ésimo enlace de sufijo en el camino desde (que se convertirá en el valor nuevo ).
Así, cada iteración de este bucle lleva al hecho de que la posición del string como sufijo del string actual aumentará de forma monótona. Por lo tanto este ciclo no puede ejecutarse más de iteraciones, que era lo que se requería demostrar.
Implementación
Primero describimos una estructura de datos que guardará toda la información sobre una transición específica (, y la lista de transiciones). Si es necesario se puede agregar un flag terminal aquí, así como otra información. Guardaremos la lista de transiciones en forma de un , que nos permite alcanzar memoria total y tiempo para procesar todo el string.
struct state {
int len, link;
map<char, int> next;
};El autómata de sufijos mismo se guardará en un arreglo de estas estructuras . Guardamos el tamaño actual y también la variable , el estado correspondiente a todo el string en el momento.
const int MAXLEN = 100000;
state st[MAXLEN * 2];
int sz, last;Damos una función que inicializa un autómata de sufijos (creando un autómata de sufijos con un solo estado).
void sa_init() {
st[0].len = 0;
st[0].link = -1;
sz++;
last = 0;
}Y finalmente damos la implementación de la función principal — que agrega el siguiente carácter al final de la línea actual, reconstruyendo la máquina en consecuencia.
void sa_extend(char c) {
int cur = sz++;
st[cur].len = st[last].len + 1;
int p = last;
while (p != -1 && !st[p].next.count(c)) {
st[p].next[c] = cur;
p = st[p].link;
}
if (p == -1) {
st[cur].link = 0;
} else {
int q = st[p].next[c];
if (st[p].len + 1 == st[q].len) {
st[cur].link = q;
} else {
int clone = sz++;
st[clone].len = st[p].len + 1;
st[clone].next = st[q].next;
st[clone].link = st[q].link;
while (p != -1 && st[p].next[c] == q) {
st[p].next[c] = clone;
p = st[p].link;
}
st[q].link = st[cur].link = clone;
}
}
last = cur;
}Como se mencionó arriba, si se sacrifica memoria (, donde es el tamaño del alfabeto), entonces se puede alcanzar el tiempo de construcción de la máquina en , incluso para cualquier tamaño de alfabeto . Pero para esto habrá que guardar un arreglo de tamaño en cada estado (para saltar rápido a la transición de la letra), y adicionalmente una lista de todas las transiciones (para iterar rápido sobre las transiciones).
Propiedades adicionales
Número de estados
El número de estados en un autómata de sufijos del string de longitud no excede (para ).
La demostración es el algoritmo de construcción mismo, ya que inicialmente el autómata consiste de un estado, y en la primera y segunda iteración solo se creará un solo estado, y en los pasos restantes se crearán a lo sumo estados cada uno.
Sin embargo también podemos mostrar esta estimación sin conocer el algoritmo. Recordemos que el número de estados es igual al número de conjuntos distintos. Además estos conjuntos forman un árbol (un vértice padre contiene todos los conjuntos hijos en su conjunto). Consideremos este árbol y transformémoslo un poco: mientras tenga un vértice interno con solo un hijo (lo que significa que el conjunto del hijo omite al menos una posición del conjunto padre), creamos un hijo nuevo con el conjunto de las posiciones faltantes. Al final tenemos un árbol en el que cada vértice interno tiene grado mayor que uno, y el número de hojas no excede . Por lo tanto no hay más de vértices en tal árbol.
Esta cota del número de estados se puede de hecho alcanzar para cada . Un string posible es:
En cada iteración, empezando en la tercera, el algoritmo partirá un estado, resultando en exactamente estados.
Número de transiciones
El número de transiciones en un autómata de sufijos de un string de longitud no excede (para ).
Demostremos esto:
Primero estimemos el número de transiciones continuas. Consideremos un árbol de expansión de los caminos más largos en el autómata empezando en el estado . Este esqueleto consistirá solo de las aristas continuas, y por tanto su número es menor que el número de estados, es decir, no excede .
Ahora estimemos el número de transiciones no continuas. Sea la transición no continua actual con el carácter . Tomamos el string correspondiente , donde el string corresponde al camino más largo del estado inicial a , y al camino más largo de a cualquier estado terminal. Por un lado, cada tal string para cada string incompleto será distinto (ya que los strings y se forman solo por transiciones completas). Por otro lado cada tal string , por la definición de los estados terminales, será un sufijo de todo el string . Como hay solo sufijos no vacíos de , y ninguno de los strings puede contener (porque todo el string solo contiene transiciones completas), el número total de transiciones incompletas no excede .
Combinando estas dos estimaciones nos da la cota . Sin embargo, como el número máximo de estados solo se puede alcanzar con el caso de prueba y este caso claramente tiene menos de transiciones, obtenemos la cota más ajustada de para el número de transiciones en un autómata de sufijos.
Esta cota también se puede alcanzar con el string:
Aplicaciones
Aquí vemos algunas tareas que se pueden resolver usando el autómata de sufijos. Por simplicidad asumimos que el tamaño del alfabeto es constante, lo que nos permite considerar la complejidad de agregar un carácter y el recorrido como constantes.
Comprobar ocurrencia
Dado un texto , y varios patrones . Tenemos que comprobar si los strings aparecen o no como subcadena de .
Construimos un autómata de sufijos del texto en tiempo . Para comprobar si un patrón aparece en , seguimos las transiciones, empezando desde , según los caracteres de . Si en algún punto no existe una transición, entonces el patrón no aparece como subcadena de . Si podemos procesar todo el string de esta forma, entonces el string aparece en .
Es claro que esto tomará tiempo por cada string . Además el algoritmo de hecho encuentra la longitud del prefijo más largo de que aparece en el texto.
Número de subcadenas distintas
Dado un string . Se quiere calcular el número de subcadenas distintas.
Construyamos un autómata de sufijos para el string .
Cada subcadena de corresponde a algún camino en el autómata. Por lo tanto el número de subcadenas distintas es igual al número de caminos distintos en el autómata empezando en .
Dado que el autómata de sufijos es un grafo dirigido acíclico, el número de formas distintas se puede calcular usando programación dinámica.
A saber, sea el número de formas, empezando en el estado (incluyendo el camino de longitud cero). Entonces tenemos la recursión:
Es decir, se puede expresar como la suma de respuestas para todos los extremos de las transiciones de .
El número de subcadenas distintas es el valor (ya que no contamos la subcadena vacía).
Complejidad temporal total:
Como alternativa, podemos aprovechar el hecho de que cada estado coincide con subcadenas de longitud . Por lo tanto, dado , tenemos el total de subcadenas distintas en el estado siendo .
Esto se demuestra de forma sucinta abajo:
long long get_diff_strings(){
long long tot = 0;
for(int i = 1; i < sz; i++) {
tot += st[i].len - st[st[i].link].len;
}
return tot;
}Aunque esto también es , no requiere espacio extra ni llamadas recursivas, en consecuencia corre más rápido en la práctica.
Longitud total de todas las subcadenas distintas
Dado un string . Queremos calcular la longitud total de todas sus subcadenas distintas.
La solución es similar a la anterior, solo que ahora es necesario considerar dos cantidades para la parte de programación dinámica: el número de subcadenas distintas y su longitud total .
Ya describimos cómo calcular en la tarea anterior. El valor se puede calcular usando la recursión:
Tomamos la respuesta de cada vértice adyacente , y le sumamos (ya que cada subcadena es un carácter más larga cuando se empieza desde el estado ).
Otra vez esta tarea se puede calcular en tiempo .
Como alternativa, podemos, otra vez, aprovechar el hecho de que cada estado coincide con subcadenas de longitud . Como y la fórmula de la serie aritmética (donde denota la suma de términos, representando el primer término, y representando el último), podemos calcular la longitud de las subcadenas en un estado en tiempo constante. Luego sumamos estos totales para cada estado en el autómata. Esto se muestra en el código de abajo:
long long get_tot_len_diff_substings() {
long long tot = 0;
for(int i = 1; i < sz; i++) {
long long shortest = st[st[i].link].len + 1;
long long longest = st[i].len;
long long num_strings = longest - shortest + 1;
long long cur = num_strings * (longest + shortest) / 2;
tot += cur;
}
return tot;
}Este enfoque corre en tiempo , pero experimentalmente corre 20x más rápido que la versión de programación dinámica con memoización en strings aleatorizados. No requiere espacio extra ni recursión.
-ésima subcadena lexicográfica {data-toc-label=“k-ésima subcadena lexicográfica”}
Dado un string . Tenemos que responder múltiples consultas. Para cada número dado tenemos que encontrar el -ésimo string en la lista ordenada lexicográficamente de todas las subcadenas.
La solución a este problema se basa en la idea de los dos problemas anteriores. La -ésima subcadena lexicográfica corresponde al -ésimo camino lexicográfico en el autómata de sufijos. Por lo tanto, después de contar el número de caminos desde cada estado, podemos buscar fácilmente el -ésimo camino empezando desde la raíz del autómata.
Esto toma tiempo para el preprocesamiento y luego por cada consulta (donde es la respuesta a la consulta y es el tamaño del alfabeto).
Menor desplazamiento cíclico
Dado un string . Queremos encontrar el desplazamiento cíclico lexicográficamente más pequeño.
Construimos un autómata de sufijos para el string . Entonces el autómata contendrá en sí mismo como caminos todos los desplazamientos cíclicos del string .
En consecuencia el problema se reduce a encontrar el camino lexicográficamente más pequeño de longitud , lo que se puede hacer de forma trivial: empezamos en el estado inicial y pasamos de forma voraz por las transiciones con el carácter mínimo.
La complejidad temporal total es .
Número de ocurrencias
Para un texto dado . Tenemos que responder múltiples consultas. Para cada patrón dado tenemos que averiguar cuántas veces aparece el string en el string como subcadena.
Construimos el autómata de sufijos para el texto .
Luego hacemos el siguiente preprocesamiento: para cada estado en el autómata calculamos el número que es igual al tamaño del conjunto . De hecho todos los strings correspondientes al mismo estado aparecen en el texto una cantidad igual de veces, que es igual al número de posiciones en el conjunto .
Sin embargo no podemos construir los conjuntos de forma explícita, por lo tanto solo consideramos sus tamaños .
Para calcularlos procedemos de la siguiente forma. Para cada estado, si no fue creado por clonación (y si no es el estado inicial ), lo inicializamos con . Luego recorreremos todos los estados en orden decreciente de su longitud , y sumaremos el valor actual a los enlaces de sufijo:
Esto da el valor correcto para cada estado.
¿Por qué es correcto? El número total de estados obtenidos no vía clonación es exactamente , y los primeros de ellos aparecieron cuando agregamos los primeros caracteres. En consecuencia para cada uno de estos estados contamos la posición correspondiente en la que se procesó. Por lo tanto inicialmente tenemos para cada estado de ese tipo, y para todos los demás.
Luego aplicamos la siguiente operación para cada : . El significado detrás de esto es que si un string aparece veces, entonces también todos sus sufijos aparecen en las mismas posiciones finales exactas, por tanto también veces.
¿Por qué no sobrecontamos en este procedimiento (es decir, no contamos algunas posiciones dos veces)? Porque agregamos las posiciones de un estado a solo un otro estado, así que no puede ocurrir que un estado dirija sus posiciones a otro estado dos veces de dos formas distintas.
Así podemos calcular las cantidades para todos los estados del autómata en tiempo .
Después de eso responder una consulta es simplemente consultar el valor , donde es el estado correspondiente al patrón, si tal estado existe. En caso contrario responder con . Responder una consulta toma tiempo .
Posición de la primera ocurrencia
Dado un texto y múltiples consultas. Para cada string de consulta queremos encontrar la posición de la primera ocurrencia de en el string (la posición del inicio de ).
Otra vez construimos un autómata de sufijos. Además precomputamos la posición para todos los estados del autómata, es decir, para cada estado queremos encontrar la posición del final de la primera ocurrencia. En otras palabras, queremos encontrar de antemano el elemento mínimo de cada conjunto (ya que obviamente no podemos mantener todos los conjuntos de forma explícita).
Para mantener estas posiciones extendemos la función sa_extend().
Cuando creamos un estado nuevo , ponemos:
Y cuando clonamos un vértice como , ponemos:
(ya que la única otra opción para un valor sería que es definitivamente demasiado grande)
Así la respuesta a una consulta es simplemente , donde es el estado correspondiente al string . Responder una consulta otra vez toma solo tiempo .
Todas las posiciones de ocurrencia
Esta vez tenemos que mostrar todas las posiciones de las ocurrencias en el string .
Otra vez construimos un autómata de sufijos para el texto . Similar a la tarea anterior computamos la posición para todos los estados.
Claramente es parte de la respuesta, si es el estado correspondiente a un string de consulta . Así tomamos en cuenta el estado del autómata que contiene . ¿Qué otros estados necesitamos tomar en cuenta? Todos los estados que corresponden a strings para los cuales es un sufijo. En otras palabras necesitamos encontrar todos los estados que pueden alcanzar el estado vía enlaces de sufijo.
Por lo tanto para resolver el problema necesitamos guardar para cada estado una lista de referencias de sufijo que llevan a él. La respuesta a la consulta entonces contendrá todos los para cada estado que podemos encontrar en un DFS / BFS empezando desde el estado usando solo las referencias de sufijo.
En general, esto requiere para el preprocesamiento y por cada petición, donde — este es el tamaño de la respuesta.
Primero, bajamos por el autómata por cada carácter del patrón para encontrar nuestro nodo de inicio requiriendo . Luego, usamos nuestro workaround que funcionará en tiempo , porque no visitaremos un estado dos veces (porque solo un enlace de sufijo sale de cada estado, así que no puede haber dos caminos distintos que lleven al mismo estado).
Solo debemos tener en cuenta que dos estados distintos pueden tener el mismo valor . Esto ocurre si un estado se obtuvo clonando otro. Sin embargo, esto no arruina la complejidad, ya que cada estado solo puede tener a lo sumo un clon.
Además, también podemos deshacernos de las posiciones duplicadas, si no emitimos las posiciones de los estados clonados.
De hecho un estado, al que un estado clonado puede llegar, también es alcanzable desde el estado original.
Así si recordamos el flag is_cloned para cada estado, podemos simplemente ignorar los estados clonados y solo emitir para todos los demás estados.
Aquí hay algunos esbozos de implementación:
struct state {
...
bool is_clone;
int first_pos;
vector<int> inv_link;
};
// after constructing the automaton
for (int v = 1; v < sz; v++) {
st[st[v].link].inv_link.push_back(v);
}
// output all positions of occurrences
void output_all_occurrences(int v, int P_length) {
if (!st[v].is_clone)
cout << st[v].first_pos - P_length + 1 << endl;
for (int u : st[v].inv_link)
output_all_occurrences(u, P_length);
}String más corto que no aparece
Dado un string y un cierto alfabeto. Tenemos que encontrar un string de la menor longitud, que no aparece en .
Aplicaremos programación dinámica sobre el autómata de sufijos construido para el string .
Sea la respuesta para el nodo , es decir, ya procesamos parte de la subcadena, estamos actualmente en el estado , y queremos encontrar el menor número de caracteres que hay que agregar para encontrar una transición inexistente. Calcular es muy simple. Si no hay transición usando al menos un carácter del alfabeto, entonces . En caso contrario un carácter no es suficiente, y así necesitamos tomar el mínimo de todas las respuestas de todas las transiciones:
La respuesta al problema será , y el string real se puede restaurar usando el arreglo calculado .
Subcadena común más larga de dos strings
Dados dos strings y . Tenemos que encontrar la subcadena común más larga, es decir, un string que aparece como subcadena en y también en .
Construimos un autómata de sufijos para el string .
Ahora tomaremos el string , y para cada prefijo buscaremos el sufijo más largo de este prefijo en . En otras palabras, para cada posición en el string , queremos encontrar la subcadena común más larga de y que termina en esa posición.
Para ello usaremos dos variables, el estado actual , y la longitud actual . Estas dos variables describirán la parte coincidente actual: su longitud y el estado que le corresponde.
Inicialmente y , es decir, la coincidencia está vacía.
Ahora describamos cómo podemos agregar un carácter y recalcular la respuesta para él.
- Si hay una transición desde con el carácter , entonces simplemente seguimos la transición y aumentamos en uno.
- Si no hay tal transición, tenemos que acortar la parte coincidente actual, lo que significa que necesitamos seguir el enlace de sufijo: . Al mismo tiempo, la longitud actual tiene que acortarse. Obviamente necesitamos asignar , ya que después de pasar por el enlace de sufijo terminamos en un estado cuyo string más largo correspondiente es una subcadena.
- Si todavía no hay transición usando el carácter requerido, repetimos y otra vez vamos por el enlace de sufijo y disminuimos , hasta que encontramos una transición o llegamos al estado ficticio (lo que significa que el símbolo no aparece en absoluto en , así que asignamos ).
La respuesta a la tarea será el máximo de todos los valores .
La complejidad de esta parte es , ya que en un movimiento podemos o bien aumentar en uno, o hacer varios pases a través de los enlaces de sufijo, cada uno termina reduciendo el valor .
Implementación:
string lcs (string S, string T) {
sa_init();
for (int i = 0; i < S.size(); i++)
sa_extend(S[i]);
int v = 0, l = 0, best = 0, bestpos = 0;
for (int i = 0; i < T.size(); i++) {
while (v && !st[v].next.count(T[i])) {
v = st[v].link ;
l = st[v].len;
}
if (st[v].next.count(T[i])) {
v = st [v].next[T[i]];
l++;
}
if (l > best) {
best = l;
bestpos = i;
}
}
return T.substr(bestpos - best + 1, best);
} Subcadena común más larga de múltiples strings
Hay strings dados. Tenemos que encontrar la subcadena común más larga, es decir, un string que aparece como subcadena en cada string .
Unimos todos los strings en un string grande , separando los strings por caracteres especiales (uno por cada string):
Luego construimos el autómata de sufijos para el string .
Ahora necesitamos encontrar un string en la máquina, que está contenido en todos los strings , y esto se puede hacer usando los caracteres especiales agregados. Nótese que si una subcadena está incluida en algún string , entonces en el autómata de sufijos existe un camino empezando desde esta subcadena que contiene el carácter y no contiene los otros caracteres .
Así necesitamos calcular la alcanzabilidad, que nos dice para cada estado de la máquina y cada símbolo si existe tal camino. Esto se puede computar fácilmente con DFS o BFS y programación dinámica. Después de eso, la respuesta al problema será el string para el estado , desde el cual existían los caminos para todos los caracteres especiales.