Skip to Content

Algoritmo de Manacher - Encontrar todos los sub-palíndromos en O(N)O(N)

Enunciado

Se da un string ss de longitud nn. Encontrar todos los pares (i,j)(i, j) tales que la subcadena s[ij]s[i\dots j] es un palíndromo. Un string tt es un palíndromo cuando t=trevt = t_{rev} (trevt_{rev} es el string invertido de tt).

Enunciado más preciso

En el peor caso un string puede tener hasta O(n2)O(n^2) subcadenas palindrómicas, y a primera vista parece que no hay un algoritmo lineal para este problema.

Pero la información sobre los palíndromos se puede guardar de forma compacta: para cada posición ii encontraremos el número de palíndromos no vacíos centrados en esa posición.

Los palíndromos con un centro común forman una cadena contigua: es decir, si tenemos un palíndromo de longitud ll centrado en ii, también tenemos palíndromos de longitudes l2l-2, l4l-4 y así sucesivamente, también centrados en ii. Por lo tanto, recolectaremos la información de todas las subcadenas palindrómicas de esta manera.

Los palíndromos de longitud impar y par se contabilizan por separado como dodd[i]d_{odd}[i] y deven[i]d_{even}[i]. Para los palíndromos de longitud par asumimos que están centrados en la posición ii si sus dos caracteres centrales son s[i]s[i] y s[i1]s[i-1].

Por ejemplo, el string s=abababcs = abababc tiene tres palíndromos de longitud impar con centros en la posición s[3]=bs[3] = b, es decir, dodd[3]=3d_{odd}[3] = 3:

a b a bs3 a bdodd[3]=3ca\ \overbrace{b\ a\ \underbrace{b}{s_3}\ a\ b}^{d{odd}[3]=3} c

Y el string s=cbaabds = cbaabd tiene dos palíndromos de longitud par con centros en la posición s[3]=as[3] = a, es decir, deven[3]=2d_{even}[3] = 2:

c b a as3 bdeven[3]=2dc\ \overbrace{b\ a\ \underbrace{a}{s_3}\ b}^{d{even}[3]=2} d

Es un hecho sorprendente que existe un algoritmo, suficientemente simple, que calcula estos “arreglos de palindromicidad” dodd[]d_{odd}[] y deven[]d_{even}[] en tiempo lineal. El algoritmo se describe en este artículo.

Solución

En general, este problema tiene muchas soluciones: con hashing de strings se puede resolver en O(nlogn)O(n\cdot \log n), y con árboles de sufijos y LCA rápido este problema se puede resolver en O(n)O(n).

Pero el método descrito aquí es suficientemente más simple y tiene una constante oculta menor en complejidad temporal y de memoria. Este algoritmo fue descubierto por Glenn K. Manacher en 1975.

Otra forma moderna de resolver este problema y de tratar con palíndromos en general es mediante el llamado árbol palindrómico, o eertree.

Algoritmo trivial

Para evitar ambigüedades en la descripción posterior denotamos qué es el “algoritmo trivial”.

Es el algoritmo que hace lo siguiente. Para cada posición de centro ii intenta aumentar la respuesta en uno mientras sea posible, comparando un par de caracteres correspondientes cada vez.

Tal algoritmo es lento: solo puede calcular la respuesta en O(n2)O(n^2).

La implementación del algoritmo trivial es:

vector<int> manacher_odd_trivial(string s) { int n = s.size(); s = "$" + s + "^"; vector<int> p(n + 2); for(int i = 1; i <= n; i++) { while(s[i - p[i]] == s[i + p[i]]) { p[i]++; } } return vector<int>(begin(p) + 1, end(p) - 1); }

Se usaron los caracteres terminales $ y ^ para evitar tratar los extremos del string por separado.

Algoritmo de Manacher

Describimos el algoritmo para encontrar todos los sub-palíndromos de longitud impar, es decir, para calcular dodd[]d_{odd}[].

Para un cálculo rápido mantendremos los bordes exclusivos (l,r)(l, r) del (sub-)palíndromo más a la derecha encontrado (es decir, el (sub-)palíndromo más a la derecha actual es s[l+1]s[l+2]s[r1]s[l+1] s[l+2] \dots s[r-1]). Inicialmente ponemos l=0,r=1l = 0, r = 1, lo que corresponde al string vacío.

Así, queremos calcular dodd[i]d_{odd}[i] para el siguiente ii, y todos los valores anteriores en dodd[]d_{odd}[] ya se han calculado. Hacemos lo siguiente:

  • Si ii está fuera del sub-palíndromo actual, es decir, iri \geq r, simplemente lanzamos el algoritmo trivial.

    Así incrementaremos dodd[i]d_{odd}[i] de forma consecutiva y comprobaremos cada vez si la subcadena más a la derecha actual [idodd[i]i+dodd[i]][i - d_{odd}[i]\dots i + d_{odd}[i]] es un palíndromo. Cuando encontremos el primer desajuste o lleguemos a los bordes de ss, nos detendremos. En este caso ya habremos calculado dodd[i]d_{odd}[i]. Después de esto, no debemos olvidar actualizar (l,r)(l, r). rr debe actualizarse de forma que represente el último índice del sub-palíndromo más a la derecha actual.

  • Ahora consideremos el caso en que iri \le r. Intentaremos extraer alguna información de los valores ya calculados en dodd[]d_{odd}[]. Así, encontremos la posición “espejo” de ii en el sub-palíndromo (l,r)(l, r), es decir, obtendremos la posición j=l+(ri)j = l + (r - i), y comprobamos el valor de dodd[j]d_{odd}[j]. Como jj es la posición simétrica a ii respecto de (l+r)/2(l+r)/2, casi siempre podemos asignar dodd[i]=dodd[j]d_{odd}[i] = d_{odd}[j]. Ilustración de esto (el palíndromo alrededor de jj se “copia” de hecho al palíndromo alrededor de ii):

     sl+1  sjdodd[j]+1  sj  sj+dodd[j]1 palindrome  sidodd[j]+1  si  si+dodd[j]1 palindrome  sr1 palindrome  \ldots\ \overbrace{ s_{l+1}\ \ldots\ \underbrace{ s_{j-d_{odd}[j]+1}\ \ldots\ s_j\ \ldots\ s_{j+d_{odd}[j]-1}\ }\text{palindrome}\ \ldots\ \underbrace{ s{i-d_{odd}[j]+1}\ \ldots\ s_i\ \ldots\ s_{i+d_{odd}[j]-1}\ }\text{palindrome}\ \ldots\ s{r-1}\ }^\text{palindrome}\ \ldots

    Pero hay un caso delicado que hay que manejar correctamente: cuando el palíndromo “interno” alcanza los bordes del “externo”, es decir, jdodd[j]lj - d_{odd}[j] \le l (o, lo que es lo mismo, i+dodd[j]ri + d_{odd}[j] \ge r). Como la simetría fuera del palíndromo “externo” no está garantizada, asignar simplemente dodd[i]=dodd[j]d_{odd}[i] = d_{odd}[j] será incorrecto: no tenemos datos suficientes para afirmar que el palíndromo en la posición ii tiene la misma longitud.

    En realidad, debemos restringir por ahora la longitud de nuestro palíndromo, es decir, asignar dodd[i]=rid_{odd}[i] = r - i, para manejar correctamente esas situaciones. Después de esto ejecutaremos el algoritmo trivial, que intentará aumentar dodd[i]d_{odd}[i] mientras sea posible.

    Ilustración de este caso (el palíndromo con centro jj se restringe para caber en el palíndromo “externo”):

     sl+1  sj  sj+(jl)1 palindrome  si(ri)+1  si  sr1palindrome palindrome try moving here \ldots\ \overbrace{ \underbrace{ s_{l+1}\ \ldots\ s_j\ \ldots\ s_{j+(j-l)-1}\ }\text{palindrome}\ \ldots\ \underbrace{ s{i-(r-i)+1}\ \ldots\ s_i\ \ldots\ s_{r-1} }\text{palindrome}\ }^\text{palindrome}\ \underbrace{ \ldots \ldots \ldots \ldots \ldots }\text{try moving here}

    En la ilustración se muestra que el palíndromo con centro jj podría ser más grande y salir del palíndromo “externo”, pero, con ii como centro, solo podemos usar la parte que cabe por completo en el palíndromo “externo”. Pero la respuesta para la posición ii (dodd[i]d_{odd}[i]) puede ser mucho mayor que esta parte, así que a continuación ejecutaremos nuestro algoritmo trivial, que intentará hacerlo crecer fuera de nuestro palíndromo “externo”, es decir, hacia la región “try moving here”.

De nuevo, no debemos olvidar actualizar los valores (l,r)(l, r) después de calcular cada dodd[i]d_{odd}[i].

Complejidad del algoritmo de Manacher

A primera vista no es obvio que este algoritmo tenga complejidad lineal, porque a menudo ejecutamos el algoritmo naive al buscar la respuesta para una posición particular.

Sin embargo, un análisis más cuidadoso muestra que el algoritmo es lineal. De hecho, el algoritmo de construcción de la función Z, que se parece a este algoritmo, también trabaja en tiempo lineal.

Podemos notar que cada iteración del algoritmo trivial incrementa rr en uno. Además rr no puede disminuir durante el algoritmo. Así, el algoritmo trivial hará O(n)O(n) iteraciones en total.

Las demás partes del algoritmo de Manacher trabajan obviamente en tiempo lineal. Así, obtenemos complejidad O(n)O(n).

Implementación del algoritmo de Manacher

Para calcular dodd[]d_{odd}[], obtenemos el siguiente código. Cosas a notar:

  • ii es el índice de la letra central del palíndromo actual.
  • Si ii supera rr, dodd[i]d_{odd}[i] se inicializa en 0.
  • Si ii no supera rr, dodd[i]d_{odd}[i] se inicializa o bien con dodd[j]d_{odd}[j], donde jj es la posición espejo de ii en (l,r)(l,r), o bien dodd[i]d_{odd}[i] se restringe al tamaño del palíndromo “externo”.
  • El bucle while denota el algoritmo trivial. Lo lanzamos independientemente del valor de kk.
  • Si el tamaño del palíndromo centrado en ii es xx, entonces dodd[i]d_{odd}[i] almacena x+12\frac{x+1}{2}.
vector<int> manacher_odd(string s) { int n = s.size(); s = "$" + s + "^"; vector<int> p(n + 2); int l = 0, r = 1; for(int i = 1; i <= n; i++) { if(i <= r) { p[i] = min(r - i, p[l + (r - i)]); } while(s[i - p[i]] == s[i + p[i]]) { p[i]++; } if(i + p[i] > r) { l = i - p[i], r = i + p[i]; } } return vector<int>(begin(p) + 1, end(p) - 1); }

Trabajar con paridades

Aunque es posible implementar el algoritmo de Manacher para longitudes impares y pares por separado, la implementación de la versión para longitudes pares a menudo se considera más difícil, porque es menos natural y fácilmente conduce a errores por uno.

Para mitigar esto, es posible reducir todo el problema al caso en que solo tratamos palíndromos de longitud impar. Para ello, podemos poner un carácter adicional # entre cada letra del string y también al principio y al final del string:

abcbcba#a#b#c#b#c#b#a#,abcbcba \to #a#b#c#b#c#b#a#,

d=[1,2,1,2,1,4,1,8,1,4,1,2,1,2,1].d = [1,2,1,2,1,4,1,8,1,4,1,2,1,2,1].

Como se puede ver, d[2i]=2deven[i]+1d[2i]=2 d_{even}[i]+1 y d[2i+1]=2dodd[i]d[2i+1]=2 d_{odd}[i], donde dd denota el arreglo de Manacher para palíndromos de longitud impar en el string unido con #, mientras que doddd_{odd} y devend_{even} corresponden a los arreglos definidos arriba en el string inicial.

En efecto, los caracteres # no afectan a los palíndromos de longitud impar, que siguen centrados en los caracteres del string inicial, pero ahora los palíndromos de longitud par del string inicial son palíndromos de longitud impar del nuevo string centrados en caracteres #.

Nótese que d[2i]d[2i] y d[2i+1]d[2i+1] son esencialmente las longitudes incrementadas en 11 de los palíndromos más grandes de longitud par e impar centrados en ii, respectivamente.

La reducción se implementa de la siguiente manera:

vector<int> manacher(string s) { string t; for(auto c: s) { t += string("#") + c; } auto res = manacher_odd(t + "#"); return vector<int>(begin(res) + 1, end(res) - 1); }

Por simplicidad, se omite partir el arreglo en doddd_{odd} y devend_{even} así como su cálculo explícito.

Problemas