Tres algoritmos principales de O (n) para la concordancia de cadenas: KMP, Manacher y KMP extendido

Prefacio

Al escuchar la cadena, la primera reacción: enumeración, hash, DP, solo echa una mano.

Pero de lo que quiero hablar es del algoritmo de cadena, y la cadena que está en contacto con los conceptos básicos de C ++ no es una calificación en absoluto.

Por ejemplo, para la pregunta n≤1000, puede usar una cadena, y para la pregunta n≤10000000, debe usar el algoritmo de cadena.

Los tres algoritmos de este artículo tratan sobre la coincidencia de cadenas en algoritmos de cadenas. Debe comprender algunas cosas básicas (o omitirlas directamente):

La coincidencia de cadenas es uno de los problemas más antiguos y más estudiados de la informática . Una cadena de caracteres es una secuencia de caracteres definida en un alfabeto finito Σ. Por ejemplo, ATCTAGAGA es una cadena de caracteres en el alfabeto ∑ = {A, C, G, T}. El problema de la coincidencia de cadenas es buscar todas las apariciones de una cadena P en una cadena T grande. Entre ellos, T se llama texto, P se llama patrón y tanto T como P se definen en el mismo alfabeto Σ.

Aunque los algoritmos de coincidencia de cadenas se han desarrollado durante décadas, los algoritmos muy prácticos solo han aparecido en los últimos años. Existe una desconexión entre la investigación teórica y la aplicación práctica en el estudio de la correspondencia de cadenas. Los académicos que se especializan en la investigación de algoritmos se preocupan por los algoritmos que se ven muy bien en teoría, con una buena complejidad de tiempo . Los desarrolladores solo buscan el algoritmo más rápido posible en aplicaciones prácticas. Entre los dos nunca presten atención a lo que está haciendo el otro. Los algoritmos que combinan la investigación teórica con aplicaciones prácticas (como el algoritmo BNDM) solo han aparecido en los últimos años. En aplicaciones prácticas, a menudo es difícil encontrar un algoritmo que satisfaga las necesidades; tal algoritmo existe realmente, pero solo los expertos experimentados lo entienden. Considere la siguiente situación: un desarrollador de software, un biólogo computacional, un investigador o un estudiante no tiene un conocimiento profundo del campo de la correspondencia de cadenas, pero ahora necesita lidiar con un problema de búsqueda de texto. Esos libros sudorosos hacen que los lectores se ahoguen en un mar de varios algoritmos de coincidencia, pero no hay suficiente conocimiento para elegir el algoritmo más adecuado. Al final, a menudo conduce a la situación: elija uno de los algoritmos más simples para implementar. Esto a menudo conduce a un rendimiento deficiente, lo que afecta la calidad de todo el sistema de desarrollo. Para empeorar las cosas, elegí un algoritmo que se veía hermoso en teoría y dediqué mucho esfuerzo a implementarlo. Como resultado, descubrí que el efecto real es similar a un algoritmo simple, o incluso peor que un algoritmo simple. Por lo tanto, se debe seleccionar un algoritmo "práctico", es decir, que funcione mejor en aplicaciones prácticas, y un programador común puede completar el código de implementación del algoritmo en unas pocas horas. Además, en el campo de la investigación de emparejamiento de cadenas, un hecho bien conocido es que "cuanto más simple es la idea del algoritmo, mejor es el efecto de la aplicación práctica".

Los algoritmos tradicionales de coincidencia de cadenas se pueden resumir como búsqueda de prefijo, búsqueda de sufijo y búsqueda de subcadena. Los algoritmos representativos incluyen KMP , Shift-And , Shift- Or, BM , Horspool , BNDM , BOM, etc. Las técnicas utilizadas incluyen ventanas correderas, paralelismo de bits, autómatas , árboles de sufijos, etc.

——Un cierto grado

km²

El algoritmo KMP se utiliza para resolver un tipo de problema de búsqueda de prefijo: encuentre el prefijo más largo de A que pueda coincidir con el sufijo de cada prefijo de la cadena B.

Aquí llamamos a B una cadena coincidente y A una cadena de patrón (imagen vívida) ,

El método de fuerza bruta es la enumeración, suponga que la cadena actual B coincide con el punto i, y la cadena A coincide con el punto j:

  • Si B_i = A_j, entonces la longitud de la respuesta del prefijo i es j;
  • Si B_i \ neq A_j, entonces la longitud de la respuesta del prefijo i solo puede ser menor que j, entonces j está constantemente saltando hacia adelante, haciendo coincidir  A_1 ~  A_j y  B_ {i-j + 1} ~  Bi . Si la coincidencia es exitosa, la respuesta se registra como j;
  • ++ i , ++ j

Se puede demostrar que si iyj comienzan desde el primer lugar, la respuesta obtenida por este método debe ser la coincidencia más larga.

Obviamente, este enfoque se puede optimizar en el pretratamiento, aunque el segundo paso Biy A_jla coincidencia falla, lo anterior existe una longitud de cadena coincidente exitosa del j-1, excepto el prefijo respuesta menor que 2, la coincidencia final de la A resultante. el último carácter debe coincidir con el sufijo de esta cadena j-1,

Entonces podemos preprocesar el prefijo más largo de A que pueda coincidir con el sufijo de cada prefijo de la cadena A (algo así como la raíz de la pregunta), de modo que podamos ahorrar  el tiempo de hacer coincidir  A_1 ~  A_j y  B_ {i-j + 1} ~  Bi:

Vamos a fallar [i]denotar el prefijo más largo que puede coincidir con el sufijo del prefijo i de A, tales como:

Entonces podemos estar seguros: cada vez que falla la coincidencia, j puede saltar a la posición más cercana  fallar [j], porque se ha garantizado que la A1~ anterior A_jcoincidirá, solo es necesario comparar A_ {j + 1}y Bi:

Código específico:

for(int i=1,j=0;i<=m;i++){
	while(j>0&&a[j+1]!=b[i])j=fail[j];
	if(a[j+1]==b[i])ans[i]=++j;//配得长度为j+1
}

Dado que las definiciones se ven muy similares, el método de preprocesamiento falla es en realidad el mismo que el de emparejar (consulte el código para obtener más detalles):

for(int i=2,j=0;i<=n;i++){  //fail从2开始求
	while(j>0&&a[j+1]!=a[i])j=fail[j];
	if(a[j+1]==a[i])fail[i]=++j;
}

¿Por qué es la complejidad Sobre)? Mucha gente tiene esta duda al aprender KMP, después de mucho tiempo, puede que se olviden de pensar que KMP debe traer un log y así sucesivamente y no se atrevan a usarlo;

De hecho, el intervalo de subcadena de B obtenido por el algoritmo durante la coincidencia se desplaza continuamente hacia atrás. Específicamente, suponga que la subcadena de B previamente emparejada es l ~ r (l = ij, r = i-1),

 Cuando i y j coinciden con éxito, r se mueve hacia atrás;

 Cuando i y j no coinciden, j salta hacia adelante, es decir, l se mueve hacia atrás;

 r se mueve hacia atrás la mayoría de las | B |veces, y l se mueve hacia atrás la mayoría de las | B |veces, por lo que la complejidad total Sobre).

Manacher

El algoritmo de Manacher se utiliza para encontrar la subcadena palíndromo en una cadena.

La subcadena palíndromo, es decir, la subcadena simétrica en los lados izquierdo y derecho, es esencialmente un problema de coincidencia de cuerdas.

Sabemos que las cadenas de palíndromo se dividen en dos tipos, como aba y abba. La longitud par es esencialmente la misma que la longitud impar. Solo necesitamos insertar el mismo carácter en cada dos caracteres: # a # b # a #, # a # b # b # a #, cada cadena de palíndromo se centrará en un carácter.

Por lo tanto, al resolver tales problemas, se utiliza el siguiente procesamiento previo:

s[0]='$',s[1]='#';  //s[0]区别处理
while((s[n+1]=getchar())!='\n'&&s[n+1]>0)s[n+2]='#',n+=2;//每次输入加一个字符
s[n+1]=0;

Luego, comience con lo básico: encuentre el palíndromo más largo centrado en cada subíndice

Supongamos que existen las siguientes dos subcadenas palindrómicas (rojo y naranja):

Entonces se puede concluir que existe otro palíndromo (azul, simétrico a naranja):

Esto es obvio, porque los lados izquierdo y derecho son simétricos en la cuerda roja;

Por lo tanto, la idea del algoritmo es muy obvia: para cada subíndice i, guarde la cadena del palíndromo (l, mid, r) (mid <i≤r) cuyo centro está a la izquierda y el extremo derecho está más a la derecha de antemano, entonces, dado que el subíndice es anterior, se ha obtenido la cadena del palíndromo para todos los puntos, y la respuesta preliminar se puede obtener presionando directamente la simetría media;

Pero para esta situación:

Dado que r 'está fuera de la cuerda roja y no se puede garantizar que sea simétrico a la izquierda, tenemos que retraer r'to r y luego ensancharlo violentamente;

Si r 'está en la cuerda roja, entonces no debe ensancharse, porque se ha determinado la longitud del palíndromo simétrico pasado.

El código tiene este aspecto:

for(int i=1,r=0,mid=0;i<=n;i++){
	if(i<=r)mnc[i]=min(r-i+1,mnc[(mid<<1)-i]);
	while(s[i+mnc[i]]==s[i-mnc[i]])mnc[i]++;
	if(i+mnc[i]-1>r)r=i+mnc[i]-1,mid=i;//mnc[i]-1为i处的真实回文串长度
	ans=max(ans,mnc[i]-1); //求最长回文串
}

Complejidad: Dado que la r del palíndromo guardado solo se expandirá hacia la derecha, la complejidad total es Sobre).

EX (extensión) KMP

El KMP extendido se utiliza para resolver otro tipo de problema de búsqueda de prefijo: encontrar el prefijo más largo de A que pueda coincidir con el prefijo de cada sufijo de la cadena B.

El problema se parece mucho al problema de KMP, pero en realidad es más simple.

Aquí solo resolvemos un subproblema: encuentre el prefijo más largo de A que pueda coincidir con el prefijo de cada sufijo de la cadena A, es decir, la longitud de l de cada lugar de la cadena B es la misma que el prefijo de la cadena A comienzo de la cuerda Si con l Para maximizar l.

De la explicación del algoritmo KMP anterior, sabemos que después de resolver este subproblema, cualquiera que piense un poco sabe cómo luchar a continuación ...

Deje que e [i]denotan la longitud coincidencia más larga desde la posición i;

Suponiendo que la enumeración actual alcanza la posición i-ésima, y ​​hay una j antes de la posición i, que satisface el r = j + e [j] -1máximo,

Si r <i, pida directamente violencia e [i]y luego asigne j a i;

Si r≥i: (El subíndice comienza desde 0, que es diferente al anterior) Es fácil de encontrar,

Si e [ij] <r-i + 1, entonces e [i]debe ser igual e [ij], porque las dos cadenas marcadas con rojo son exactamente iguales y e [ij]no se pueden expandir hacia la derecha, tampoco e [i]debe expandirse;

Si e [ij] \ geq r-i + 1, entonces solo se puede garantizar el éxito de la coincidencia dentro de i ~ r, pero no después de r, así que expanda violentamente de r en adelante.

El código es muy corto:

for(int i=1,j=0,r=0;i<n;i++){
	if(i<=r)e[i]=min(e[i-j],r-i+1);
	while(A[i+e[i]]==A[e[i]])e[i]++;
	if(i+e[i]-1>r)r=i+e[i]-1,j=i;
}
for(int i=0,j=0,r=0;i<m;i++){
	if(i<=r)ans[i]=min(e[i-j],r-i+1);
	while(B[i+ans[i]]==A[ans[i]])ans[i]++;
	if(i+ans[i]-1>r)r=i+ans[i]-1,j=i;
}

La exactitud de esto es obvia, y luego considere la complejidad;

Cada ensanchamiento violento debe retroceder hasta n veces, por lo que la complejidad total Sobre).

posdata

Aunque escribí durante mucho tiempo y pensé en ello durante mucho tiempo, todavía siento que es muy complicado. Si no lo entiende, simplemente arreglárselas con el código.

Supongo que te gusta

Origin blog.csdn.net/weixin_43960287/article/details/111058741
Recomendado
Clasificación