Algoritmos de Strings Héctor Navarro Substrings • Dado un string T (posiblemente muy grande) y un patrón P (de tamaño menor), encontrar la primera (o todas) las apariciones de P en T • Solución trivial de O(NM) • N es el tamaño de T • M es el tamaño de P Substrings for (i=0; T[i] != '\0'; i++) { for (j=0; T[i+j] != '\0' && P[j] != '\0' && T[i+j]==P[j]; j++) ; if (P[j] == '\0') // found a match } Substrings • Peor caso: T=“aaaa…a” P=“aaa…ab” • Normalmente no se comporta tan mal ya que se descartan rápidamente en las primeras iteraciones Substrings P = “nano” i 0 1 2 3 4 5 6 7 8 9 10 11 T b a n a n a n o b a n o i=0 X n a n X n a n o n X n X i=1 i=2 i=3 i=4 i=5 i=6 i=7 i=8 i=9 i=10 X X X X X X Autómata • Es posible construir un autómata en base al patrón para hacer búsquedas n j=0 n a j=1 j=2 o j=3 j=4 En cada estado existen dos posibilidades: La letra actual coincide con la letra reconocida en ese estado: avanzar al siguiente estado y letra Autómata • Es posible construir un autómata en base al patrón para hacer búsquedas n j=0 n a j=1 j=2 o j=3 j=4 En cada estado existen dos posibilidades: La letra actual no coincide con la letra reconocida en ese estado: avanzar al siguiente estado, no avanzar la letra Autómata • Es posible construir un autómata en base al patrón para hacer búsquedas n j=0 n a j=1 j=2 o j=3 En el estado 0 siempre se avanza de letra j=4 Autómata • Es posible construir un autómata en base al patrón para hacer búsquedas n j=0 n a j=1 BANANANOBANO j=2 o j=3 j=4 Autómata • Es posible construir un autómata en base al patrón para hacer búsquedas n j=0 NANANA n a j=1 j=2 a j=3 j=4 KMP – Knuth Morris Pratt • Convertir el autómata en código j = 0; for (i = 0; i < n; i++) while(1) { if (T[i] == P[j]) { // matches? j++; // yes, move on to next state if (j == m) { // match! j = reset[j];// ya reconocimos algunos caracteres de P } break; } else if (j == 0) break; // no match (j==0), next T else j = reset[j]; // no match (j!=0), // shorter partial match } KMP – Knuth Morris Pratt • Recorrer este autómata es de O(N), ya que en cada iteración se inspecciona un solo carácter de T • El while más interno sólo se puede ejecutar en el peor caso una cantidad de veces igual al estado en el que estemos actualmente (M estados), pero una vez que se hace esto, regresamos al estado 0 (costo amortizado) KMP – Knuth Morris Pratt • Falta ver cómo se realiza la construcción del arreglo de transiciones de estados (reset) Reset • reset[i] indica el estado al que hay que saltar una vez que ya se han reconocido i caracteres y hay una falla • Por ejemplo, si P=“aabaab” y ya hemos reconocido 5 caracteres (aabaa), ¿A dónde debemos saltar? Reset • Por ejemplo, si P=“aabaab” y ya hemos reconocido 5 caracteres (aabaa), ¿A dónde debemos saltar? • El sufijo más largo de esos 5 caracteres que también es prefijo de P, sería aa aabaab aabaa Reset • • • • Supongamos P=AAABAA reset[0]=0 (aunque nunca se usa) reset[1]=0 Para reset[2] basta ver que P[2-1]=P[0], por lo tanto, si hay una falla en este punto debemos regresar al estado 1 (reset[2]=1) Reset • Supongamos P=AAABAA • Para reset[3] basta ver que P[3-1]=P[1], por lo tanto, si hay una falla en este punto debemos regresar al estado 2 (reset[3]=2) • Para reset[4], P[4-1]!=P[2], esto significa que cuando estamos en el estado 4 y no viene una B, debemos regresar al estado indicado por reset[3] (estado 2) • Pero como P[3]!=P[1], este estado tampoco sirve. El único estado que sirve es el estado 0 Reset int i = 0, j = -1; reset[0] = -1; while(i<m){ while(j>=0 && P[i]!=P[j]) j = reset[j]; i++; j++; reset[i] = j; } Reset • En cada iteración se avanza un caracter de P • El while más interno se hace a lo sumo tantas veces como caracteres hayamos procesado hasta ahora. Pero una vez que esto se hace, regresamos a j=0, por lo que el while interno se hará más corto en las siguientes iteraciones • Esta parte del algoritmo es entonces de O(M) Reset • Otros ejemplos • P=AAABAA Reset • Otros ejemplos • P=ABCABC Reset • Otros ejemplos • P=ABXYZXYZ KMP • Finalmente KMP requiere de O(M)+O(N)=O(N+M) • N tamaño del string en donde queremos hacer la búsqueda • M tamaño del substring que estamos buscando Boyer-Moore • También está basado en un autómata para hacer eficiente la búsqueda • En lugar de hacer matching en los primeros caracteres del patrón de búsqueda, se hace en los últimos, acelerando el procesamiento • Mientras el patrón sea más grande, el algoritmo se ejecutará más rápidamente Tries • Un Trie es un árbol en donde se almacenan palabras para encontrarlas rápidamente • Supongamos que las palabras están formadas por caracteres de un alfabeto con cardinalidad n • Cada nodo del Trie puede tener n hijos Tries • Por ejemplo, si las palabras pueden estar formadas por letras mayúsculas en inglés (26 caracteres), un nodo se ve como esto: A B C D struct NodoTrie{ NodoTrie * hijo[26]; bool esPalabra; }; … Y Z Tries • Insertar “CASA” C A S A El atributo esPalabra indica si en ese nodo termina una palabra Tries • Insertar “CARRO” C A R R S A O El atributo esPalabra indica si en ese nodo termina una palabra Tries • Insertar “DE” C D A E R R S A O El atributo esPalabra indica si en ese nodo termina una palabra Tries • Insertar “DEDO” C D A E R R S D A O El atributo esPalabra indica si en ese nodo termina una palabra O Tries • Búsqueda – Seguir los enlaces dependiendo del valor de la letra actual – Si un enlace es nil, la palabra no está en el Trie – Si al terminar la palabra llegamos a un nodo con esPalabra en falso, la palabra no está en el Trie – Si al terminar la palabra llegamos a un nodo con esPalabra en true, la palabra está en el Trie Tries • Buscar “CARA” C D A E R R O S A D O Tries • Buscar “CARA” C D A E R R O S A D O Tries • Buscar “CARA” C D A E R R O S A D O Tries • Buscar “CARA” C D A E R A R O El apuntador es nil S A D O Tries • Buscar “CA” C D A E R A R O S A D O Tries • Buscar “CA” C D A E R A R O S A D O Tries • Buscar “CA” C D A E R A R S A O El apuntador no nil pero esPalabra es falso D O Tries bool buscar(char *w, Trie * T){ if(*w==NULL) return T->esPalabra; if(T->hijo[*w-’A’]==NULL) return false; return buscar(w+1, T->hijo[*w-’A’]); } Trie de Sufijos • Sirve para almacenar todos los sufijos de varios strings con el fin de poder recuperarlos rápidamente • Con un Trie podemos rápidamente saber si una palabra está almacenada • Con un Suffix Trie podemos rápidamente saber si algún substring está almacenado Trie de Sufijos • Supongamos la palabra “CASA” • Los posibles sufijos son: – CASA – ASA – SA –A Trie de Sufijos • Si queremos agregar una palabra a un Suffix Trie, agregamos cada uno de sus sufijos (es posible que ya exista) • Por ejemplo, supongamos las palabras CASA, CARRO, ASAR, SAPO Trie de Sufijos • CASA: CASA,ASA,SA,A A C S S A A S A A Trie de Sufijos • CARRO: CARRO,ARRO,RRO,RO,O A C R R O O A S A R O S R R O S R A O A Trie de Sufijos • ASAR: ASAR, SAR, AR, R A C R O A S R A O R R O S R R O S R A O R A Trie de Sufijos • SAPO: SAPO, APO, PO, O A O C P O R R A O A S R O S R R O P R S O R A O A P O R Trie de Sufijos • Ahora podemos buscar cualquier substring de cualquier palabra del diccionario muy fácilmente • Recordemos que el Trie permite reconocer prefijos del string • Como almacenamos los sufijos de los strings, podemos reconocer cualquier prefijo de cualquier sufijo • Un substring es justamente eso, un prefijo de un sufijo Trie de Sufijos • CARRO, CASA, SAPO, ASAR. Buscar el substr SAR A O C P O R R A O A S R O S R R O P R S O R A O A P O R Trie de Sufijos • CARRO, CASA, SAPO, ASAR. Buscar el substr RRO A O C P O R R A O A S R O S R R O P R S O R A O A P O R Trie de Sufijos • CARRO, CASA, SAPO, ASAR. Buscar el substr AR A O C P O R R A O A S R O S R R O P R S O R A O A P O R Árbol de Sufijos • Sirve para almacenar eficientemente todos los sufijos de un solo string • Usaremos un carácter especial para marcar el final del string ($), cuyo código ASCII sea menor que el del resto de los caracteres del string • El sufijo vacío debe estar en el suffix tree Árbol de Sufijos • Palabra: GATAGACA$ • Sufijos: i Sufijo 0 GATAGACA$ 1 ATAGACA$ 2 TAGACA$ 3 AGACA$ 4 GACA$ 5 ACA$ 6 CA$ 7 A$ 8 $ Árbol de Sufijos • En cada nodo hoja del suffix tree se almacena el índice del sufijo que se encuentra ahí i Sufijo 0 GATAGACA$ 1 ATAGACA$ 2 TAGACA$ 3 AGACA$ 4 GACA$ 5 ACA$ 6 CA$ 7 A$ 8 $ $ 8 A 6 $ 2 4 7 5 0 3 1 Árbol de Sufijos • Hay muchos substrings que se repiten (vértices repetidos) • Un suffix tree se puede crear a partir de un suffix trie uniendo los nodos consecutivos que tienen un solo hijo Árbol de Sufijos • CASA $ A C 4 S S A A S A A A 2 $ 0 3 1 Aplicaciones del Suffix Tree • Substring en O(m+occ): m es el tamaño del patrón de búsqueda y occ es el número de ocurrencias del patrón en T 8 A 6 $ 2 4 7 5 0 3 1 Buscar el patrón “A” Aplicaciones del Suffix Tree • Encontrar el substring más largo repetido en O(n) 8 A 6 $ 2 4 7 5 0 3 1 Aplicaciones del Suffix Tree • String común más largo en O(N): construir el suffix tree de ambos strings en interceptarlos Ejemplo: SAPO vs TRAPO Arreglo de Sufijos • La construcción del suffix tree es muy compleja • El suffix array tiene funcionalidades parecidas al suffix tree pero es más fácil de construir y de usar Arreglo de Sufijos • Un arreglo de sufijos es un arreglo de enteros que almacena una permutación de los índices de los sufijos ordenados • Por ejemplo, con T=“GATAGACA$”, n=9 • El arreglo de sufijos asociado sería {8,7,5,3,1,6,4,0,2} Arreglo de Sufijos i Sufijo i sa[i] Sufijo 0 GATAGACA$ 0 8 $ 1 ATAGACA$ 1 7 A$ 2 5 ACA$ 3 AGACA$ 3 3 AGACA$ 4 GACA$ 4 1 ATAGACA$ 5 ACA$ 5 6 CA$ 6 CA$ 6 4 GACA$ 7 A$ 7 0 GATAGACA$ 8 $ 8 2 TAGACA$ 2 TAGACA$ Ordenar El arreglo de sufijos asociado sería {8,7,5,3,1,6,4,0,2} Arreglo de Sufijos • Si hacemos un recorrido en preorden del suffix tree obtenemos el suffix array 8 A 6 $ 2 4 7 5 0 3 1 {8,7,5,3,1,6,4,0,2} Arreglo de Sufijos • Un nodo interno del suffix tree corresponde a un rango en el suffix array 8 A 6 $ 2 4 7 5 0 3 1 {8,7,5,3,1,6,4,0,2} {8,7,5,3,1,6,4,0,2} Arreglo de Sufijos • Un nodo hoja del suffix tree corresponde a una entrada simple del suffix array 8 A 6 $ 2 4 7 5 0 3 1 Arreglo de Sufijos • Primera implementación char T[MAX_N]; int SA[MAX_N], i, n; bool cmp(int a, int b){ return strcmp(T+a,T+b)<0; } … n = strlen(T); for(int i=0; i<n; i++) SA[i] = i; sort(SA, SA+n, cmp); Arreglo de Sufijos • En tiempo requiere O(N2 log N) • Esto puede mejorarse ordenando los sufijos caracter por caracter (usando un ordenamiento lineal basado en radix-sort) • O(N log N) • Duplicar en cada paso el tamaño del sub string a comparar Arreglo de Sufijos GATAGACA$ (0) ATAGACA$ (1) TAGACA$ (2) AGACA$ (3) GACA$ (4) ACA$ (5) CA$ (6) A$ (7) $ (8) Ordenar según el primer caracter $ (8) ATAGACA$ (1) AGACA$ (3) ACA$ (5) A$ (7) CA$ (6) GATAGACA$ (0) TAGACA$ (2) GACA$ (4) Arreglo de Sufijos $ (8) ATAGACA$ (1) Ordenar AGACA$ (3) según dos primeros ACA$ (5) caracteres A$ (7) CA$ (6) GATAGACA$ (0) TAGACA$ (2) GACA$ (4) $ (8) A$ (7) ACA$ (5) AGACA$ (3) ATAGACA$ (1) CA$ (6) GACA$ (4) GATAGACA$ (0) TAGACA$ (2) Arreglo de Sufijos $ (8) A$ (7) Ordenar ACA$ (5) según 4 AGACA$ (3) primeros caracteres ATAGACA$ (1) CA$ (6) GACA$ (4) GATAGACA$ (0) TAGACA$ (2) $ (8) A$ (7) ACA$ (5) AGACA$ (3) ATAGACA$ (1) CA$ (6) GACA$ (4) GATAGACA$ (0) TAGACA$ (2) Arreglo de Sufijos $ (8) A$ (7) Ordenar ACA$ (5) según 8 AGACA$ (3) primeros caracteres ATAGACA$ (1) CA$ (6) GACA$ (4) GATAGACA$ (0) TAGACA$ (2) $ (8) A$ (7) ACA$ (5) AGACA$ (3) ATAGACA$ (1) CA$ (6) GACA$ (4) GATAGACA$ (0) TAGACA$ (2) Arreglo de Sufijos • Búsqueda de substring: hacer búsqueda binaria $ (8) A$ (7) ACA$ (5) AGACA$ (3) ATAGACA$ (1) CA$ (6) GACA$ (4) GATAGACA$ (0) TAGACA$ (2) Buscar: GAC li = 0 ls = 8 m=4 T[S[m]] = ATAGACA$ Arreglo de Sufijos • Búsqueda de substring: hacer búsqueda binaria $ (8) A$ (7) ACA$ (5) AGACA$ (3) ATAGACA$ (1) CA$ (6) GACA$ (4) GATAGACA$ (0) TAGACA$ (2) Buscar: GAC li = 4 ls = 8 m=6 T[S[m]] = GACA$ Arreglo de Sufijos • Contar apariciones de un substring: hacer búsqueda binaria lower bound y upper bound para buscar en donde está Palabra: CALABALA $ A$ ABALA$ ALA$ ALABALA$ BALA$ CALABALA$ LA$ LABALA$ Contar apariciones de: ALA Arreglo de Sufijos • Contar apariciones de un substring: hacer búsqueda binaria lower bound y upper bound para buscar en donde está Palabra: CALABALA $ A$ ABALA$ ALA$ ALABALA$ BALA$ CALABALA$ LA$ LABALA$ Contar apariciones de: ALA Arreglo de Sufijos • Longest Common Prefix (LCP): el prefijo común más largo entre pares consecutivos de sufijos • lcp[0] = 0 • De ahí en adelante lcp[i] almacena el prefijo común más largo entre el s[i] y s[i-1] • Algoritmo trivial de O(N2) • Puede hacerse en O(N) Arreglo de Sufijos i sa[i] Sufijo lcp[i] 0 8 $ 0 1 7 A$ 0 2 5 ACA$ 1 3 3 AGACA$ 1 4 1 ATAGACA$ 1 5 6 CA$ 0 6 4 GACA$ 0 7 0 GATAGACA$ 2 8 2 TAGACA$ 0 Arreglo de Sufijos • Un grupo de elementos consecutivos con el mismo lcp (mayor a cero) equivalen a un nodo interno del suffix tree 8 A 6 $ 2 4 7 5 0 3 1 Arreglo de Sufijos • Substring repetido más largo • Supongamos el lcp siguiente: i lcp[i] 0 0 1 0 2 0 3 3 4 3 5 2 6 0 7 1 8 1 9 1 10 0 Arreglo de Sufijos • Substring que más se repite i lcp[i] 0 0 1 0 2 0 3 3 4 3 5 2 6 0 7 1 8 1 9 1 10 0