Il problema Università di Torino – Facoltà di Scienze MFN Corso di Studi in Informatica Curriculum SR (Sistemi e Reti) • Dato un testo, che possiamo idealmente pensare come una (lunga) stringa t, e data una (molto più corta) stringa s, trovare – se esiste – il posto in cui s compare in t. • Nota: la stringa da cercare viene anche chiamata pattern. Algoritmi e Laboratorio a.a. 2009-10 Lezioni • Esempio. La classe String di Java possiede un metodo int indexOf(String g str) che restituisce l’indice di inizio della prima occorrenza della sottostringa str nella stringa this (oppure -1 se str non compare). prof. Elio Giovannetti Lezione 39 – String matching. versione 03/02/10 • Esaminiamo due algoritmi risolventi per tale problema. Quest' opera è pubblicata sotto una Licenza Creative Commons Attribution-NonCommercial-ShareAlike 2.5. 03/02/10 17.25 Algoritmo ingenuo Si fa scorrere sul testo la stringa da cercare, e dopo ogni tentativo fallito si sposta la stringa esattamente di una posizione nel testo. Esempio: Trovare la sottostringa “aab” nel testo “aaaaaaab”. a a a a a a a b a a a a a a a b a a b E. Giovannetti - AlgELab-09-10 - Lez.39 2 Algoritmo ingenuo Si fa scorrere sul testo la stringa da cercare o pattern, e dopo ogni tentativo fallito si sposta la stringa esattamente di una posizione nel testo. Esempio: Trovare la sottostringa “aab” nel testo “aaaaaaab”. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 a a b 3 03/02/10 17.25 Algoritmo ingenuo E. Giovannetti - AlgELab-09-10 - Lez.39 4 Algoritmo ingenuo Si fa scorrere sul testo la stringa da cercare, e dopo ogni tentativo fallito si sposta la stringa esattamente di una posizione nel testo. Esempio: Trovare la sottostringa “aab” nel testo “aaaaaaab”. Si fa scorrere sul testo la stringa da cercare, e dopo ogni tentativo fallito si sposta la stringa esattamente di una posizione nel testo. Esempio: Trovare la sottostringa “aab” nel testo “aaaaaaab”. a a a a a a a b a a a a a a a b a a b 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 a a b 5 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 6 1 Algoritmo ingenuo Algoritmo ingenuo Si fa scorrere sul testo la stringa da cercare, e dopo ogni tentativo fallito si sposta la stringa esattamente di una posizione nel testo. Esempio: Trovare la sottostringa “aab” nel testo “aaaaaaab”. Si fa scorrere sul testo la stringa da cercare, e dopo ogni tentativo fallito si sposta la stringa esattamente di una posizione nel testo. Esempio: Trovare la sottostringa “aab” nel testo “aaaaaaab”. a a a a a a a b a a a a a a a b a a b 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 a a b 7 03/02/10 17.25 TROVATO ! E. Giovannetti - AlgELab-09-10 - Lez.39 Nota 8 Invariante Se si spostasse la stringa di più di una posizione nel testo, si potrebbe perdere una presenza della stringa nel testo. Esempio: a a a b c d e f (sono necessari due indici, ma è comodo tenerne tre) 0 k i t: s: a a b j Se, avendo ottenuto un mismatch fra a e b, Se b spostassi la stringa “aab” direttamente al punto nel testo in cui si è avuto il mismatch, cioè: n-1 m-1 significato degl’indici: degl indici: • k: indice del carattere iniziale del prefisso del pattern riconosciuto nel testo; • i: indice del prossimo carattere da esaminare nel testo; • j: indice del prossimo carattere del pattern; a a a b c d e f a a b perderei il riconoscimento della stringa. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 9 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 Invariante Test del while (sono necessari due indici, ma è comodo tenerne tre) 0 k i t: n-1 t: 0 j m-1 t[k .. i-1] = s[ 0.. j-1] la stringa g s è stata riconosciuta nel testo t a p partire dalla posizione k, per i primi j caratteri della stringa; i è l’indice del successivo carattere da esaminare nel testo. Corpo del ciclo • caso t[i] == s[j]: anche il nuovo carattere va bene, quindi: i++; j++; • caso t[i] != s[j]: bisogna ricominciare il confronto dall’inizio di s, spostandosi a destra di uno nel testo, quindi k++; i = k; j = 0; E. Giovannetti - AlgELab-09-10 - Lez.39 k i n-1 s: s: 03/02/10 17.25 10 11 j m-1 Si deve uscire se: • j == m: si è riconosciuta l’intera stringa s, iniziante da k; oppure • i == n: si è arrivati alla fine del testo. Quindi il ciclo deve continuare (pensa anche a de Morgan) se i < n e j < m: while(i < n && j < m) 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 12 2 Complessità del caso peggiore. static int search(String str, String text) { char[] s = str.toCharArray(); char[] t = text.toCharArray(); int m = s.length, n = t.length; int i = 0, j = 0, k = 0; while(i < n && j < m) { if(t[i] == s[j]) { i++; j++; } else { k++; i = k; j = 0; } } return j == m ? k : -1; } 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 13 m confronti con la stringa in posizione k, ripetuti n volte (in realtà n-m+1 volte): T(n) = Θ(m • n) Esempio: Nell’esempio precedente, stringa “aab” nel testo “aaaaaaab”, si operano 6 (= 8 – 3 + 1) volte 3 confronti, cioè 18 confronti. Se si fa crescere m proporzionalmente a n, si ha ovviamente T(n) = Θ(n2) (algoritmo quadratico) Si può fare meglio ? 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 Verso un miglioramento: esempio. c o n t e n t i s s Esempio (fine) Ma è anche inutile spostare la stringa di due posizioni nel testo, cioè: i m o _ e _ c o n t e s s a c o n t e n t • Dopo aver riconosciuto la stringa “conte” e aver fallito sul carattere successivo, è inutile spostare la stringa di un solo carattere nell testo, cioè: i è c o n t e n t i s s i m o _ e _ c o n t e s s a perché i 4 successivi caratteri del testo, cioè “onte”, sono già noti, e non si accordano con “cont”. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 15 s s i m o _ e _ i s s i m o _ e _ perché i 3 successivi caratteri del testo, cioè “nte”, sono già noti, ti e non sii accordano d con ““con”. ” Analogamente per le tre posizioni successive. Si può quindi spostare la stringa direttamente di 5 posizioni nel testo, sul primo carattere non riconosciuto: c o n t e n t s s i m o _ e _ c o n t e s s a 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 16 a b a a b b a b a b a a b a b a a b a a b a a b a b a a b b a b a b a a b a b a Potremmo quindi determinare a priori quando tale scorrimento è sicuramente destinato al fallimento ! E. Giovannetti - AlgELab-09-10 - Lez.39 i Allora si può fa scorrere il pattern di 3 posti, e inoltre ricominciare i confronti non dall’inizio del pattern: c o n t e s s a 03/02/10 17.25 i m o _ e _ Anche qui, è inutile spostare il pattern di 1, 2, o 3 posizioni, perché i primi 4 caratteri di abaab, cioè abaa non si accordano con gli ultimi 4, cioè baab; e così i primi 3 con gli ultimi l i i3 3. I Invece i primi i i 2 caratterii di abaab, b b cioè i è ab, b sono uguali ai due ultimi: a b a a b a a b c o n t e s s a c o n t e n t s s Altro esempio Far scorrere il pattern “contessa” di k posizioni nel testo equivale a far scorrere il sotto-pattern “conte” di k posizioni rispetto a se stesso ! i i c o n t e s s a Osservazione c o n t e n t 14 a b a a b a a b 17 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 18 3 L’idea dell’algoritmo. Riassunto • Nei due esempi precedenti non si ri-esaminano elementi del testo già esaminati, bensì si continua con il carattere non riconosciuto del testo, eventualmente “tornando indietro” nel pattern, ma non necessariamente all’inizio del pattern, in modo da non perdere un eventuale sotto-pattern già riconosciuto. • Si può allora eseguire una pre-elaborazione che, esaminando il pattern da cercare, determini per ognuno dei possibili m fallimenti (secondo, terzo, …, m-esimo carattere del pattern) da quale carattere del pattern stesso occorre ripartire. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 19 testo s: 0 j-1 j Dopo il mismatch, mismatch si può avere un nuovo matching del pattern che cominci nella parte di testo già esaminata solo se … 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 Riassunto 20 Riassunto testo testo s: s: 0 03/02/10 17.25 j-1 j E. Giovannetti - AlgELab-09-10 - Lez.39 0 21 03/02/10 17.25 k j-1 j E. Giovannetti - AlgELab-09-10 - Lez.39 Riassunto 22 Riassunto testo testo s: s: 0 k j-1 j 0 s: j-1 j s: 0 j-1 j 0 j-1 j … quindi solo se esiste un k tale che: s[0 .. k-1] = s[j-k .. j-1] s: 0 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 23 03/02/10 17.25 k j-k j-1 j E. Giovannetti - AlgELab-09-10 - Lez.39 24 4 Che cosa deve produrre il pre-processing Nota Bene Sia m la lunghezza del pattern s. Per ogni j (con 1 ≤ j < m) bisogna pre-calcolare l’indice nel pattern da cui ricominciare la scansione se s[j] non è uguale al carattere t[i] letto nel testo, memorizzando tale valore in un array b. È conveniente che l’array b sia di dimensione m, in modo che: Il caso in cui serve il preprocessing è quello in cui il matching del pattern col testo fallisce dopo il primo carattere del pattern da cercare. In tal caso si riprende la scansione del pattern dal k precalcolato, mentre l’indice i nel testo non viene incrementato, poiché bisogna confrontare con s[k] lo stesso carattere t[i] che aveva causato il mismatching con s[j] b[j] = indice k nel pattern da cui ricominciare la scansione se s[j] non è uguale al carattere t[i] letto nel testo. L’elemento b[0] per ora possiamo considerarlo una cella “sprecata”, perché non utilizzata dall’algoritmo di ricerca vero e proprio. In realtà – come vedremo – fissandone il valore in modo opportuno essa sarà usata per rendere più semplice il codice del preprocessing. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 25 Esempio stringa da trovare (o pattern): array prodotto dal preprocessing: testo: 1 2 3 4 5 6 ? 0 0 0 1 2 3 b a r b a r b a r a b a r b a r a testo: b a r b a r b a r a b a r b a r a 0 1 2 3 4 5 6 Il matching fallisce sul carattere di indice 6 del pattern: si ricomincia a scandire il pattern dall’indice 3. 03/02/10 17.25 03/02/10 17.25 26 E. Giovannetti - AlgELab-09-10 - Lez.39 27 static int KMPsearch(String str, String text) { int m = str.length(), n = text.length(); int[] b = preprocess(s); int i = 0, j = 0; while(i < n && j < m) { if(text.charAt(i) == str.charAt[j]) { j++; i++; match riuscito del carattere: } avanzo sia si nell pattern tt che h nell testo t st else if(j == 0) i++; mismatch con il primo caratt. di str : avanzo nel testo. else j = b[j]; mismatch con un car. (non il primo) di str : sto fermo nel testo e riparto su str dall’indice pre-calcolato } ... 03/02/10 17.25 Uscita dal ciclo e risultato E. Giovannetti - AlgELab-09-10 - Lez.39 E. Giovannetti - AlgELab-09-10 - Lez.39 28 La procedura Due casi: • si è usciti dal ciclo con j < m, cioè senza aver riconosciuto tutto il pattern; allora è evidentemente i = n, cioè il testo è finito e il pattern s non è stato trovato: restituisco -1. • si è usciti dal ciclo perché (o con) j = m, cioè avendo riconosciuto l’intero pattern: restituisco la posizione (cioè l’indice n c del primo pr mo caratt carattere) r ) del patt pattern rn nel n testo, t sto, i – m. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 La procedura di ricerca in Java: il ciclo. b a r b a r a 0 Se invece il mismatching avviene col primo carattere del pattern, cioè per j = 0, bisogna invece in ogni caso avanzare nel testo di 1 carattere e naturalmente ricominciare la scansione del pattern da 0. 29 (trasformando per efficienza la stringa-pattern in un array di caratteri, cosa comunque non indispensabile) static int KMPsearch(String str, String text) { int m = str.length(), n = text.length(); char[] s = str.toCharArray(); int[] b = preprocess(s); int t i = 0, , j = 0; ; while(i < n && j < m) { if(text.charAt(i) == s[j]) {j++; i++;} else if(j == 0) i++; else j = b[j]; } return (j < m) ? -1 : i-m; } 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 30 5 Alcune necessarie definizioni preliminari. • prefisso di una stringa c0c1 … cm-1: è una sottostringa iniziale (eventualmente vuota) c0 … ck-1 (con 0 ≤ k ≤ m) della stringa; k è la lunghezza del prefisso. • suffisso di una stringa c0c1 … cm-1: è una sottostringa finale (eventualmente vuota) cm-k … cm-1 con 0 ≤ k ≤ m della stringa; k è la lunghezza del suffisso. • prefisso o suffisso proprio proprio: è un prefisso o suffisso che non coincide con l’intera stringa (è più corto), cioè k < m; • bordo di una stringa s: è una sottostringa che è allo stesso tempo prefisso proprio e suffisso proprio di s. Note: la stringa vuota è un bordo di qualunque stringa; la stringa vuota non ha bordo. Esempio: I bordi della stringa “abacab” sono “” e “ab”; i bordi di “aaaa” sono: “”, “a”, “aa”, “aaa”. Il preprocessing. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 31 03/02/10 17.25 Ricorda: E. Giovannetti - AlgELab-09-10 - Lez.39 32 Che cosa deve produrre il pre-processing a b a a b b a b a b a a b a b a a b a a b a a b Il matching fallisce al k-esimo carattere del pattern: allora si deve spostare il pattern finché un “prefisso proprio” del pattern viene a coincidere con un suffisso (proprio) della parte riconosciuta del pattern (parte gialla): in questo caso il prefisso/suffisso è “ab”. Ma un prefisso (proprio) coincidente con un suffisso è un bordo: bisogna quindi trovare il più lungo bordo della parte riconosciuta del pattern: il più lungo bordo di “abaab” è “ab”. Sia m la lunghezza del pattern s. Per ogni j (con 1 ≤ j < m) bisogna pre-calcolare la lunghezza del massimo bordo del sottopattern s[0 .. j-1] , costruendo un array b di dimensione m, dove: b[j] = indice nel pattern da cui ricominciare la scansione se s[j] non è uguale al carattere l letto nell testo. = lunghezza del più lungo bordo di s[0 .. j-1] a b a a b b a b a b a a b a b a a b a a b a a b 03/02/10 17.25 stringa da trovare (o pattern): array prodotto dal preprocessing: E. Giovannetti - AlgELab-09-10 - Lez.39 33 03/02/10 17.25 Esempio 1 2 3 4 5 6 7 1. Se s[0 .. h] è un bordo non vuoto di s[0 .. k], allora • s[0 .. h-1] è un bordo (eventualmente vuoto) di s[0 .. k-1], • s[h] = s[k] . 8 0 0 0 0 1 2 3 1 0 1 2 3 4 34 Due teoremi preliminari (dimostrazioni ovvie). a b c d a b c a d 0 E. Giovannetti - AlgELab-09-10 - Lez.39 5 6 a b a b c a b c d 7 0 8 è il carattere del pattern su cui si h mismatch ha i h a b c d a h-1 h k-1 k 2. Se b1 e b2 sono due bordi di una stringa s, con b2 più corto di b1, allora b2 è anche un bordo di b1. b1 b1 a b c d a b a b c d a b c 03/02/10 17.25 a b c d a b c a b2 35 03/02/10 17.25 b2 b2 E. Giovannetti - AlgELab-09-10 - Lez.39 b2 36 6 Nota terminologica a proposito del Teorema 1. Un bordo s[0 .. h-1] di s[0 .. k-1] tale che s[h] = s[k] si dice che è un bordo estensibile (da bordo di s[0 .. k-1] a bordo di s[0 ..k]). Il bordo s[0 .. h] si dice estensione del bordo s[0 .. h-1] . Il Teorema 1 si può allora formulare così: Un bordo non vuoto di s[0 .. k] è ll’estensione estensione di un bordo estensibile di s[0 .. k-1]. 0 k-1 k 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 Il preprocessing: l’invariante del ciclo esterno. 0 j b: k 0 k-1 INVARIANTE (significato degl’indici): • j è l’indice dell’ultimo elemento di b che è stato calcolato; • quindi se chiamiamo k il valore di b[j], k è la lunghezza del più lungo bordo di s[0 .. j-1], cioè s[0 .. k-1] è il più lungo bordo di s[0 .. j-1]. Analogamente, per i precedenti elementi di b si ha: • se k1 = b[j-1], s[0 .. k1-1] è il più lungo bordo di s[0 .. j-2]; • se k2 = b[j-2], s[0 .. k2-1] è il più lungo bordo di s[0 .. j-3]; • ... ecc. 37 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 Il preprocessing: l’invariante del ciclo esterno. 0 j b: k-1 38 Il ciclo esterno Attenzione: poiché j è l’ultimo elemento calcolato, si deve uscire dal ciclo quando j = m-1, NON quando j = m. k 0 j-1 j s: j-1 j for(int j = 0; j < m-1; j++) { s: k = b[j]; // lunghezza del più lungo bordo di s[0 .. j-1] calcolata nel passo precedente; INVARIANTE (in altre parole): per ogni h ≤ j, b[h] è la l lunghezza l h kh del d l più iù lungo l b bordo d di s[0 [0 .. h h-1], 1] cioè s[0 .. kh-1] è il più lungo bordo di s[0 .. h-1]. devo trovare il più lungo bordo di s[0 .. j] In particolare, per h = j: b[j] è il valore k tale che s[0 .. k-1] è il più lungo bordo di s[0 .. j-1]. cioè cerco il più lungo bordo s[0 .. k-1] di s[0 .. j-1] tale che s[k] = s[j]; la lunghezza del massimo bordo di s[0 .. j] è allora k+1: allora cerco il più lungo bordo estensibile di s[0 .. j-1] Attenzione: diversamente dalla maggior parte degli algoritmi visti, j è scelto come l’indice dell’ultimo elemento calcolato, NON del successivo elemento Eda calcolare ! , 39 b[j+1] = k+1; } 03/02/10 17.25 Il preprocessing. E. Giovannetti - AlgELab-09-10 - Lez.39 40 Il corpo del ciclo, per calcolare b[j+1] Inizializzazione Come abbiamo visto, b[0] non è utilizzato dall’algoritmo di ricerca. Esso però potrà essere utilizzato, come vedremo, dall’algoritmo di preprocessing stesso. All’interno di esso, poiché b[h] è la lunghezza kh del più lungo bordo di s[0 .. h-1], allora b[0] è la lunghezza del più lungo bordo di s[0..-1], cioè la lunghezza del più lungo bordo del bordo vuoto. Ma il bordo vuoto non ha bordi, quindi per indicare ciò poniamo b[0] = -1, il che sarà utile per la scrittura del codice. b[j+1] dovrà essere la lunghezza del max bordo di s[0 .. j] Allora dobbiamo calcolare l’indice k’ tale che s[0 .. k’] sia il più lungo bordo di s[0 .. j]; in b[j+1] dovrò poi mettere la sua lunghezza, che è k’+1. • Per il Teorema 1, il più lungo bordo di di s[0 .. j] è l’estensione del più lungo bordo estensibile di di s[0 .. j-1] oppure, pp , se un tale bordo estensibile non esiste,, il bordo vuoto. 0 j j+1 b: k k’ 0 k’ k-1 k j-1 j s: 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 41 max. max bordo bordo di s[0..j-1] estensibile E. Giovannetti - AlgELab-09-10 - Lez.39 di s[0..j-1] 42 7 Il ciclo interno. Il ciclo interno. • Per trovare tale bordo estensibile (o per scoprire che non esiste) bisogna fare, all’interno del ciclo principale, un ciclo annidato che generi, in ordine di lunghezza decrescente, tutti i bordi di s[0 .. j-1] , finché non ne trova uno estensibile, o finché non ci sono più bordi. s[0..k-1] è il più lungo bordo di s[0..j-1] 0 k-1 k j-1 j s: s[k] = s[j] ? se no, s[0..k-1] non è un bordo estensibile, bisogna provare con il bordo immediatamente più corto: 0 k’ j-1 j s: s[k’] = s[j] ? se no, s[0..k-1] non è un bordo estensibile, bisogna provare con il bordo ancora più corto 0 k’’ j-1 j s: s[k’’] = s[j] ? se no, ecc.; se si, ho trovato ! 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 43 03/02/10 17.25 Il ciclo interno E. Giovannetti - AlgELab-09-10 - Lez.39 44 Pittograficamente: • Al passo generico, dato un bordo di s[0 .. j-1]: • controlla se tale bordo è estensibile, e se non lo è trova il bordo più lungo fra i bordi di s[0 .. j-1] più corti del precedente; • e così via finché si trova un bordo estensibile, o si scopre che un tale bordo non esiste. • Per il Teorema 2, per cercare un bordo di s[0 .. j-1] più corto di quello dato, basta cercare un bordo del bordo dato: • il bordo dato ha lunghezza k, allora il più lungo bordo di tale bordo ha lunghezza b[k]; • quindi si opera l’assegnazione k = b[k] 0 k-1 k j-1 j s: Supponiamo che il bordo giallo di s[0..j-1] non sia estensibile: allora devo cercare un altro bordo di s[0..j-1], più corto, per vedere se è estensibile. Attenzione: k = b[k] è l’istruzione chiave, e non è intuitiva ! 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 45 03/02/10 17.25 Pittograficamente: 0 k-1 k E. Giovannetti - AlgELab-09-10 - Lez.39 46 Pittograficamente: j-1 j 0 k-1 k j-1 j s: s: Supponiamo che il bordo giallo di s[0..j-1] non sia estensibile: allora devo cercare un altro bordo di s[0..j-1], più corto, per vedere se è estensibile. Ma come faccio per trovare un bordo di s[0..j-1] più corto di quello giallo ? Supponiamo che il bordo giallo di s[0..j-1] non sia estensibile: allora devo cercare un altro bordo di s[0..j-1], più corto, per vedere se è estensibile. Ma come faccio per trovare un bordo di s[0..j-1] più corto di quello giallo ? Per il Teorema 2 basta che cerchi un bordo del bordo giallo ! Ma il più lungo bordo del prefisso giallo, cioè il più lungo bordo di s[0..k-1], è il valore di b[k] che è stato calcolato in uno dei passi precedenti. Quindi faccio l’assegnazione k = b[k] ! 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 47 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 48 8 Pittograficamente: 0 k Il corpo del ciclo interno j-1 j s: Controllo se questo più corto bordo di s[0..j-1] è estensibile, cioè se s[k] = s[j]. Ho i due casi: • s[k] = s[j]: il bordo rosso di s[0..j-1] è estensibile ad un bordo di s[0..j], s[0 j] ho quindi trovato il più lungo bordo di s[0..j] s[0 j] k j-1 j .0 s: k = b[j]; while(k indica un prefisso && s[0 .. k-1] non estensibile) k = b[k]; Traducendo: k = b[j]; while(k >= 0 && s[j] != s[k]) k = b[k]; • s[k] ≠ s[j]: il bordo rosso s[0..j-1] non è estensibile: devo provare con un bordo di s[0..j-1] ancora più corto del bordo rosso: ma, sempre per il Teor.2, per trovarlo basta che cerchi un 0 k bordo del bordo rosso; ecc. j-1 j s: 49 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 50 All’uscita dal ciclo interno Il preprocessing: l’algoritmo. • caso 1: k è la lunghezza del più lungo bordo estensibile di s[0 .. j-1], quindi la lunghezza del più lungo bordo di s[0 .. j] è k+1: si esegue quindi l’istruzione b[j+1] = k+1; static int[] preprocess(char[] s) { int m = s.length; int[] b = new int[m]; b[0] = -1; // al primo passo è k = -1, quindi int k; // il corpo del while non viene eseguito for(int j = 0; j < m-1; j++) { k = b[j]; while(k >= 0 && s[j] != s[k]) k = b[k]; b[j+1] = k+1; } // al primo passo si ottiene sempre b[1] = 0 return b; } Osserva: b[1] è la lunghezza del più lungo bordo di s[0 .. 0]; ma l’unico bordo di un segmento di lunghezza 1 è il bordo vuoto (lunghezza 0). 0 s: 03/02/10 17.25 52 • caso 2: un tale bordo non esiste, k = -1, 1, quindi occorre ricominciare il matching del pattern dall’indice 0, cioè b[j+1] = 0; Ma poiché k è -1, ciò si ottiene, come nel caso precedente, con l’istruzione b[j+1] = k+1; 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 51 Miglioramenti e semplificazioni. Miglioramenti e semplificazioni. Si può quindi inizializzare, oltre che b[0] a -1, anche b[1] a 0, e iniziare il ciclo esterno da j = 1 invece che da j = 0: static int[] preprocess(char[] s) { int m = s.length; int[] b = new int[m]; b[0] = -1; b[1] = 0; int k; for(int j = 1; j < m-1; j++) { k = b[j]; while(k >= 0 && s[j] != s[k]) k = b[k]; b[j+1] = k+1; } return b; } static int[] preprocess(char[] s) { int m = s.length; int[] b = new int[m]; b[0] = -1; b[1] = 0; int k; for(int j = 1; j < m-1; j++) { k = b[j]; while(k >= 0 && s[j] != s[k]) k = b[k]; b[j+1] = k+1; } return b; } Si osservi che ad ogni iterazione il valore di b[j] calcolato nell’iterazione precedente è k+1; si può quindi semplificare il codice eliminando l’istruzione k = b[j] e incrementando k. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 53 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 54 9 Miglioramenti e semplificazioni. Versione finale. static int[] preprocess(char[] s) { int m = s.length; int[] b = new int[m]; b[0] = -1; b[1] = 0; int k = 0; int j = 1; for(int j = 0; j < m-1; j++) { while(k >= 0 && s[j] != s[k]) k = b[k]; b[j+1] = k+1; k++; } return b; } Inoltre, invece di operare l’assegnazione b[j+1] = … e poi incrementare j nel ciclo for, si può usare un ciclo while in cui si incrementano j e k prima di fare l’assegnazione: 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 55 static int[] preprocess(char[] s) { int m = s.length; int[] b = new int[m]; b[0] = -1; b[1] = 0; int k = 0; int j = 1; while(j < m-1) { while(k >= 0 && s[j] != s[k]) k = b[k]; b[++j] = ++k; } return b; } Esercizio: si scriva la versione analoga alla precedente in cui si inizializza fuori dal ciclo solo b[0] e non anche b[1]. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 Attenzione ! Analisi della complessità • La cosa più difficile da capire nell’algoritmo di KMP è l’istruzione k = b[k] del ciclo interno. • Essa si basa sulla proprietà espressa dal Teorema 2. • Nello studio, accertatevi di avere ben capito il ruolo di tale istruzione e del teorema, cioè di avere ben compreso che il fatto che essa funzioni è una conseguenza del Teorema 2. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 57 • Si può dimostrare che l’algoritmo di preprocessing ha complessità O(m). • L’algoritmo di ricerca, esclusa la fase di preprocessing, ha evidentemente complessità O(n). • L’intero algoritmo di KMP ha quindi complessità O(m+n). • Ma poiché è m ≤ n, la complessità è semplicemente O(n). • Abbiamo Abbi m quindi un algoritmo l ritm lin lineare r anziché nziché qu quadratico dr tic come l’algoritmo ingenuo. 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 Esempio di esecuzione dell’algoritmo di preprocess. pattern s: a b c a b c d a b 0 1 2 3 4 5 56 6 7 Esempio. pattern s: a b c a b c d a b 8 0 b[0] = -1; b[1] = 0 58 1 2 3 4 5 6 7 8 j = 5, k = 2: s[k] = s[j] non viene eseguito il corpo del while b[++j]= ++k; ⇒ b[6] = 3 (bordo estendibile) j = 1, k = 0: s[k] ≠ s[j] viene eseguito il corpo del while k = b[k]; ⇒ k = -1 non esiste bordo estendibile b[++j]= ++k; ⇒ b[2] = 0 j = 2, k = 0: s[k] ≠ s[j] viene eseguito il corpo del while k = b[k]; ⇒ k = -1 non esiste bordo estendibile b[++j]= ++k; ⇒ b[3] = 0 j = 6, k = 3: s[k] ≠ s[j] si esegue il corpo del while k = b[k] = b[3] ⇒ k = 0 k = 0, s[k] ≠ s[j] si riesegue il corpo del while k = b[k]; ⇒ k = -1 non esiste bordo estendibile b[++j]= ++k; ⇒ b[7] = 0 j = 3, k = 0: s[k] = s[j] non viene eseguito il corpo del while b[++j]= ++k; ⇒ b[4] = 1 (bordo estendibile) j = 7, k = 0: s[k] = s[j] non viene eseguito il corpo del while b[++j]= ++k; ⇒ b[8] = 1 (bordo estendibile) j = 4, k = 1: s[k] = s[j] non viene eseguito il corpo del while b[++j]= ++k; ⇒ b[5] = 2 (bordo estendibile) b: -1 0 0 0 1 2 3 0 1 03/02/10 17.25 E. Giovannetti - AlgELab-09-10 - Lez.39 59 0 03/02/10 17.25 1 2 3 4 5 6 7 E. Giovannetti - AlgELab-09-10 - Lez.39 8 60 10