Algoritmi e Laboratorio a.a. 2009-10 Lezioni Il problema Algoritmo

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