Algoritmos de Strings

Anuncio
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
Descargar