You are on page 1of 19

Aho-Corasick e Bioinformatica

Alessandro Arzilli (arzilli@cli.di.unipi.it),


Matteo Raimondi (raimondi@cli.di.unipi.it),
Guido Scatena (scatena@cli.di.unipi.it),
Giorgio Zoppi (zoppi@cli.di.unipi.it)
13th December 2005

1
Contents
1 Introduzione al Pattern Matching. 3
2 Keyword Tree 4
2.1 Caratteristiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.2 Costruzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
2.3 Ricerca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5

3 Algoritmo di Aho-Corasick 5
3.1 Automa di Aho-Corasick . . . . . . . . . . . . . . . . . . . . . . . 5
3.2 Ricerca . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3.2.1 Complessità della ricerca . . . . . . . . . . . . . . . . . . 7
3.3 Costruzione dell'automa di Aho-Corasick . . . . . . . . . . . . . 8
3.3.1 Prima fase: funzione goto . . . . . . . . . . . . . . . . . . 8
3.3.2 Seconda fase: costruzione della funzione fail . . . . . . . . 8
3.3.3 Pseudocodice . . . . . . . . . . . . . . . . . . . . . . . . . 9
3.3.4 Complessità . . . . . . . . . . . . . . . . . . . . . . . . . . 10

4 Aho-Corasick con caratteri jolly 12


4.1 Descrizione dell'algoritmo . . . . . . . . . . . . . . . . . . . . . . 12
4.2 Esempio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
4.3 Complessità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

5 Baeza-Yates & Perleberg (BYP) 15


5.1 Introduzione al problema del Bounded Matching . . . . . . . . . 15
5.1.1 Algoritmo di allineamento e Bounded Matching . . . . . . 15
5.2 Algoritmo di Baeza-Yates & Perleberg . . . . . . . . . . . . . . . 15
5.3 Complessità . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

6 Bioinformatica 16
6.1 Sequence Tagged Sites . . . . . . . . . . . . . . . . . . . . . . . . 16
6.2 Simple Sequence Repeats e TROLL . . . . . . . . . . . . . . . . . 16
6.2.1 TROLL . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
6.3 Motivi di Sequenza . . . . . . . . . . . . . . . . . . . . . . . . . . 17
6.4 Baeza-Yates & Perleberg (BYP) . . . . . . . . . . . . . . . . . . 18

2
1 Introduzione al Pattern Matching.
Un aspetto fondamentale della bioinformatica è lo studio delle informazioni
contenute nelle biosequenze. Queste sono costituite da stringhe di caratteri:
adenina (A), citosina (C), guanina (G) e timina (T) o uracile (U) ad sempio nel
caso del DNA o RNA.
Secondo il principio genetico del determinismo, è fattibile predire la funzione
di ogni gene nel genoma dalle relative informazioni di sequenza. Questo suppone
implicitamente che l'ambiente di ogni gene è inoltre calcolabile dalla sequenza
completa di genoma perché la funzione di una molecola può diventare espressiva
soltanto rispetto al relativo ambiente. Di conseguenza, le intere architetture
molecolari ed i processi di reazione molecolari in una cellula possono essere
calcolabile dalla sequenza genomica.
L'informatica ha il compito di considerare una varietà di problemi biologi-
camente importanti deniti soprattutto sulle sequenze, o sulle stringhe:
• ricostruzione delle serie lunghe di DNA dai suoi frammenti (fragment as-
sembly);
• determinazione di mappe siche e genetiche dai dati della sonda sotto i
vari protocolli sperimentali;
• immagazzinamento, richiamando e confrontando le stringhe del DNA;
• confronto di due o più stringhe per le somiglianze; cercando nelle basi di
dati le stringhe relative e le sottostringhe;
• denizione ed esplorazione di dierenti nozioni di stringhe relazionali;
• ricerca di modelli nuovi o mal deniti che occorrono frequentemente in
DNA;
• ricerca dei modelli strutturali in DNA e proteina;
Quando si determina una nuova sequenza, la prima operazione eettuata per
la sua caratterizzazione è la ricerca, all'interno di una banca dati, di sequenze
con un grado di similarità statisticamente signicativo rispetto alla sequenza in
esame. Questo prende il nome di pattern matching approssimato.
Ci concentreremo invece su un sottoproblema più semplice: il pattern match-
ing esatto su insiemi (exact set matching ). Il problema consiste nel localizzare
le occorrenze di più pattern appartenenti ad un insieme P = {P1 , . . . , Pz }in un
testo T [1, . . . , m].
Pk
Sia n = i=1 |Pi | il problema dell' Exact set matching può essere risolto con
un tempo O(|P1 | + m + . . . + |Pk | + m) = O(n + km) applicando un algoritmo
exact matching lineare una volta per ogni pattern dell'insieme (ovvero k volte).
Aho e Corasick furono tra i primi, intorno agli anni '80, ad arontare il
problema dello string matching esatto su un insieme di pattern.
L'algoritmo di Aho-Corasick ha complessità O(n+m+z), dove z è il numero
di occorrenze dei vari Pi in T. Per ottenere un tale risultato, serve una struttura
dati apposita.

3
Osservazione Per ottenere la complessità di P nell'ordine di O(n + m + z),
che implica una sola scansione del testo, bisognerà memorizzare gli elementi di
in modo eciente e "furbo". Nasce così una struttura dati chiamata keyword
tree (o trie ), che è una sorta di generalizzazione dei radix tree. L'algoritmo di
AC utilizzerà un trie per costruire un automa a stati niti che verrà utilizzato
per la ricerca, in modo simile all'algoritmo di Knuth-Morris-Pratt.
Per comprendere il funzionamento e la complessità dell'AC occorre prima
introdurre il keyword tree.

2 Keyword Tree
2.1 Caratteristiche
Un keyword tree (o trie ) per un insieme di pattern P è un albero K con le
seguenti caratteristiche:

• ogni arco è etichettato con un carattere


• due archi uscenti di un nodo hanno dierenti etichette

Si denisce l'etichetta di un nodo v come la concatenazione delle etichette sugli


archi che compongono il path dal nodo root a v, e si indica con L(v).

2.2 Costruzione
Indicate le caratteristiche del keyword tree passiamo ora all'analisi dei passi
necessari per la sua costruzione per l'insieme P = P1 , . . . , Pz .

1. Iniziare dal nodo radice

2. Inserire ogni pattern Pi , come segue:

3. Partendo dal nodo radice, seguire il cammino etichettato con i caratteri


di Pi

• Se il cammino nisce prima di Pi , si aggiunge un nuovo arco e un


nuovo nodo per ogni carattere rimanente di Pi .
• Alla ne salvare l'identicatore i di Pi nell'ultimo nodo del cammino

Come si può notare l'algoritmo di costruzione è caratterizzato da una comp-


lessità:
O(|P1 | + . . . + |Pk |) = O(n)
Per capirne meglio le caratteristiche vediamo un esempio di trie per P =
{he1 , she2 , his3 , hers4 }. La struttura che si ottiene è rappresentata in Figura 1.
Si può notare che per ogni p ∈ P c'è un nodo v con L(v) = p e l'etichetta
L(v) di ogni foglia v equivale a qualche p ∈ P .

4
Figure 1: Keyword Tree

2.3 Ricerca
L'ultimo aspetto che resta da analizzare per concludere la nostra breve panoram-
ica sulla struttura dati keyword tree è la ricerca di una stringa s.
La ricerca è caratterizzata dai seguenti passi:

• Partire dal nodo padre e seguire il cammino etichettato da caratteri di s


no a quando è possibile

 se il cammino si ferma in un nodo in cui è presente un identicatore


allora s è uno dei pattern cercati;
 altrimenti se termina prima di s, la stringa non è uno dei pattern
cercati

Come si può notare l'algotimo di ricerca è caratterizzato da una complessità


O(|s|).
Un'applicazione banale del trie al set pattern matching dovrebbe portare
a una complessità pari a Θ(nm). Per questo motivo estenderemo il keyword
tree in un automa che ci permetterà di realizzare l'exact set matching in tempo
lineare.

3 Algoritmo di Aho-Corasick
3.1 Automa di Aho-Corasick
Gli automi a stati niti possono esser considerati sia come modelli semplicati
di macchine che come meccanismi usati per specicare linguaggi, entrambi gli
aspetti sono utili al ne del problema del pattern matching. Diamo quindi una
denizione di automa a stati niti deterministico.

5
Un automa a stati niti deterministico è una quintupla A = hΣ, Q, g, q, F i,
dove Σ = {a1 , . . . , an } è l'alfabeto di input, Q = {q0 , . . . , qn } è un insieme nito
e non vuoto di stati, F ⊆ Q è l'insieme degli stati nali, q0 è lo stato iniziale
e g : Q × Σ 7−→ Q è la funzione di transizione che ad ogni coppia (carattere,
stato) associa uno stato successivo.
Assumiamo che la funzione di transizione non sia totale, ovvero il valore
g(q, a) è lo stato raggiunto dallo stato q mediante la transizione etichettata dal
carattere a, se presente. L'automa che a noi interessa è un automa di string
matching. Si parla di automa di string matching quando dato un pattern p,
tale automa Ap accetta l'insieme di tutte le parole contenenti p come susso.
Estendendo tale automa otterremmo l'automa di Aho-Corasick denito come
l'automa che accetta tutte le parole aventi una parola dell'insieme nito di
parole W come susso.
Detto questo siamo diamo allora la denizione dell'automa di Aho-Corasick.
L'automa viene denito mediante tre funzioni:

1. Sia q ∈ Q e a ∈ Σ si denisce la funzione goto g(q, a) da lo stato successivo


partendo dallo stato q facendo il matching del carattere a (funzione di
transizione). Se un arco (q, v) è etichettato da a, allora g(q, a) = v , altresi
g(0, a) = 0 per ogni a che non etichetta un arco fuori dalla radice. Inoltre
l'automa rimane nello stato iniziale se esamina caratteri non presenti nel
pattern, cioè g(q, a) = 0.

2. la funzione fail f : Q 7−→ Q. Il valore f (q), per q 6= 0 dà lo stato in


cui mi trovo quando si verica una mismatch. Diremo che f (q) è il nodo
etichettato dal più lungo susso proprio w di L(q) tale che w sia presso di
qualche pattern, per questo motivo una transizione della funzione failure
(fail transition) non manca nessuna potenziale occorrenza. Notiamo che
f(q) è sempre denita, poichè L(0) è un presso di ogni pattern.
3. La funzione output out(q) ci dà l'insieme dei patterm riconosciuti quando
entriamo in uno stato q. Gli stati dell'automa sono tipicamente rappre-
sentati mediante un trie (keyword tree), vediamo all'opera un esempio in
Figura 2, dove le linee tratteggiate sono le fail transition.

3.2 Ricerca
Vediamo ora come funziona la ricerca di un insieme di pattern all'interno di un
testo T [1...m], supponendo di aver gia inizializzato l'automa di AC. La funzione
di transizione può essere implementata mediante una tabella, che viene espressa
mediante una matrice. Devo:

• Partire dalla radice del trie ed eseguire fail transition nchè l'insieme resti-
tuito dalla funzione g non è vuoto per il carattere in esame.

• Eettuare una goto transition arrivando ad uno stato.

6
Figure 2: Transizioni di Failure

• Se tale stato ha una funzione di output non vuota, allora ho trovato un


match.

Vediamo lo pseudocodice in Python, dove g è la funzione di goto, f è la funzione


fail, out è la funzione di output:

q = 0 # stato iniziale root


# range(m+1) funzione che restituisce
# interi da 1 a m.
for i in range(1,m+1):
while (not Empty(g(q,T[i]))):
q =f(q); # segui un fallimento
q = g(q,T[i]); # segui un goto
if (not Empty(out(q))):
print i,out(q)

Visto l'algoritmo occorre vederne l'ecienza, che si dimostra mediante il seguente


teorema.

3.2.1 Complessità della ricerca


Teorema: La ricerca nel testo T [1...m] con un automa di Aho-Corasick si
eettua in O(m + z), dove z è il numero di occorrenze del pattern cercato.

Dimostrazione: Una volta costruito l'automa di AC l'algoritmo compie, per


ogni carattere di t letto, alcune transizioni di fallimento e esattamente una di
goto. Il numero complessivo di transizioni di goto sarà quinidi, banalmente,
O(m).
Il numero delle transizioni di fallimento, ovviamente, può essere al massimo
n ogni volta, più precisamente il massimo numero di transizioni di fallimento è
limitato alla profondità del nodo corrente che dipende dal numero di caratteri di
t letti no a questo momento; quindi, complessivamente, il numero di transizioni
di fallimento sarà, anch'esso, O(m).

7
A questo punto resta da considerare l'if alla ne del ciclo più interno, questo
sarà eseguito tante volte quante sono le occorrenze del pattern cercato, ovvero
z volte: la complessità della ricerca, quindi, è O(m + z).

3.3 Costruzione dell'automa di Aho-Corasick


3.3.1 Prima fase: funzione goto
Anché sia possibile eseguire la ricerca con l'algoritmo di AC è necessario che
l'automa di AC sia stato costruito, inizializzando correttamente le funzioni goto,
fail e output.
L'automa di AC è valido se e solo se l'algoritmo di ricerca, con tale automa,
indica che la keyword y termina in posizione i del testo x se e solo se x = uyv
e la lunghezza di uy è i.
La costruzione dell'automa, per un insieme di pattern ssato avviene in due
fasi:

• nella prima fase si determinano gli stati dell'automa e il grafo della fun-
zione goto

• nella seconda fase si denisce la funzione fail;

la costruzione della funzione output ha inizio nelle prima fase e si conclude nella
seconda fase.
La funzione goto e il suo grafo non sono altro che un keyword tree, o
trie, come già detto: basterà quindi eseguire l'algoritmo per la costruzione del
keyword tree sull'insieme di pattern.
Per completare la funzione goto, vengono creati dei cicli sullo stato iniziale
settando g(0,a) = 0 per ogni simbolo a appartenente all'alfabeto, diverso dai
caratteri iniziali delle keyword nell'insieme.

Esempio Vediamo come si comporta la prima fase della costruzione dell'au-


toma di Aho-Corasick con l'insieme di pattern P = {he, she, his, hers}: viene
creato il keyword tree aggiungendo i pattern come cammini orientati da 0 a
v, e settato out(v)=L(v) per i nodi destinazione dei cammini orientati. Inne
si creano dei cicli sulla radice per ogni carattere dell'alfabeto diverso da ogni
carattere iniziale di un pattern dell'insieme: in questo caso per ogni carattere
diverso da 'h' e 's'. Il risultato di questa prima fase è mostrato in Figura 3.

3.3.2 Seconda fase: costruzione della funzione fail


La funzione fail è calcolaa per i nodi del trie attraverso una visita in ampiezza
(BFS) in cui ci si occupa di completare anche la funzione output.
Si denisce la profondità di uno stato s nel trie, come la lunghezza del più
breve cammino dalla radice allo stato s.
La funzione fail è calcolata nel seguente modo:

8
Figura 3: Esempio di costruzione dell'automa di AC: risultato della prima fase

• vengono messi in coda i nodi a profondità 1 (ovvero i nodi della stella


uscente della radice) e vi si associa un valore di fail uguale a 0;

• il valore della funzione fail associato al generico nodo a profondità d è


calcolato sulla base dei valori della funzione di goto e di output dei nodi
a profondità minore

Per calcolare la fail di un nodo s a profondità d, si considera state = r, con r


il nodo di profondità (d − 1) predecessore di s; se g(f (state), a) 6= null, dove a
è l'etichetta dell'arco che congiunge i nodi r ed s, allora f (s) = g(f (state), a),
altrimenti si ripete questo controllo con state = f (state) nché non si riesce a
inizializzare f (s). Si noti che g(0, c) 6= null per ogni c appartenente all'alfabeto,
quindi il ciclo terminerà sempre. Intuitivamente il ciclo serve a trovare il nodo
più profondo state tale che L(state) sia un susso proprio di L(s) e g(state, a)
sia denita.
Una volta calcolata la funzione fail del generico nodo s l'algoritmo aggiornera
la funzione output unendo l'insieme di output di s con quello di f(s). Questo
viene fatto perché f (s) se esiste è un susso proprio (= non nullo) di L(s) e
dovrà quindi essere riconosciuto anche allo stato s.
In Figura 4 vengono mostrati i passi iniziali dell'algoritmo con in ingresso
l'automa costruito nella fase precedente per l'isieme di pattern P = {00 he00 ,00 she00 ,00 his00 ,00 hers00 }.
Viene anche fatta vedere la funzione fail risultante.

3.3.3 Pseudocodice
Dato che la prima fase della costruzione dell'automa di AC è banale mostreremo
solo lo pseudocodice della seconda fase:

9
Figura 4: Esempio di costruzione dell'automa di AC: primi passi della seconda
fase

Q = emptyQueue()
for ((a in Σ) and (g(0,a) 6= 0)):
f(g(0,a)) = 0
Q.add(s)
while not Empty(Q):
r = Q.remove()
for ((a in Σ) and (g(r,a) 6= 0)):
s = g(r,a)
Q.add(s)
state = f(r)
while (g(state,a) = None):
state = f(state) //(*)
f(s) = g(state,a)
out(s) = out(s) ∪ out(f(s))

3.3.4 Complessità
La costruzione del keyword tree è O(n), come abbiamo già mostrato. La sec-
onda fase, la costruzione della funzione fail, è una scansione in ampiezza del
keyword tree e, dunque, il corpo dell'iterazione verrà eseguito una sola volta
per ogni nodo. All'interno dell'iterazione abbiamo diverse istruzioni che possi-
amo ragionevolmente assumere O(1) ; i punti critici dell'algoritmo, che possono
far aumentare la complessità sono: il ciclo indicato con (*) per il calcolo della
funzione fail e l'aggiornamento delle funzioni di output.

Calcolo della funzione fail: supponiamo che l'inserimento del pattern p =


a1 a2 ...ak nel keyword tree abbia generato i nodi che chiameremo u1 , ...uk . In-
dicheremo la profondità di un nodo nel keyword tree (ovvero la sua distanza
dalla radice) con d(u) .

10
Dimostreremo induttivamente che per i = 1...k il numero di iterazioni della
riga (*) è proporzionale ad i:

• Caso Base: è evidente che per u1 la riga marcata non viene eseguita
(per i nodi tali che d(u) = 1 f (u) viene azzerata nell'inizializzazione della
scansione in ampiezza) e per u2 viene eseguita una sola volta.

• Induzione: supponiamo che per calcolare f (ui−1 ) siano state eseguite


x iterazioni di (*) allora avremo che d(f (ui−1 )) = (i − 2) − x, nel caso
peggiore, il calcolo di f (ui ) ne potrà richiedere al più (i−2)−x supponendo
che, anche per ui , ogni iterazione di (*) ci porti indietro verso la radice di
un solo livello (sempre il caso peggiore). L'inserimento di tutti i nodi no
ad i richiederà quindi: x + (i − 2) − x = i − 2 iterazioni di i.

Dunque l'inserimento dell'intero pattern lungo k richiede O(k) iterazioni della


riga (*) e quindi l'inserimento di tutti i pattern richiederà O(n) iterazioni di
(*).

Aggiornamento della funzione di output


out(u) = out(u) ∪ out(f(u))
L'unione di due insiemi non può essere eseguita, in generale, in tempo costante:
il problema principale è l'individuazione e il trattamento degli elementi comuni
ai due insiemi. Se però i due insiemi sono disgiunti possono essere rappresentati
come liste e la loro unione si riduce al concatenamento (ed è, quindi, eseguibile
in tempo costante).
Dimostreremo che i due insiemi: out(u) e out(f(u)), sono disgiunti perché
tutti gli elementi in out(f(u)) sono stringhe di lunghezza minore della stringa
più corta di out(u). Notiamo anzitutto che questa istruzione è all'interno di una
visita in ampiezza del keyword tree e quindi viene eseguita una sola volta per
ogni nodo del grafo, dunque out(u) è, alternativamente:

1. Vuoto: se u non è un nodo terminale di una parola del dizionario

2. Altrimenti contiene un solo elemento, corrispondente all'etichetta del nodo.

Dalla prima fase della costruzione dell'automa di AC è banalmente vericabile


che non è possibile che out(u) contenga più di un elemento prima dell'esecuzione
della seconda fase.
Nel caso 1 i due insiemi sono banalmente disgiunti. Nel secondo caso notiamo
che out(f(u)) può essere:

1. Vuoto: se f(u) non è un nodo terminale di una parola del dizionario e


nessun susso proprio dell'etichetta di f(u) è una parola del dizionario
(notare che siccome f(u) è più vicino alla radice rispetto ad u la sua fun-
zione di output è già stata aggiornata). In questo caso i due insiemi sono
banalmente disgiunti

11
2. Contenente un solo elemento: in questo caso l'elemento non sarà altro che
l'etichetta di f(u) ma essendo il nodo f(u) più vicino alla radice rispetto
ad u la sua etichetta deve essere più corta di quella del nodo u (per come
sono denite le etichette dei nodi), dunque out(u) = {L(u)} e out(f (u)) =
{L(f (u))} che sono disgiunti.
3. Se out(f(u)) contiene più di un elemento avremo che L(f (u)) ∈ out(f (u))
e gli elementi di out(f(u)) dierenti da L(f(u)) saranno stati inseriti es-
eguendo l'istruzione di aggiornamento qui discussa. Per questi elementi
vale, ricorsivamente, lo stessa proprietà che vale per gli elementi di out(u)
rispetto a quelli di out(f(u)) e avranno quindi lunghezza minore di L(f(u))
e quindi tutti gli elementi di out(f(u)) sono più corti di quello in out(u).

Riassumendo: la costruzione del keyword tree è banalmente lineare, la sec-


onda fase dell'algoritmo è una visita in ampiezza (lineare nel numero di nodi
del keyword tree) che contiene un ciclo che, sebbene possa sembrare anch'esso
lineare in n, per come viene usato nell'algoritmo il suo peso è ammortizzato ad
O(1). L'aggiornamento delle funzioni di output, anch'esso eseguito una volta per
ogni nodo, può essere implementato in tempo costante rappresentando l'insieme
out(u) con una lista dato che gli insiemi da unire sono sempre disgiunti e quindi
l'unione è il semplice concatenamento delle due liste. Quindi, complessivamente,
la costruzione dell'automa è lineare in n.

4 Aho-Corasick con caratteri jolly


L'algoritmo di Aho-Corasick può essere utilizzato, mediante una semplice esten-
sione, anche per risolvere il problema del pattern matching con caratteri jolly.
Un carattere jolly, nel seguito indicato con Φ, è un carattere tale che combacia
con qualsiasi altro carattere singolo. Dato un pattern P contenente caratteri
jolly, il problema del pattern matching con caratteri Jolly è denito come il
trovare tutte le occorrenze di P in un testo T.

4.1 Descrizione dell'algoritmo


Se il numero di caratteri Jolly è limitato da una costante, il pattern P con carat-
teri Jolly può essere trovato in tempo lineare, attraverso l'applicazione dell'algo-
ritmo di Aho-Corasick, semplicemente contando le occorrenze delle sottostringhe
massimali di P non contenenti caratteri Jolly.
A questo scopo è necessario denire un vettore C di contatori interi di
lunghezza uguale al numero di caratteri del testo T, e, dato A = {P1 , . . . Pz },
insieme delle sottostringhe massimali non contenenti caratteri Jolly, si denisce
anche un vettore L con la stessa cardinalità di P contenente le posizioni di
partenza delle sottostringhe Pi all'interno di P.
L'algoritmo di ricerca di pattern con caratteri Jolly è costituito di una fase
di preprocessing ed una fase di ricerca:

12
• La fase di preprocessing costruisce l'automa di Aho-Corasick per A ed
inizializza tutte le posizioni di C a 0.
• La fase di ricerca scandisce il testo T applicando Aho-Corasick con l'in-
sieme di pattern A (le sotto stringhe); quando viene trovato un pattern Pj
che termina in posizione i ≤ L [j] all'interno del testo T, viene incrementa-
to il contatore C [i − L [j] + 1] di un unità (contatore corrispondente alla
posizione di inizio di Pj nel testo).

Al termine della scansione di T ogni i tale che C [i] = z = |A| corrisponde alla
locazione di inizio di un occorrenza di P nel testo.

4.2 Esempio
Per fare pattern matching con caratteri Jolly per il pattern P = ΦAT CΦΦT CΦAT C ,
si esegue set pattern matching sull'insieme di pattern P = {00 AT C 00 ,00 T C 00 ,00 AT C 00 }
(insieme di tutte le sottostringhe massimali di P non contenenti caratteri Jolly).
Si procede scandendo il testo in modo sequenziale spostandoci sugli stati
dell'automa di Aho-Corarick generato in fase di preprocessing.

In posizione i = 6 del testo, trovandoci nello stato 3 dell'automa, viene seg-


nalata le occorrenze di "ATC", di "TC" e del secondo "ATC". L'unica occor-
renza che però porta un aggiornamento del vettore dei contatori è l'occorrenza
del primo "ATC", in quanto è l'unica che rispetta la condizione che i ≤ L [j].
Si va quindi ad incrementare il contatore in posizione C [6 − 4 + 1] = C [3].

Arrivati alla ne del testo, l'unico contatore che risulta avere un valore uguale
a k = 3 è il contatore C[3]: questo indica quindi un occorrenza di P in posizione
3 del testo.

13
4.3 Complessità
Sia n la lunghezza della stringa con caratteri Jolly P e sia m la lunghezza del
testo.
La fase di preprocessing ha una complessità di O(n + m). Un costo di O(n)
è dovuto alla costruzione dell'automa di AC per l'insieme di pattern costitu-
ito dalle sottostringhe massimali di P non contenenti caratteri Jolly; questo
costo è O(n) in quanto, a causa della presenza di caratteri Jolly, la somma
delle lunghezze delle sottostringhe massimali non contenenti caratteri Jolly è
sicuramente minore o uguale alla lunghezza di P. A questa complessità si ag-
giunge un costo proporzionale alla lunghezza del testo, dovuto al dover azzerare
il contatore C in tutta la sua lunghezza.
La fase di ricerca ha una complessità di O(m + t) dove t è il numero delle
occorrenze. Infatti il costo O(m) è dovuto alla scansione sequenziale di tutto
il testo a cui si aggiunge un costo proporzionale al numero di occorrenze del
pattern all'interno del testo. Infatti ogni occorrenza incrementa una posizione
di C di un unità ed ogni posizione C [1] , . . . , C [m] è incrementata al più z volte.
Quindi t ≤ zm e quindi la fase di ricerca ha una complessità O(m) nel caso in
cui z è limitato da una costante.
E quindi possibile derivare il teorema che, se il numero di caratteri Jolly
è limitato da una costante, il pattern matching esatto con caratteri Jolly può
essere eettuato in tempo O(|P | + |T |).
E' da notare che la complessità della fase di ricerca è comunque legata al
numero di occorrenze presenti nel testo in quanto non si conoscono approcci
indipendenti del numero di occorrenze.

14
5 Baeza-Yates & Perleberg (BYP)
5.1 Introduzione al problema del Bounded Matching
La ricerca inesatta di una stringa S all'interno di una stringa (generalmente
molto più grande) T consiste nel cercare ogni occorrenza di S che può comparire
anche alcuni caratteri errati o mancanti e si risolve in modo simile al problema
di trovare un allineamento tra due stringhe con un algoritmo di programmazione
dinamica la cui complessità è O(nm) (dove |S| = n e|T | = m). Le prestazioni
di questa programmazione dinamica potrebbero essere considerate insucienti.
Per questo motivo si è scelto di esaminare una versione del problema della
ricerca inesatta di sottostringhe, semplicata alle sole occorrenze della stringa
S che compaiono con al più k errori. Questo problema viene detto di Bounded
Matching e, a dierenza del problema della ricerca inesatta, se ne conosce una
soluzione in O(m).

5.1.1 Algoritmo di allineamento e Bounded Matching


Prima di vedere la soluzione lineare al problema osserviamo che l'algoritmo di
allineamento continua a risolvere anche il problema del bounded matching, con-
tando 1 sia i mismatch che gli in/del. L'algoritmo di programmazione dinamica
può anche essere migliorato per essere eseguito in O(kn) nel seguente modo:

• La matrice viene riempita per colonne


• Si interrompe il riempimento di una colonna alla riga p tale che A[p, j] > k
• Per la colonna successiva sarà suciente riempire no alla riga p.

5.2 Algoritmo di Baeza-Yates & Perleberg


Vediamo i passi dell'algoritmo BYP:

1. La stringa S viene suddivisa in k+2 sottostringhe Sj1 , ...S


k k+2 assicurandoci
n
che le prime k + 1 abbiano uguale lunghezza r = k+1 .

2. Si utilizza AC (ma anche un algoritmo simile, purché O(n) è adatto)


per cercare ogni occorrenza delle sottostringhe di S costruite al punto
precedente.

3. Ogni volta AC trova un'occorrenza si esegue l'algoritmo di allineamento


(eventualmente migliorato per il bounded matching) sulla sottostringa t[i−
n − k, i + n + k] e si decide se si tratta eettivamente di una occorrenza
di S all'interno di T con al più k errori.

L'algoritmo trova eettivamente tutte le occorrenze di S in T con al più k errori,


infatti se un'occorrenza di S in T compare con al più k errori allora almeno una
delle sottostringhe di S costruite al punto 1 dell'algoritmo deve comparire senza

15
errori. Nel caso peggiore ognuna delle sottostringhe conterrà esattamente un
errore tranne una, dato che il numero massimo di errori è k mentre le stringhe
sono almeno k+1.

5.3 Complessità
Il primo punto e la costruzione dell'automa di AC possono essere eseguiti in
tempo O(n), la ricerca con l'automa di AC, da sola, viene eseguita in tempo
O(m). Dunque né il punto 1 né il punto 2 rappresentano un problema.
Più dicile è stabilire la complessità del terzo punto. Ogni esecuzione
dell'algoritmo di programmazione dinamica è O(n2 ) in quanto viene eseguito
su due stringhe di lunghezza O(n). Il numero medio di volte che una delle sot-
tostringhe costruite al punto 1 comparirà in T è m(k+1)
|Σ|r e quindi la complessità
2
del punto 3 sarà: O( mn|Σ|(k+1)
r ). Anché l'algoritmo sia lineare in m questa
quantità deve restare lineare in m ovvero:

mn2 (k + 1)
r ≤ cm
|Σ|

che è vericato quando k = O(n/ log n).

6 Bioinformatica
6.1 Sequence Tagged Sites
Uno dei primi obiettivi del sequenziamento di un genoma (ad esempio lo Human
Genome Project) è quello di mappare nel DNA un insieme di Sequence Tagged
Sites (STS), disposte in modo che ci sia almeno una STS ogni 100.000bp. Le
mappe di STS possono infatti essere utilizzate per localizzare l'esatta posizione
di una sequenza anonima all'interno del DNA.
Una STS non è altro che una stringa di 200 - 300bp le cui estremità, en-
trambe di circa 20 - 30bp, compaiono una sola volta all'interno del genoma;
di conseguenza l'intera STS compare dunque una sola volta nel genoma. Una
volta in possesso di una mappa di STS l'individuazione di una sequenza di
DNA anonima viene ridotta al problema di ricerca multipla esatta, risolvibile
con l'Aho-Corasick.

6.2 Simple Sequence Repeats e TROLL


Uno dei modi usati per cercare STS, o in genrale altri marcatori, è cercare le
Simple Sequence Repeats (SSR), dette anche Short Tandem Repeats Polymor-
phisms (STRP) o microsatelliti.
Una SSR è costituita da una sequenza corta, 2-5bp, ripetuta molte volte
(10-30). Queste sequenze possono essere trovate con una versione modicata
dell'AC, il programma open source TROLL, reperibile all'indirizzo http://nder.sourceforge.net/,

16
utilizza appunto questa strategia, vedremo una breve descrizione di questo al-
goritmo.

6.2.1 TROLL
TROLL cerca i microsatelliti utilizzando una lista di motivi specicata dall'utente,
verranno trovati solo i microsatelliti composti da ripetizioni di questi motivi.
TROLL eseguirà la scansione della biosequenza in cui cercare i microsatelliti
usando l'algoritmo di AC con un automa costruito sopra la lista di motivi e
manterrà in memoria i microsatelliti correnti. Ogni microsatellite verrà rappre-
sentato da tre parametri:
• Il motivo di cui è composto
• La posizione di inizio
• La lunghezza del microsatellite (nora)
Ogni volta che l'AC trova un motivo si aggiorna la struttura dati contenente
i microsatelliti correntemente attivi (eventualmente rimuovendo microsatelliti
terminati o aggiungendo un nuovo microsatellite, minimale, composto da una
sola ripetizione del motivo trovato).
Un'implementazione naif della struttura dati dei microsatelliti attivi porterebbe
ad un algoritmo O(mz), dove m è la lunghezza del testo cercato e z è il numero
di motivi. TROLL, invece, utilizza una matrice triangolare inferiore le cui celle
contengono descrizioni di microsatelliti, la cella di indice (i,j) conterrà un mi-
crosatellite il cui motivo è lungo i e che comincia in posizione ki + j dove k è
un intero qualsiasi.
Ogni volta che l'AC trova un motivo si dovrà solo controllare la cella (len,
start%len) dove start è la posizione di inizio del motivo trovato e len è la
lunghezza del motivo trovato.
Inoltre, TROLL, contiene un semplice accorgimento che gli permette di
evitare di segnalare per la stringa ACTACTACT microsatelliti di ACT, CTA e
TAC ma solo uno (il più lungo).

6.3 Motivi di Sequenza


Esaminando le proteine si è scoperto che certi motivi tendono a comparire
uguali su proteine diverse, queste similarità tra proteine vengono utilizzate,
assieme ad altri criteri, per raggruppare le proteine in categorie. Molto spesso
questi motivi possono essere individuati soltanto osservando la struttura tridi-
mensionale della proteina, alcuni motivi si possono trovare esaminando semplice-
mente la struttura primaria della proteina cercando una espressione regolare. In
certi casi particolari l'espressione regolare è così semplice che può essere cercata
usando l'estensione dell'AC con caratteri jolly.
Ad esempio lo Zinc Finger, un motivo presente in molti fattori di trascrizione,
ovvero particolari proteine che legano ad alcune locazioni di DNA e ne regolano
la trascrizione in RNA, ha una forma denita come:

17
CΦΦCΦΦΦΦΦΦΦΦΦΦΦΦΦHΦΦH
dove C e H indicano la presenza degli amino acidi della cysteine e dell'
histidine mentre Φ è il carattere Jolly.

6.4 Baeza-Yates & Perleberg (BYP)


Per lungo tempo si è ritenuto che il bounded matching non avesse nessuna
applicazione nella bioinformatica dato che è quasi sempre molto dicile limitare
superiormente il numero di errori e molto spesso questo numero sarebbe troppo
grande perché il bounded matching potesse dare un contributo utile. Inoltre il
problema del bounded matching parica gli errori per in/del con i mismatch,
un assunzione spesso non realistica.
Recentemente sono stati individuati due utilizzi per il bounded matching in
bioinformatica:

1. Prima del sequenziamento su larga scala diversi pezzi più piccoli di DNA
vengono sequenziati e inseriti in basi di dati. Si è spesso interessati a
comparare le sequenze generate dal sequenziamento su larga scala con
quelle già presenti nel database. Ci si aspetta, infatti, che il numero di
dierenze sia minimo e facilmente limitabile. In questo modo è possibile
correggere le sequenze nel database dato che il sequenziamento su larga
scala è considerato più accurato.

2. Rimuovere le ridondanze in un database. Delle veriche vengono eseguite


periodicamente per cercare sequenze molto simili (il 98% circa dei residui
identici). Ovvimanete sequenze molto simili hanno un numero di dierenze
piccolo e limitato quando vengono allineate.

References
[1] Ecient String Matching: An Aid to Bibliographic Search, A.V. Aho e
M.J. Corasick, Communications of ACM: June 1975 Vol. 18 Num. 6

[2] TROLL - Tandem Repeat Occurrence Locator, A.D. Castelo, W. Martins


e G.R. Gao, Bioinformatics Applications Note: Vol. 18 no. 4 2002

[3] TROLL homepage: nder.sourceforge.net

[4] Introduzione alla Bioinformatica cap. 2, A.M. Lesk, ed. McGraw-Hill

[5] Articial Intelligence and Molecular Biology cap. 1, AA. VV.


http://www.aaai.org/Library/Books/Hunter/hunter.html.

[6] Sequence Comparsion Algorithms in Genome Databases, John Tsang,


http://citeseer.ist.psu.edu/358957.html

[7] Sequence Motif, Wikipedia Article, http://en.wikipedia.org/wiki/Sequence_motif

18
[8] Structural Motif Wikipedia Article, http://en.wikipedia.org/wiki/Structural_motif

[9] Zinc Finger, Wikipedia Article, http://en.wikipedia.org/wiki/Zinc_nger

19

You might also like