Factorización de Lyndon
Factorización de Lyndon
Primero definamos la noción de factorización de Lyndon.
Un string se llama simple (o palabra de Lyndon), si es estrictamente más pequeño que cualquiera de sus propios sufijos no triviales. Ejemplos de strings simples son: , , , , , , . Se puede mostrar que un string es simple si y solo si es estrictamente más pequeño que todos sus desplazamientos cíclicos no triviales.
A continuación, sea un string dado. La factorización de Lyndon del string es una factorización , donde todos los strings son simples, y están en orden no creciente .
Se puede mostrar que para cualquier string tal factorización existe y que es única.
Algoritmo de Duval
El algoritmo de Duval construye la factorización de Lyndon en tiempo usando de memoria adicional.
Primero introduzcamos otra noción: un string se llama pre-simple, si tiene la forma , donde es un string simple y es un prefijo de (posiblemente vacío). Un string simple también es pre-simple.
El algoritmo de Duval es voraz. En cualquier punto de su ejecución, el string estará de hecho dividido en tres strings , donde la factorización de Lyndon de ya se encontró y está finalizada, el string es pre-simple (y conocemos la longitud del string simple en él), y está completamente sin tocar. En cada iteración el algoritmo de Duval toma el primer carácter del string e intenta agregarlo al string . Si deja de ser pre-simple, entonces la factorización de Lyndon de alguna parte de se vuelve conocida, y esa parte pasa a .
Describamos el algoritmo con más detalle. El puntero siempre apuntará al comienzo del string . El bucle externo se ejecutará mientras . Dentro del bucle usamos dos punteros adicionales, que apunta al comienzo de , y que apunta al carácter actual con el que estamos comparando. Queremos agregar el carácter al string , lo que requiere una comparación con el carácter . Puede haber tres casos distintos:
- : si este es el caso, entonces agregar el símbolo a no viola su pre-simplicidad. Así que simplemente incrementamos los punteros y .
- : aquí, el string se vuelve simple. Podemos incrementar y resetear al comienzo de , de modo que el siguiente carácter se pueda comparar con el comienzo de la palabra simple.
- : el string ya no es pre-simple. Por lo tanto partiremos el string pre-simple en sus strings simples y el resto, posiblemente vacío. El string simple tendrá longitud . En la siguiente iteración empezamos de nuevo con el restante.
Implementación
Aquí presentamos la implementación del algoritmo de Duval, que devolverá la factorización de Lyndon deseada de un string dado .
vector<string> duval(string const& s) {
int n = s.size();
int i = 0;
vector<string> factorization;
while (i < n) {
int j = i + 1, k = i;
while (j < n && s[k] <= s[j]) {
if (s[k] < s[j])
k = i;
else
k++;
j++;
}
while (i <= k) {
factorization.push_back(s.substr(i, j - k));
i += j - k;
}
}
return factorization;
}Complejidad
Estimemos el tiempo de ejecución de este algoritmo.
El bucle while externo no supera iteraciones, ya que al final de cada iteración aumenta. También el segundo bucle while interno corre en , ya que solo emite la factorización final.
Así que solo nos interesa el primer bucle while interno. ¿Cuántas iteraciones realiza en el peor caso? Es fácil ver que las palabras simples que identificamos en cada iteración del bucle externo son más largas que el resto que comparamos adicionalmente. Por lo tanto también la suma de los restos será menor que , lo que significa que solo realizamos a lo sumo iteraciones del primer bucle while interno. De hecho el número total de comparaciones de caracteres no superará .
Encontrar el desplazamiento cíclico más pequeño
Sea un string. Construimos la factorización de Lyndon del string (en tiempo ). Buscaremos un string simple en la factorización que empiece en una posición menor que (es decir, empieza en la primera instancia de ), y termine en una posición mayor o igual que (es decir, en la segunda instancia de ). Se afirma que la posición de inicio de este string simple será el comienzo del desplazamiento cíclico más pequeño deseado. Esto se puede verificar fácilmente usando la definición de la descomposición de Lyndon.
El comienzo del bloque simple se puede encontrar fácilmente: basta con recordar el puntero al comienzo de cada iteración del bucle externo, que indicaba el comienzo del string pre-simple actual.
Así obtenemos la siguiente implementación:
string min_cyclic_string(string s) {
s += s;
int n = s.size();
int i = 0, ans = 0;
while (i < n / 2) {
ans = i;
int j = i + 1, k = i;
while (j < n && s[k] <= s[j]) {
if (s[k] < s[j])
k = i;
else
k++;
j++;
}
while (i <= k)
i += j - k;
}
return s.substr(ans, n / 2);
}