lgoritmos 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 (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 i=1 X i=2 n a n X i=3 X i=4 n a n o i=5 X i=6 n X i=7 X i=8 X i=9 n X i=10 X
utómata Es posible construir un autómata en base al patrón para hacer búsquedas n a n o j=0 j=1 j=2 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
utómata Es posible construir un autómata en base al patrón para hacer búsquedas n a n o j=0 j=1 j=2 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
utómata Es posible construir un autómata en base al patrón para hacer búsquedas n a n o j=0 j=1 j=2 j=3 j=4 En el estado 0 siempre se avanza de letra
utómata Es posible construir un autómata en base al patrón para hacer búsquedas n a n o j=0 j=1 j=2 j=3 j=4 B N N N B N
utómata Es posible construir un autómata en base al patrón para hacer búsquedas n a n a j=0 j=1 j=2 j=3 j=4 N N N
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 ecorrer este autómata es de (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)
eset 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), dónde debemos saltar?
eset Por ejemplo, si P= aabaab y ya hemos reconocido 5 caracteres (aabaa), dónde debemos saltar? El sufijo más largo de esos 5 caracteres que también es prefijo de P, sería aa aabaa aabaab
eset Supongamos P=B 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)
eset Supongamos P=B 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
eset 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; }
eset 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 (M)
eset tros ejemplos P=B
eset tros ejemplos P=BCBC
eset tros ejemplos P=BXYZXYZ
KMP Finalmente KMP requiere de (M)+(N)=(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: B C D Y Z struct NodoTrie{ NodoTrie * hijo[26]; bool espalabra; };
Tries Insertar CS C S El atributo espalabra indica si en ese nodo termina una palabra
Tries Insertar C C S El atributo espalabra indica si en ese nodo termina una palabra
Tries Insertar DE C D E S El atributo espalabra indica si en ese nodo termina una palabra
Tries Insertar DED C D E S D El atributo espalabra indica si en ese nodo termina una palabra
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 C C D E S D
Tries Buscar C C D E S D
Tries Buscar C C D E S D
Tries Buscar C C D E S D El apuntador es nil
Tries Buscar C C D E S D
Tries Buscar C C D E S D
Tries Buscar C C D E S D El apuntador no nil pero espalabra es falso
Tries bool buscar(char *w, Trie * T){ if(*w==null) return T->esPalabra; if(t->hijo[*w- ]==NULL) return false; return buscar(w+1, T->hijo[*w- ]); }
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 CS Los posibles sufijos son: CS S S
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 CS, C, S, SP
Trie de Sufijos CS: CS,S,S, C S S S
Trie de Sufijos C: C,,,, C S S S
Trie de Sufijos S: S, S,, C S S S
Trie de Sufijos SP: SP, P, P, C P S P S S P
Trie de Sufijos hora podemos buscar cualquier substring de cualquier palabra del diccionario muy fácilmente ecordemos 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 C, CS, SP, S. Buscar el substr S C P S P S S P
Trie de Sufijos C, CS, SP, S. Buscar el substr C P S P S S P
Trie de Sufijos C, CS, SP, S. Buscar el substr C P S P S S P
Á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 SCII 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: GTGC$ Sufijos: i Sufijo 0 GTGC$ 1 TGC$ 2 TGC$ 3 GC$ 4 GC$ 5 C$ 6 C$ 7 $ 8 $
Árbol de Sufijos i En cada nodo hoja del suffix tree se almacena el índice del sufijo que se encuentra ahí Sufijo 0 GTGC$ 1 TGC$ 2 TGC$ 3 GC$ 4 GC$ 5 C$ 6 C$ 7 $ 8 $ 7 5 $ 3 1 $ 8 6 2 4 0
Á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 CS S C S 3 $ 4 $ 0 2 S 1
plicaciones del Suffix Tree Substring en (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 7 $ 6 2 4 5 3 1 0 Buscar el patrón
plicaciones del Suffix Tree Encontrar el substring más largo repetido en (n) 8 7 $ 6 2 4 5 3 1 0
plicaciones del Suffix Tree String común más largo en (N): construir el suffix tree de ambos strings en interceptarlos Ejemplo: SP vs TP
rreglo 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
rreglo 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= GTGC$, n=9 El arreglo de sufijos asociado sería {8,7,5,3,1,6,4,0,2}
rreglo de Sufijos i Sufijo 0 GTGC$ 1 TGC$ 2 TGC$ 3 GC$ 4 GC$ 5 C$ 6 C$ 7 $ 8 $ rdenar i sa[i] Sufijo 0 8 $ 1 7 $ 2 5 C$ 3 3 GC$ 4 1 TGC$ 5 6 C$ 6 4 GC$ 7 0 GTGC$ 8 2 TGC$ El arreglo de sufijos asociado sería {8,7,5,3,1,6,4,0,2}
rreglo de Sufijos Si hacemos un recorrido en preorden del suffix tree obtenemos el suffix array 8 7 $ 6 2 4 5 3 1 0 {8,7,5,3,1,6,4,0,2}
rreglo de Sufijos Un nodo interno del suffix tree corresponde a un rango en el suffix array 8 7 $ 6 2 4 5 3 1 0 {8,7,5,3,1,6,4,0,2} {8,7,5,3,1,6,4,0,2}
rreglo de Sufijos Un nodo hoja del suffix tree corresponde a una entrada simple del suffix array 8 7 $ 6 2 4 5 3 1 0
rreglo de Sufijos Primera implementación char T[MX_N]; int S[MX_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++) S[i] = i; sort(s, S+n, cmp);
rreglo de Sufijos En tiempo requiere (N 2 log N) Esto puede mejorarse ordenando los sufijos caracter por caracter (usando un ordenamiento lineal basado en radix-sort) (N log N) Duplicar en cada paso el tamaño del sub string a comparar
rreglo de Sufijos GTGC$ (0) TGC$ (1) TGC$ (2) GC$ (3) GC$ (4) C$ (5) C$ (6) $ (7) $ (8) rdenar según el primer caracter $ (8) TGC$ (1) GC$ (3) C$ (5) $ (7) C$ (6) GTGC$ (0) TGC$ (2) GC$ (4)
rreglo de Sufijos $ (8) TGC$ (1) GC$ (3) C$ (5) $ (7) C$ (6) GTGC$ (0) TGC$ (2) GC$ (4) rdenar según dos primeros caracteres $ (8) $ (7) C$ (5) GC$ (3) TGC$ (1) C$ (6) GC$ (4) GTGC$ (0) TGC$ (2)
rreglo de Sufijos $ (8) $ (7) C$ (5) GC$ (3) TGC$ (1) C$ (6) GC$ (4) GTGC$ (0) TGC$ (2) rdenar según 4 primeros caracteres $ (8) $ (7) C$ (5) GC$ (3) TGC$ (1) C$ (6) GC$ (4) GTGC$ (0) TGC$ (2)
rreglo de Sufijos $ (8) $ (7) C$ (5) GC$ (3) TGC$ (1) C$ (6) GC$ (4) GTGC$ (0) TGC$ (2) rdenar según 8 primeros caracteres $ (8) $ (7) C$ (5) GC$ (3) TGC$ (1) C$ (6) GC$ (4) GTGC$ (0) TGC$ (2)
rreglo de Sufijos Búsqueda de substring: hacer búsqueda binaria $ (8) $ (7) C$ (5) GC$ (3) TGC$ (1) C$ (6) GC$ (4) GTGC$ (0) TGC$ (2) Buscar: GC li = 0 ls = 8 m = 4 T[S[m]] = TGC$
rreglo de Sufijos Búsqueda de substring: hacer búsqueda binaria $ (8) $ (7) C$ (5) GC$ (3) TGC$ (1) C$ (6) GC$ (4) GTGC$ (0) TGC$ (2) Buscar: GC li = 4 ls = 8 m = 6 T[S[m]] = GC$
rreglo de Sufijos Contar apariciones de un substring: hacer búsqueda binaria lower bound y upper bound para buscar en donde está Palabra: CLBL Contar apariciones de: L $ $ BL$ L$ LBL$ BL$ CLBL$ L$ LBL$
rreglo de Sufijos Contar apariciones de un substring: hacer búsqueda binaria lower bound y upper bound para buscar en donde está Palabra: CLBL Contar apariciones de: L $ $ BL$ L$ LBL$ BL$ CLBL$ L$ LBL$
rreglo 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] lgoritmo trivial de (N 2 ) Puede hacerse en (N)
rreglo de Sufijos i sa[i] Sufijo lcp[i] 0 8 $ 0 1 7 $ 0 2 5 C$ 1 3 3 GC$ 1 4 1 TGC$ 1 5 6 C$ 0 6 4 GC$ 0 7 0 GTGC$ 2 8 2 TGC$ 0
rreglo de Sufijos Un grupo de elementos consecutivos con el mismo lcp (mayor a cero) equivalen a un nodo interno del suffix tree 8 7 $ 6 2 4 5 3 1 0
Substring repetido más largo Supongamos el lcp siguiente: rreglo de Sufijos 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
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 rreglo de Sufijos