universita` degli studi di padova studio della trasformata di burrows e

Transcript

universita` degli studi di padova studio della trasformata di burrows e
UNIVERSITA’ DEGLI STUDI DI PADOVA
Facoltà di Ingegneria
Dipartimento di Ingegneria dell’Informazione
STUDIO DELLA TRASFORMATA DI
BURROWS E WHEELER
Relatore: Ch.mo Prof. Andrea Pietracaprina
Laureando: Simone Spagnol
Anno Accademico 2005-2006
1
Indice
1
Introduzione
3
2
La trasformata di Burrows e Wheeler
2.1
Algoritmo C
2.2
Algoritmo D
2.3
Varianti
5
5
6
7
3
Compressione con la BWT
3.1
Move-To-Front
3.1.1 Algoritmo M: Codifica MTF
3.1.2 Algoritmo W: Decodifica MTF
3.2
Codici di Huffman
3.2.1 Algoritmo H: Codifica di Huffman
3.2.2 Algoritmo H’ : Decodifica di Huffman
3.3
Codifica Run-Length
3.3.1 Algoritmo RLE: Run-Length Encoding
3.3.2 Algoritmo RLD: Run-Length Decoding
3.4
Efficienza
9
10
10
10
11
11
12
12
13
13
13
4
Analisi della BWT
4.1
L’entropia empirica di una stringa
4.2
Analisi dell’algoritmo BW0
15
15
17
5
Prestazioni della BWT
27
6
Implementazione efficiente della BWT
6.1
Il problema dell’ordinamento
6.1.1 Algoritmo Sort
6.2
Decompressione efficiente
6.3
Implementazione in pseudocodice
6.3.1 Algoritmo C
6.3.2 Algoritmo D
6.3.3 Inizializzazione della lista M
6.3.4 Algoritmo M
6.3.5 Algoritmo W
6.3.6 Costruzione dell’albero di Huffman
31
31
31
34
34
34
35
35
36
36
37
7 Conclusioni
39
Bibliografia
41
2
3
1 Introduzione
La compressione dei dati è uno dei problemi cruciali nel campo
dell’informazione. Essa infatti ha molti usi: ad esempio, nell'ambito delle reti,
il tempo di trasferimento di un file può essere significativamente ridotto
comprimendo file di grandi dimensioni; nell’ambito delle grandi moli di dati,
è utile per codificare i dati in modo che occupino meno spazio in memoria, e
così via. Esistono diversi algoritmi di compressione, generalmente facenti
parte di una delle due seguenti categorie:
•
compressione statistica: gli algoritmi che fanno parte di questa categoria
partono da alcuni studi statistici sul formato dell'input per ottenere una
buona compressione. Ad esempio in un file di testo da comprimere si
studia la frequenza relativa di ciascun carattere per associare poi ai
caratteri più frequenti codici più corti, e viceversa ai caratteri meno
frequenti codici più lunghi. Di questa classe fanno parte codifiche come
quella di Huffman, a cui si accennerà successivamente, e come quella
aritmetica;
•
compressione con dizionario: l’idea che sta alla base di questi algoritmi è
quella di utilizzare un dizionario (una struttura dati contenente delle
entries a due campi posti in corrispondenza biunivoca, la frase e l’indice)
per rappresentare molti simboli con un’unica parola di codice, costituita
dall’indice di una frase del dizionario. Di questa classe fanno parte ad
esempio gli algoritmi di Lempel e Ziv.
Fino a poco più di dieci anni fa gli algoritmi di compressione più utilizzati
erano sicuramente quelli basati sulle due tecniche di Lempel e Ziv, ovvero
LZ77 e LZ78. Esse si basano su un dizionario adattivo, ossia aggiornato
durante la fase di codifica in base all’evolversi dell’input processato, e
trasmesso implicitamente nel testo codificato. Le tecniche di compressione
statistica erano meno usate in quanto, seppur in grado di fornire una
compressione migliore, la loro velocità è molto inferiore.
La trasformata di Burrows e Wheeler (detta anche algoritmo Block-Sorting),
scoperta da David Wheeler nel 1983 ma resa nota al mondo solo nel 1994,
non è concettualmente un algoritmo di compressione a tutti gli effetti, e quindi
non rientra in nessuna delle due categorie definite poco fa. Si tratta infatti di
una trasformazione reversibile (lossless, ovvero senza perdita di
informazione) che prende in input un blocco di testo e manda in output lo
stesso testo con un opportuno riordinamento dei caratteri in esso contenuti,
dunque senza modificare la dimensione dell’input. Il vantaggio di una
trasformazione di questo tipo risiede nel fatto che il testo ottenuto può essere
compresso più efficientemente rispetto all’originale con dei semplici e comuni
algoritmi di compressione, in quanto essa tende a raggruppare le diverse
istanze dello stesso carattere sparse nel testo. Questo approccio, ovvero l’idea
di trasformare l’input per renderlo più adatto alla compressione, rappresentò
una novità nel campo della Data Compression, anche se, dopo l’apparizione
della trasformata di Burrows e Wheeler, alcuni ricercatori dimostrarono che
essa è comunque legata ad altre tecniche di compressione precedentemente
conosciute.
4
L’obiettivo di questa tesina è quello di illustrare in maniera esaustiva e
didattica, tramite l’utilizzo di diversi esempi, l’algoritmo di compressione
completo seguendo la descrizione fatta dagli stessi Michael Burrows e David
Wheeler nel documento originale [1] del Systems Research Center (SRC),
integrandola con una dimostrazione matematica della sua efficienza e con lo
pseudocodice delle sezioni più importanti. Chiameremo questo algoritmo
BW0.
Nel Capitolo 2 introdurremo i due algoritmi di trasformazione e
antitrasformazione di Burrows e Wheeler, con delle possibili varianti in
appendice.
Nel Capitolo 3 spiegheremo perché la Burrows-Wheeler Transform (che d’ora
in poi chiameremo BWT per semplicità) tende a raggruppare i caratteri nel
testo, quindi presenteremo alcuni possibili algoritmi da utilizzare per la
compressione vera e propria dopo lo stadio della BWT (Move-To-Front,
codifica di Huffman, Run-Length Encoding) e i corrispondenti algoritmi di
decompressione.
Nel Capitolo 4 analizzeremo matematicamente l’algoritmo di compressione
completo BW0 (BWT + Move-To-Front + Huffman), utilizzando il concetto
di entropia empirica modificata; vedremo che l’algoritmo raggiungerà al caso
peggiore un rapporto di compressione lineare rispetto all’entropia, a meno di
un fattore costante.
Nel Capitolo 5 analizzeremo le prestazioni del nostro algoritmo di
compressione applicato a degli input di test e le confronteremo con quelle di
altri noti algoritmi. Vedremo in particolare che l’operazione di trasformazione
di Burrows e Wheeler permette di raggiungere un rapporto di compressione
vicino a quello che si può ottenere con una compressione statistica, e una
velocità paragonabile agli algoritmi basati sulle tecniche di Lempel e Ziv.
Infine nel Capitolo 6 vedremo come può essere implementata efficientemente
la BWT, soffermandoci in particolare sul problema dell’ordinamento delle
rotazioni dell’input, fase cruciale dell’operazione di trasformazione. Inoltre
daremo l’implementazione in pseudocodice degli algoritmi più importanti
presentati nei Capitoli 2 e 3.
5
2 La trasformata di Burrows e Wheeler
Il cuore dell’algoritmo BWT è costituito dalle due operazioni di
trasformazione (chiamata nel documento [1] “Algoritmo C”) e
antitrasformazione (“Algoritmo D”) del blocco di testo. Sebbene queste due
procedure siano di facile descrizione e comprensione, vedremo più avanti che
la loro implementazione più efficiente richiederà un notevole sforzo
applicativo.
2.1 Algoritmo C
L’algoritmo funziona nel modo seguente: si forma una matrice N x N, che
chiameremo M, le cui righe sono le rotazioni della stringa S in input, di N
caratteri S[0], … , S[N-1] appartenenti ad un alfabeto X. Ad esempio, se la
nostra stringa è S = ‘abracadabra’, M sarà la seguente matrice 11 x 11:
abracadabra
bracadabraa
racadabraab
acadabraabr
cadabraabra
adabraabrac
dabraabraca
abraabracad
braabracada
raabracadab
aabracadabr
Le righe della matrice devono quindi essere disposte in ordine lessicografico a
partire dal primo carattere; nel nostro esempio, M diventa:
0
1
2
3
4
5
6
7
8
9
10
aabracadabr
abraabracad
abracadabra
acadabraabr
adabraabrac
braabracada
bracadabraa
cadabraabra
dabraabraca
raabracadab
racadabraab
Infine, sia L l’ultima colonna di M, ossia L[0], … , L[N-1] = M[0, N-1], … ,
M[N-1, N-1] e sia I l’indice della riga in cui appare la stringa originale;
l’algoritmo restituisce in output L ed I. Nell’esempio,
L = ‘rdarcaaaabb’
e
I = 2.
6
2.2
Algoritmo D
Come primo passo l’algoritmo di antitrasformazione calcola la prima colonna
della matrice M vista in precedenza; ciò può essere fatto in maniera molto
semplice ordinando i caratteri di L (dato che le righe di M sono ordinate
lessicograficamente). Chiamiamo questa colonna F; nel nostro esempio è:
F = ‘aaaaabbcdrr’
Ora siamo a conoscenza di L, I e F. Definiamo la matrice M’ come la matrice
ottenuta ruotando di una posizione verso destra tutte le righe di M, ossia M’[i,
j] = M[i, (j-1) mod N]. Chiaramente, ad ogni riga di M’ corrisponde una riga
di M, poiché le righe di M’ sono tutte rotazioni di S. In M’ le righe sono
ordinate a partire dal secondo carattere; si può quindi dedurre che ogni
sottoinsieme di righe di M’ che iniziano con lo stesso carattere ch appare in
M’ in ordine lessicografico. Ovviamente la stessa proprietà valeva anche per
la matrice M; notiamo anche che L è la prima colonna di M’. Vediamo
nell’esempio:
0
1
2
3
4
5
6
7
8
9
10
M
aabracadabr
abraabracad
abracadabra
acadabraabr
adabraabrac
braabracada
bracadabraa
cadabraabra
dabraabraca
raabracadab
racadabraab
M’
raabracadab
dabraabraca
aabracadabr
racadabraab
cadabraabra
abraabracad
abracadabra
acadabraabr
adabraabrac
braabracada
bracadabraa
Considerando le righe che iniziano con il carattere ‘a’: le righe 0, 1, 2, 3 e 4
in M corrispondono alle righe 2, 5, 6, 7 e 8 in M’.
Ora, per mezzo di F e L calcoliamo un vettore T che indica la corrispondenza
tra le righe delle due matrici M e M’.
Nel nostro esempio T è:
T = [9 8 0 10 7 1 2 3 4 5 6]
Usando la notazione M’[j] = M[T[j]] per ogni j = 0, … , N-1, notiamo che se
L[j] è la k-esima istanza di ch in L, allora T[j] = i, dove F[i] è la k-esima
istanza di ch in F. Sostituendo, F[T[j]] = L[j].
Ora, per ogni i = 0, … , N-1, i caratteri L[i] e F[i] sono rispettivamente
l’ultimo e il primo carattere della riga i di M; quindi il carattere L[i] precede
ciclicamente il carattere F[i] in S. Sostituendo i = T[j] nella relazione ricavata
in precedenza, otteniamo che L[T[j]] precede ciclicamente L[j] in S.
A questo punto abbiamo tutti gli elementi per ricostruire la stringa S.
Prendiamo innanzitutto in considerazione l’indice I: per definizione sappiamo
che L[I] è l’ultimo carattere di S, essendo la riga I di M quella contenente la
7
stringa originaria. Usando il vettore T riusciamo a ricavare tutti i caratteri
predecessori di L[I]: per ogni i = 0, … , N-1: S[N-1-i] = L[Ti[I]], dove T0[x] =
x, e Ti+1[x] = T[Ti[x]]. Concluse le iterazioni, otteniamo S. Nel nostro
esempio,
S[10] = L[2] = ‘a’
S[9] = L[T[2]] = L[0] = ‘r’
S[8] = L[T[0]] = L[9] = ‘b’
S[7] = L[T[9]] = L[5] = ‘a’
S[6] = L[T[5]] = L[1] = ‘d’
S[5] = L[T[1]] = L[8] = ‘a’
S[4] = L[T[8]] = L[4] = ‘c’
S[3] = L[T[4]] = L[7] = ‘a’
S[2] = L[T[7]] = L[3] = ‘r’
S[1] = L[T[3]] = L[10] = ‘b’
S[0] = L[T[10]] = L[6] = ‘a’
Ö S = ‘abracadabra’.
Da notare che la sequenza Ti[I] per i = 0, … , N-1 non sempre è una
permutazione dei numeri 0, … , N-1: ciò accade quando S è una ripetizione di
una sottostringa della forma Zp per p>1; in questo caso Ti[I] per i = 0, … , N-1
diventa della forma Z’p per qualche sottosequenza Z’. Ad esempio se S =
‘fuggifuggi’, abbiamo Z = ‘fuggi’ e p = 2, e la sequenza Ti[I] per i = 0, … , N1 sarà [1 7 5 3 9 1 7 5 3 9], come si può vedere qui di seguito:
Algoritmo C:
Algoritmo D:
S = ‘fuggifuggi’
Æ
L = ‘iiuuggggff’ e I = 1;
F = ‘ffggggiiuu’ e T = [6 7 8 9 2 3 4 5 0 1];
S[9] = L[1] = ‘i’
S[8] = L[T[1]] = L[7] = ‘g’
S[7] = L[T[7]] = L[5] = ‘g’
S[6] = L[T[5]] = L[3] = ‘u’
S[5] = L[T[3]] = L[9] = ‘f’
S[4] = L[T[9]] = L[1] = ‘i’
S[3] = L[T[1]] = L[7] = ‘g’
S[2] = L[T[7]] = L[5] = ‘g’
S[1] = L[T[5]] = L[3] = ‘u’
S[0] = L[T[3]] = L[9] = ‘f’.
2.3 Varianti
Ci sono diverse varianti degli algoritmi C e D, una delle quali ad esempio è
quella di definire T in modo che S venga generata a partire dal primo carattere
verso destra invece che dall’ultimo. Un’altra variante può essere quella di
ordinare lessicograficamente da destra verso sinistra le righe della matrice M
nell’algoritmo C, prendendo poi al posto della colonna L la prima colonna
della matrice, e così via. Tuttavia si tratta solo di questioni di notazione:
l’efficienza della compressione sulla stringa trasformata resta invariata.
8
Infine, anticipiamo che nell’implementazione che tratteremo più avanti alla
stringa da trasformare verrà concatenato un carattere terminatore non
appartenente all’alfabeto X. Questo carattere sarà utile soltanto per
l’operazione di trasformazione (e di antitrasformazione nel processo inverso);
nei passi successivi esso non dovrà essere necessariamente codificato in
quanto la sua posizione in L è identificata dall’indice I (infatti la stringa che
termina con il carattere terminatore è quella originale!). Tuttavia nella nostra
implementazione terremo il carattere terminatore in L per non complicare il
codice.
9
3 Compressione con la BWT
Dopo aver presentato le operazioni di trasformazione e antitrasformazione
passiamo alla domanda più importante di questa tesina: perché il testo
trasformato con la BWT viene compresso in maniera più efficiente rispetto
all’originale?
Innanzitutto, prima di addentrarci in un’analisi vera e propria, portiamo un
esempio. Consideriamo un blocco di testo in lingua inglese: è ragionevole
pensare che la parola “the” appaia svariate volte. Quando disponiamo le
rotazioni del testo in ordine lessicografico, molte tra le rotazioni che iniziano
in “he” finiranno con il carattere “t” (altre potrebbero finire in “s” o in “ ”, che
però di solito sono più rare); quindi l’operazione di trasformazione tende a
raggruppare insieme nella stringa L molte tra le occorrenze del carattere “t”
nel testo. Dunque se guardiamo meglio L possiamo notare che in diverse zone
tendono a raggrupparsi molte occorrenze degli stessi caratteri intervallate
sporadicamente da pochi altri caratteri. E’ questa la forza della BWT: infatti la
probabilità che un certo carattere appaia in L vicino a un’altra occorrenza
dello stesso carattere è molto alta. Un esempio di raggruppamento è riportato
in Figura 1.
Questa proprietà spiana la strada ad una codifica di tipo Move-To-Front
(MTF), che per funzionare al meglio deve aver a che fare con stringhe con
molti caratteri consecutivi ripetuti; l’output del codificatore MTF potrà quindi
essere efficientemente compresso con una codifica di ordine zero, come la
codifica di Huffman o quella aritmetica. Vediamo ora più in dettaglio i passi
appena citati per la vera e propria compressione, da combinare con gli
algoritmi C e D visti in precedenza. Analizzeremo in particolare la codifica
MTF e la codifica di Huffman.
Figura 1: venti rotazioni ordinate di un testo in lingua inglese con la relativa
parte di stringa L. Notiamo che i caratteri raggruppati sono tutte vocali ([1]).
10
3.1 Move-To-Front
L’algoritmo MTF codifica ogni carattere ch di una stringa in un numero
intero, in base al numero di istanze di caratteri diversi visti dopo l’ultima
occorrenza di ch. La decodifica è in tutto e per tutto speculare alla codifica.
3.1.1 Algoritmo M: Codifica MTF
La codifica processa L carattere per carattere, andando a guardare ad ogni
iterazione la posizione del carattere corrente in un’apposita lista Y di caratteri
dinamicamente aggiornata. Più precisamente, Y contiene un’occorrenza per
ogni carattere dell’alfabeto X (dunque avrà lunghezza |X|) e deve essere
inizializzata prima della scansione di L. Costruiamo un vettore di interi R[0],
… , R[N-1]. Ora, per ogni i = 0, … , N-1, contiamo il numero di caratteri che
precedono il carattere L[i] nella lista Y, mettiamo questo valore in R[i] e
subito dopo spostiamo il carattere L[i] in testa ad Y. Mostriamo di seguito i
vari passi dell’algoritmo applicati al nostro solito esempio:
L = ‘rdarcaaaabb’
Inizializziamo Y arbitrariamente: Y = [a b c d r]
Y = [a b c d r], L[0] = ‘r’ Æ R[0] = 4
Y = [r a b c d], L[1] = ‘d’ Æ R[1] = 4
Y = [d r a b c], L[2] = ‘a’ Æ R[2] = 2
Y = [a d r b c], L[3] = ‘r’ Æ R[3] = 2
Y = [r a d b c], L[4] = ‘c’ Æ R[4] = 4
Y = [c r a d b], L[5] = ‘a’ Æ R[5] = 2
Y = [a c r d b], L[6] = ‘a’ Æ R[6] = 0
Y = [a c r d b], L[7] = ‘a’ Æ R[7] = 0
Y = [a c r d b], L[8] = ‘a’ Æ R[8] = 0
Y = [a c r d b], L[9] = ‘b’ Æ R[9] = 4
Y = [b a c r d], L[10] = ‘b’ Æ R[10] = 0
Ö R = [4 4 2 2 4 2 0 0 0 4 0].
3.1.2 Algoritmo W: Decodifica MTF
Assumiamo che il valore iniziale della lista Y usata nell’algoritmo W sia nota
al decodificatore. L’operazione di decodifica avviene in maniera del tutto
analoga alla codifica: per ogni i = 0, … , N-1, prendiamo R[i], guardiamo la
sua posizione in Y (contando da zero) e mettiamo questo valore in L[i], e
subito dopo spostiamo il carattere L[i] in testa ad Y. Nel nostro esempio,
R = [4 4 2 2 4 2 0 0 0 4 0], Y = [a b c d r]
11
Y = [a b c d r], R[0] = 4 Æ L[0] = ‘r’
Y = [r a b c d], R[1] = 4 Æ L[1] = ‘d’
Y = [d r a b c], R[2] = 2 Æ L[2] = ‘a’
Y = [a d r b c], R[3] = 2 Æ L[3] = ‘r’
Y = [r a d b c], R[4] = 4 Æ L[4] = ‘c’
Y = [c r a d b], R[5] = 2 Æ L[5] = ‘a’
Y = [a c r d b], R[6] = 0 Æ L[6] = ‘a’
Y = [a c r d b], R[7] = 0 Æ L[7] = ‘a’
Y = [a c r d b], R[8] = 0 Æ L[8] = ‘a’
Y = [a c r d b], R[9] = 4 Æ L[9] = ‘b’
Y = [b a c r d], R[10] = 0 Æ L[10] = ‘b’
Ö L = ‘rdarcaaaabb’.
3.2 Codici di Huffman
A questo punto abbiamo la stringa R e l’indice I che ci eravamo portati dietro
dall’algoritmo C. L’ultimo passo dell’algoritmo di compressione può essere
realizzato con un qualsiasi codificatore di ordine 0; tra questi, il più noto usa
l’algoritmo di Huffman. L’idea che sta alla base dell’algoritmo è quella di
rappresentare ogni carattere presente nel testo con un certo numero di bit, in
base alla frequenza con cui questo carattere appare nel testo: in questo modo, i
caratteri più frequenti saranno rappresentati con pochi bit, mentre quelli meno
frequenti con qualche bit in più. In più, i codici assegnati ai diversi caratteri
hanno la proprietà di essere liberi da prefissi: ovvero, nessun codice è prefisso
di un altro. Vediamo come funziona l’algoritmo di Huffman applicato
all’output del nostro algoritmo M.
3.2.1 Algoritmo H: Codifica di Huffman
L’algoritmo si divide in due parti: l’assegnazione di un codice ad ogni
carattere dell’alfabeto |A| contenuto in R e la trasformazione di R nella stringa
di bit Q.
Il primo passo calcola per ogni carattere (in questo caso per ogni intero) la
frequenza in percentuale con cui questo appare in R che viene dunque
associata ad esso. Quindi ogni coppia (carattere, frequenza) viene
memorizzata in un nodo isolato di un albero. Si guardano ora i due nodi a
minor frequenza, e si assegna ad esse un nodo padre con frequenza uguale alla
somma delle frequenze dei suoi figli. Il procedimento viene eseguito |A|-1
volte, finché non risulterà un albero connesso con radice a frequenza 100%. I
codici per ogni carattere vengono generati “navigando” l’albero dalla radice
fino al nodo foglia che contiene quel carattere, aggiungendo per ogni lato del
percorso uno zero se questo congiunge il nodo padre al figlio con frequenza
minore, e un uno se questo congiunge il nodo padre al figlio con frequenza
maggiore. Per maggiori dettagli consultare il [6], pag. 385-391.
Chiariamo tutto quanto descritto qui sopra con il nostro solito esempio:
R = [4 4 2 2 4 2 0 0 0 4 0]
12
Carattere
0
2
4
# occorrenze
4/11
3/11
4/11
Frequenza
36,36%
27,27%
36,36%
Figura 2: albero di Huffman per il vettore R = [4 4 2 2 4 2 0 0 0 4 0].
Quindi:
0 Æ 0, 2 Æ 10, 4 Æ 11.
Ora che ogni intero contenuto in R ha il suo codice, risulta molto semplice
convertire R in una stringa di bit: sia Q una stringa di bit vuota; per ogni i = 0,
… , N-1 si trasforma R[i] nel corrispettivo codice e lo si concatena in fondo
alla stringa Q. Nel nostro esempio la trasformazione è la seguente:
R = [4 4 2 2 4 2 0 0 0 4 0]
Æ
Q = 111110101110000110.
3.2.2 Algoritmo H’: Decodifica di Huffman
Supponendo di conoscere già l’albero di Huffman per la stringa di bit che
vogliamo decodificare, l’algoritmo H’ processa Q da sinistra a destra e
percorre l’albero dalla radice seguendo ad ogni iterazione il ramo etichettato
con il bit corrente; quando viene raggiunta una foglia viene restituito il
carattere corrispondente e il percorso ricomincia dalla radice. E’ nella fase di
decodifica che si nota la necessità di un codice libero da prefissi. Nel nostro
esempio, la trasformazione è ovviamente l’inversa di quella dell’algoritmo H:
Q = 111110101110000110
Æ
R = [4 4 2 2 4 2 0 0 0 4 0].
3.3 Codifica Run-Length
Anche per gli algoritmi di compressione “post-BWT” si può pensare a diverse
varianti. Ad esempio si potrebbe modificare il Move-To-Front facendo in
modo che i caratteri che non appaiono da diverso tempo vengano spostati
parzialmente verso la testa della lista Y. Si può provare che questo
accorgimento migliora leggermente la qualità della compressione.
La variante più nota è legata all’uso di un ulteriore passo di codifica da
eseguire tra il Move-To-Front e la codifica di Huffman, denominato Run-
13
Length Encoding (RLE). La motivazione che induce all’uso di questo
algoritmo è la presenza nella stringa in uscita dal codificatore MTF di lunghe
sequenze di zeri. L’algoritmo tende dunque a comprimere le sequenze di zeri
con un secondo livello di codifica che contenga informazioni esclusivamente
sulla lunghezza di ognuna di esse. Vediamo nel dettaglio gli algoritmi di
codifica e di decodifica.
3.3.1 Algoritmo RLE: Run-Length Encoding
Introduciamo due ulteriori numeri da unire all’alfabeto dei numeri interi
positivi: 0 e -1. Per ogni intero m ≥ 1 sia B(m) il numero m+1 scritto in
binario usando il -1 al posto dell’1 e togliendo il bit più significativo. Ad
esempio, B(1) = 0, B(2) = -1, B(3) = 00, B(4) = 0(-1), e così via. L’algoritmo
X processa il vettore R sostituendo ogni sequenza di m zeri consecutivi 0m con
B(m). Notiamo che |B(m)| = floor(log(m+1)) ≤ m, dove floor(x) è la parte
bassa di x: K sarà quindi di lunghezza minore o uguale rispetto a R. Nel nostro
esempio,
R = [4 4 2 2 4 2 0 0 0 4 0]
K = [4 4 2 2 4 2 0 0 4 0].
Æ
K verrà poi dato in ingresso all’algoritmo di codifica di Huffman al posto di
R, con le opportune modifiche di dominio.
3.3.2
Algoritmo RLD: Run-Length Decoding
In maniera speculare, l’algoritmo di decodifica processa il vettore K
sostituendo ad ogni sequenza di 0 e -1 un numero di zeri pari al numero
ottenuto con questo procedimento: si rimpiazza ogni -1 con un 1 e si aggiunge
un 1 come bit più significativo, quindi si scrive il numero in decimale e lo si
decrementa di uno. Ad esempio, se la sequenza è 000(-1)0, la si rimpiazza con
un numero di zeri pari a 1000102 – 1 = 34 – 1 = 33.
K = [4 4 2 2 4 2 0 0 4 0]
Æ
R = [4 4 2 2 4 2 0 0 0 4 0].
3.4 Efficienza
Complessivamente l’intera fase di compressione (omettendo la codifica RunLength) è composta dagli algoritmi C, M e H: chiameremo BW0 il “superalgoritmo” che li esegue in cascata. In output a BW0 vengono restituiti la
stringa di bit Q e l’indice I che avevamo ottenuto in output all’algoritmo C.
Consideriamo l’esempio che ci portiamo sulle spalle dall’inizio di questa
tesina e vediamo se la compressione congiunta alla BWT sia davvero migliore
rispetto alla compressione senza la trasformazione iniziale. Se proviamo a
codificare la stringa S = ‘abracadabra’ direttamente con gli algoritmi M e H in
cascata, usando la stessa inizializzazione della lista Y, ciò che otteniamo è una
stringa di 24 bit, contro gli 88 che sarebbero stati necessari se avessimo
memorizzato la stringa S senza comprimerla (supponendo di usare una
codifica ASCII estesa a 8 bit); quindi lo spazio occupato è in rapporto il 24/88
14
≈ 27% e vengono usati in media circa 2,18 bit per carattere. Tuttavia, come si
può vedere nel paragrafo 3.2.1, con l’aggiunta della fase di trasformazione
della BWT la stringa di bit ottenuta risulta lunga 18 bit. Ciò si traduce in
un’occupazione di spazio del 18/88 ≈ 20%, dunque in media circa 1,64 bit per
carattere: un notevole guadagno di efficienza, soprattutto se consideriamo
stringhe molto più lunghe di quella del nostro esempio.
Con l’aggiunta dell’algoritmo RLE, ovvero utilizzando in sequenza gli
algoritmi C + M + RLE + H per comporre il “super-algoritmo” che d’ora in
poi chiameremo BW0RL, riusciamo a guadagnare un altro bit di spazio,
utilizzando così in media circa 1,55 bit per carattere.
Da notare che nel calcolo dell’efficienza non abbiamo considerato l’overhead
dovuto alla memorizzazione dell’indice I, della lista Y e dell’albero di
Huffman; tuttavia l’effetto sul rapporto di compressione all’aumentare della
lunghezza di S (l’algoritmo viene generalmente applicato a blocchi di dati
dell’ordine dei MB) tende a zero.
Ovviamente una discussione basata su di un esempio non può essere
sufficiente per convincersi dell’efficienza della BWT: per questo motivo nel
prossimo capitolo dimostreremo matematicamente che l’output di BW0 si
avvicina molto in termini di entropia al valore ottimo a cui qualsiasi algoritmo
di compressione aspira.
15
4 Analisi della BWT
La prima vera e propria analisi rigorosa di un algoritmo di compressione che
contenesse al suo interno la BWT fu tracciata soltanto nel 1999 da Giovanni
Manzini, attualmente docente all’Università del Piemonte Orientale di
Alessandria, nel documento [2]. E’ sorprendente come un algoritmo semplice
e immediato da comprendere come la BWT sia in realtà difficile da studiare in
termini di rapporto di compressione. Analisi precedenti, come quelle
effettuate da Kunihiko Sadakane nel 1997-98 e da Michelle Effros nel 1999,
avevano ottenuto risultati interessanti ma non fornivano una rigorosa
dimostrazione teorica delle prestazioni della BWT, poiché erano basate sullo
studio di algoritmi non usati in pratica, consideravano solo l’average-case e
assumevano che l’input provenisse da una sorgente di Markov di ordine finito
(ossia una sequenza di variabili aleatorie discrete definite su uno stesso
alfabeto basata su una catena di Markov a stati discreti), ipotesi non sempre
realistica. Manzini eliminò quest’ultima ipotesi e analizzò l’algoritmo
originale degli stessi Burrows e Wheeler, BW0, non facendo alcuna
assunzione sull’input e ottenendo un limite superiore al caso peggiore in
termini di rapporto di compressione. L’idea che sta alla base dell’analisi di
Manzini è il confronto del rapporto di compressione degli algoritmi basati
sulla BWT con l’entropia empirica di ordine k della stringa in input, una
quantità definita sulla base del numero di occorrenze di ogni carattere o
sequenza di caratteri della stringa e quindi calcolabile per qualsiasi input
senza l’ausilio di alcuna assunzione probabilistica. La forza di questa quantità
nasce dal fatto che fornisce, per ogni k ≥ 0, un limite inferiore alla
compressione che un qualsiasi algoritmo può raggiungere usando per ogni
simbolo un codice che dipende dai k simboli che lo precedono. Analizziamo
quindi, seguendo lo studio fatto da Manzini, l’algoritmo BW0, e dimostriamo
che il rapporto di compressione raggiunto al caso peggiore è, a meno di un
fattore costante, lineare rispetto all’entropia di ordine k, per ogni k ≥ 0. Il
risultato sarà ottenuto attraverso un’analisi della codifica MTF: vedremo che
MTF trasforma una stringa localmente omogenea in una stringa globalmente
omogenea.
Per prima cosa però introduciamo la quantità nota come entropia empirica di
una stringa.
4.1 L’entropia empirica di una stringa
La definizione di entropia empirica (vedi [2]) assomiglia a quella di entropia
in uno scenario probabilistico (Shannon, 1948), ma può essere definita al
contrario di quest’ultima per qualsiasi stringa e può essere usata per misurare
la performance degli algoritmi di compressione senza alcuna assunzione
sull’input. E’ detta empirica appunto perché, data la stringa, è una quantità
fissata a priori.
Sia S una stringa di lunghezza n definita sull’alfabeto X = {α1, … , αh}, e sia
ni il numero di occorrenze del carattere αi in S. L’entropia empirica di ordine
zero della stringa S è definita in questo modo:
16
h
H 0 ( S ) = −∑
i =1
ni
⎛n
log ⎜ i
n
⎝n
⎞
⎟,
⎠
(1)
dove assumiamo 0 log 0 = 0 (tutti i logaritmi che appariranno in questa analisi
saranno in base 2). Dalla teoria sappiamo che la massima compressione che si
può raggiungere con un codice fisso antitrasformabile a lunghezza variabile è
uguale a |S|H0(S), usando –log(ni/n) bit per il carattere αi. Possiamo comunque
ottenere una compressione maggiore se il codice che assegniamo ad ogni
carattere dipende dai k caratteri che lo precedono. Per ogni parola w ∈ Xk, sia
wS la stringa ottenuta dalla concatenazione dei singoli caratteri che precedono
ogni occorrenza di w in S (ad esempio, se S = ‘abracadabra’, si ha (bra)S =
‘aa’). Notiamo che la lunghezza di wS è uguale al numero di occorrenze di w
in S, o questo numero meno uno se w è un prefisso di S. Definiamo dunque
l’entropia empirica di ordine k della stringa S come il valore:
H k (S ) =
1
S
∑
w∈X k
wS H 0 ( wS ) .
(2)
Per codici che dipendono dai k caratteri più recenti, il valore |S|Hk(S) è un
limite inferiore alla compressione raggiungibile. Un risultato fondamentale,
valido anche nel caso dell’entropia di Shannon, è il seguente: per ogni stringa
S e ogni k ≥ 0, Hk+1(S) ≤ Hk(S).
Esempio: vediamo il tutto applicato alla stringa S = ‘abracadabra’. Per
k = 0 abbiamo:
5 2
2 1
1 1
1 2
2⎞
⎛5
H 0 ( S ) = − ⎜ log + log + log + log + log ⎟ ≈ 2, 04
11 11
11 11
11 11
11 11
11 ⎠
⎝ 11
mentre per k = 1 abbiamo aS = ‘rcdr’, bS = ‘aa’, cS = ‘a’, dS = ‘a’, rS =
‘bb’; calcolando l’entropia empirica di ordine zero di ognuna di queste
sottosequenze abbiamo H0(rcdr) = 3/2, H0(aa) = 0, H0(a) = 0 e H0(bb) =
0; quindi l’entropia empirica di ordine uno è:
H1 ( S ) =
1
4 3 6
aS H 0 ( a S ) ) = × = .
(
S
11 2 11
Notiamo che l’entropia empirica di ordine k risulta un buon limite inferiore
solo quando |S| >> k: banalmente, nell’esempio precedente, se prendiamo k =
10 = |S| - 1 abbiamo soltanto (bracadabra)S = ‘a’ con H0(a) = 0; quindi
l’entropia empirica di ordine 10 risulta essere uguale a 0, un limite inferiore
che non ci dà nessuna informazione. Tuttavia, anche quando |S| >> k il limite
inferiore può non essere buono. Basti pensare alla stringa S = ‘(ab)ncc’:
abbiamo aS = bn-1, bS = an e cS = ‘bc’, da cui si ricava |S|H1(S) = (n – 1)H0(bn-1)
+ nH0(an) + 2H0(bc) = 2. Dunque la quantità |S|H1(S) non dipende da n; se n è
infinitamente grande risulta chiaro che il nostro limite inferiore è molto
scarso. Per questi motivi esiste un’altra quantità chiamata entropia empirica
modificata, la cui definizione per l’ordine zero è uguale a H0(S) tranne quando
|S| ≠ 0 e H0(S) = 0, nel cui caso vale
17
H 0* ( S ) = (1 + ⎢⎣ log S ⎥⎦ ) / S
(3)
ossia è pari al numero di bit richiesti per rappresentare |S| in binario, diviso lo
stesso |S|. La formula per l’entropia empirica modificata di ordine k ≥ 1,
Hk*(S), richiede diverse considerazioni che vanno al di là degli scopi di questa
tesina. Infatti poiché Hk ha una definizione molto più semplice e Hk(S) ≤
Hk*(S) per ogni stringa S, in molte situazioni risulta preferibile lavorare con
Hk. E’ proprio questo che noi faremo nell’analisi di BW0.
4.2 Analisi dell’algoritmo BW0
Consideriamo L, ossia la permutazione di S, output dell’algoritmo C. L ha
l’interessante proprietà che per ogni sottostringa w di S i caratteri che
precedono w in S risultano raggruppati in L, poiché tutte le rotazioni che
cominciano con w sono consecutive nella matrice M riordinata. Dunque L
contiene al suo interno una sottostringa che è una permutazione della stringa
wS. Richiamando la definizione (2) di entropia empirica di ordine k, possiamo
scrivere
L=
∪π
w
( wS ),
(4)
w∈ X k
dove πw(wS) è una permutazione della stringa wS. In realtà L contiene anche
gli ultimi k simboli di S, che non appartengono ad alcuna wS; tuttavia per
semplicità ne ignoreremo le presenza (è infatti ragionevole farlo per k << |S|).
Poiché la permutazione di una stringa non varia la sua entropia di ordine zero,
ossia H0(wS) = H0(πw(wS)), se avessimo un algoritmo “ideale” A tale che per
ogni partizione S1S2…St di S
t
A( S ) ≤ ∑ si H 0 ( si ) ,
(5)
i =1
allora dalla (2) avremmo A(L) ≤ Hk(S): in altre parole, combinando
l’algoritmo A con la BWT saremmo in grado di comprimere qualsiasi stringa
fino alla sua entropia empirica di ordine k, per ogni k ≥ 0. Possiamo quindi
ridurre il problema della compressione della stringa nella sua globalità al
problema di comprimere distinte porzioni della stringa in input fino alla loro
entropia di ordine zero. Seppur non esistano algoritmi ideali che soddisfino la
(5), vedremo che per gli algoritmi che abbiamo introdotto nel capitolo 3 la
dimensione dell’output è molto vicina alla quantità che sta a destra nella
disequazione (5); usando questi algoritmi per processare l’output
dell’algoritmo C otteniamo un rapporto di compressione vicino all’entropia di
ordine k.
Prima di addentrarci nella vera e propria analisi matematica del nostro
algoritmo, facciamo un paio di considerazioni sugli algoritmi M (Move-ToFront) e H (Huffman). Per quanto riguarda il MTF, il vantaggio che otteniamo
dal suo uso è il seguente: guardando la (4), ogni stringa wS contiene
ragionevolmente solo pochi caratteri distinti, ma i caratteri che appaiono in
πw(wS) possono benissimo essere diversi da quelli contenuti in πw(wS’). Lo
18
schema MTF trasforma sia πw(wS) che πw(wS’) in stringhe contenenti un
grande numero di piccoli interi, ovvero codifica la stringa L, localmente
omogenea, in una stringa globalmente omogenea della stessa lunghezza, R.
Come appena detto, R contiene un vasto numero di piccoli interi positivi o
nulli; per esempio, se assumiamo che S sia un testo in lingua inglese, i
caratteri contenuti in R sono tipicamente per più del 50% degli zeri. La
codifica di Huffman, così come qualsiasi altra codifica di ordine zero, utilizza
–log(ni/N) bit per ogni carattere che appare ni volte su N in R, e quindi
raggiunge un rapporto di compressione vicino ad H0(R). Ricordando che
l’output dell’algoritmo H è la stringa Q, non analizzeremo le prestazioni della
codifica di Huffman analiticamente, ma assumeremo che il proprio rapporto di
compressione sia molto vicino all’entropia di ordine zero dell’input; più
precisamente, che esista una costante μ tale che, per ogni stringa S,
Q = H (S ) ≤ S H 0 (S ) + μ S .
(6)
Per la codifica di Huffman la disequazione è valida per μ = 1; si potrebbero
usare altre codifiche come quella aritmetica, la quale in alcune semplici
implementazioni soddisfa la (6) per μ = 10-2, tuttavia al fine della nostra
analisi ciò ha relativamente poca importanza. Passiamo ora all’introduzione di
alcuni lemmi necessari per giungere al nostro obiettivo.
LEMMA 4.2.1 Per i = 1, … , t, sia si una stringa sull’alfabeto {0, 1}. Sia mi
il numero di occorrenze di ‘1’ in si. Se, per i = 1, … , t, mi ≤ |si|/2, allora
t
1
s1 st H 0 ( s1 st ) ≤ 3∑ si H 0 ( si ) +
s1 st .
40
i =1
Dimostrazione: sia h(x) = -x log(x) – (1 – x)log(1 – x), s = s1…st, e r = (m1 +
… + mt)/|s|. Poiché H0(si) = h(mi/|si|) =
– (mi/|si|)log(mi/|si|) – ((|si|-mi)/|si|)
log((|si|-mi)/|si|), dove |si|-mi è il numero di occorrenze di ‘0’ in si, la nostra tesi
è equivalente, dividendo per |s| da entrambi i lati, a
t
h(r ) ≤ 3∑
i =1
si ⎛ mi
h⎜
s ⎜⎝ si
⎞ 1
⎟⎟ + .
⎠ 40
Ora, dalla nostra ipotesi iniziale su mi, poiché per 0 ≤ x ≤ 1/2 abbiamo h(x) ≥
2x, possiamo scrivere:
t
∑
i =1
si ⎛ mi
h⎜
s ⎜⎝ si
⎞
⎛ s1 m1
+
⎟⎟ ≥ 2 ⎜⎜
s
s
1
⎠
⎝
+
st mt
s st
⎞
m1 + + mt
= 2r
⎟⎟ = 2
s
⎠
così la tesi diventa:
h( r ) ≤ 6r +
1
.
40
Si può verificare che la funzione 6r – h(r) + 1/40 ha un minimo positivo in
2,63·10-3 per r = 1/65, e il lemma è dimostrato. □
19
Il lemma appena dimostrato fornisce dunque un limite superiore a |s|H0(s) in
termini della somma ∑i |si|H0(si), per una stringa s definita sull’alfabeto {0, 1}
assumendo che ogni si contenga più ‘0’ che ‘1’. Ora facciamo un passo in
avanti, trovando un limite superiore alla quantità |s1s2|H0(s1s2) con s1 e s2
stringhe definite sull’alfabeto {0, 1} non omogenee, ovvero con s1 che
contiene più ‘1’ che ‘0’ e s2 che contiene più ‘0’ che ‘1’.
LEMMA 4.2.2 Siano s1 e s2 due stringhe definite sull’alfabeto {0, 1}. Sia
inoltre x1 il numero di ‘1’ in s1 e y1 il numero di ‘1’ in s2. Assumendo che x1 >
|s1|/2 e che y1 ≤ |s2|/2, si ha
s1s2 H 0 ( s1s2 ) ≤ 4.85 s1 + s2 H 0 ( s2 ) +
1
s1s2 .
20
Dimostrazione: introduciamo, per x, y ≥ 0, la funzione G, definita come
G ( x, y ) = − x log
x
y
,
− y log
x+ y
x+ y
(7)
con le convenzioni G(x, 0) = G(0, y) = G(0, 0) = 0. Si possono verificare
facilmente le seguenti proprietà:
G ( λ x, λ y ) = λ G ( x, y ) ,
(8)
0 ≤ G ( x, y ) ≤ x + y ,
(9)
G( x, y + z ) ≤ G( x + y, z ) + G( x, y).
(10)
Ora, il numero di ‘0’ in s1 e s2 sono, rispettivamente, x0 = |s1| - x1 e y0 = |s2| y1; esprimendo l’entropia nei termini della funzione G, abbiamo
⎛
⎞
⎛
⎞
x0 + y0
x1 + y1
s1s2 H 0 ( s1s2 ) = −( x0 + y0 ) log ⎜
⎟ − ( x1 + y1 ) log ⎜
⎟=
⎝ x0 + y0 + x1 + y1 ⎠
⎝ x0 + y0 + x1 + y1 ⎠
= G ( x0 + y0 , x1 + y1 )
e
⎛ y0 ⎞
⎛ y1 ⎞
s2 H 0 ( s2 ) = − y0 log ⎜
⎟ − y1 log ⎜
⎟ = G ( y0 , y1 );
⎝ y0 + y1 ⎠
⎝ y0 + y1 ⎠
per cui la nostra tesi diventa:
G ( x0 + y0 , x1 + y1 ) ≤ 4.85( x1 + x0 ) + G ( y0 , y1 ) +
1
( x0 + y0 + x1 + y1 ).
20
Sia adesso r = y1(x0/y0). Sostituendo nella (10) x con x0 + y0, y con y1 + r, e z
con x1 – r, otteniamo
G( x0 + y0 , x1 + y1 ) ≤ G ( x0 + y0 + y1 + r , x1 − r ) + G( x0 + y0 , y1 + r ).
20
Applicando la (8) e la (9),
⎛ x ⎞
⎛
y ⎞
G ( x0 + y0 , y1 + r ) = ⎜ 1 + 0 ⎟ G ( y0 , y1 ) ≤ G ( y0 , y1 ) + x0 ⎜1 + 1 ⎟ = G ( y0 , y1 ) + x0 + r.
y0 ⎠
y0 ⎠
⎝
⎝
A questo punto per completare la dimostrazione basta provare che:
G ( x0 + y0 + y1 + r , x1 − r ) ≤ 4.85( x1 − r ) +
1
( x0 + y0 + x1 + y1 ) :
20
espandendo G usando la (7), dividendo ambo i lati per x1 – r e definendo t =
(x0 + y0 + y1 + r)/(x1 – r), la disequazione (omettiamo i diversi passaggi
intermedi) diventa:
(1 + t ) log(1 + t ) − t log(t ) ≤ 4.85 +
1+ t
.
20
Si può verificare che la funzione (1 + t) log(1 + t) – t log(t) – (1 + t)/20 ha il
suo punto massimo in –log(21/20 – 1), quantità minore di 4.85: quindi il lemma
è dimostrato. □
Ora dobbiamo spostarci al caso più generale in cui le stringhe siano definite su
un qualsiasi alfabeto più vasto. Data una stringa s definita sull’alfabeto {0, …
, h – 1}, denotiamo con s01 la stringa in cui ogni simbolo diverso da zero è
sostituito con un ‘1’ (ad esempio, (44224200040)01 = 11111100010). Per una
stringa L definita invece su un qualsiasi alfabeto, definiamo con R = mtf(L) la
stringa ottenuta da L usando il Move-To-Front con una lista iniziale Y indotta
da L, ovvero l’i-esimo carattere di Y è l’i-esimo carattere di L (senza contare
occorrenze multiple dello stesso carattere).
Per ogni stringa L definita su un qualsiasi alfabeto,
LEMMA 4.2.3
R 01 H 0 ( R 01 ) ≤ 2 L H 0 ( L ).
Dimostrazione: per ogni i = 1, … , h, sia ni il numero di occorrenze del
carattere αi in L. Possiamo assumere senza perdere in generalità che il
carattere più frequente di L sia α1, ovvero n1 ≥ ni per i = 2, … , h. Se n1 ≤ |L|/2,
allora
h
⎛ L⎞ h
L H 0 ( L) = ∑ ni log ⎜ ⎟ ≥ ∑ ni log 2 = ∑ ni = L ,
i =1
i =1
⎝ ni ⎠ i=1
h
e la tesi è verificata poiché |R01|H0(R01) ≤ |L|. Se invece n1 > |L|/2: sia r = n2 +
… + nh e sia β = n1/|L|. Abbiamo dunque
h
⎛ L⎞
⎛ L⎞
n
=
r
L
−
ni log ni ≥ r log ⎜ ⎟ ,
log
log
⎜
⎟
∑
∑
i
i =2
i=2
⎝ ni ⎠
⎝ r ⎠
h
(11)
21
da cui ricaviamo
h
⎛ L
L H 0 ( L) = ∑ ni log ⎜
i =1
⎝ ni
⎞
⎛ L
⎟ ≥ n1 log ⎜
⎠
⎝ n1
⎞
⎛ L⎞
⎟ + r log ⎜ ⎟ = −n1 log β − r log(1 − β ). (12)
⎠
⎝ r ⎠
Ora, denotiamo con m0 il numero di ‘0’ in R01. Notiamo che, per la
definizione del Move-To-Front data in precedenza, il primo simbolo di R è 0;
inoltre, c’è uno ‘0’ in R per ogni coppia di caratteri identici consecutivi in L.
Dunque se consideriamo esclusivamente il carattere α1, esso genera n1 – r ‘0’
in R, e ciò implica m0 ≥ n1 – r. Consideriamo ora l’algoritmo di ordine zero
che codifica R01 usando –log(β) bit per il carattere ‘0’ e –log(1 – β) bit per il
carattere ‘1’. Dalla teoria sappiamo che le lunghezze dei codici ottenuti
soddisfano la disuguaglianza di Kraft, ovvero
⎛1⎞
⎜ ⎟
⎝2⎠
− log( β )
⎛1⎞
+⎜ ⎟
⎝2⎠
− log(1− β )
≤ 1.
(13)
Possiamo dunque scrivere
⎛1⎞
⎛ 1 ⎞
R 01 H 0 ( R 01 ) ≤ m0 log ⎜ ⎟ + ( L − m0 ) log ⎜
⎟
⎝β ⎠
⎝1− β ⎠
⎛ 1− β ⎞
⎛ 1 ⎞
= m0 log ⎜
⎟ + L log ⎜
⎟
⎝ β ⎠
⎝ 1− β ⎠
⎛ 1− β ⎞
⎛ 1 ⎞
≤ (n1 − r ) log ⎜
⎟ + (n1 + r ) log ⎜
⎟
⎝ β ⎠
⎝ 1− β ⎠
= n1 log β − 2r log(1 − β ) + r log β
≤ 2(−n1 log β − r log(1 − β )).
Dalla (12) vediamo che l’ultimo termine è più piccolo di 2|L|H0(L), dunque il
lemma è dimostrato. □
Introduciamo ora un risultato dimostrato nel [3]: per i = 0, … , h – 1, sia mi il
numero di occorrenze del carattere i in R. Si ha
h −1
∑ m log(i + 1) ≤ L H
i =0
i
0
( L).
(14)
Useremo questo risultato nella dimostrazione del prossimo lemma.
LEMMA 4.2.4
Per i = 1, … , t, sia Ri = mtf(Li), e sia P = R1…Rt. Si ha
t
P H 0 ( P) ≤ P 01 H 0 ( P 01 ) + 2∑ Li H 0 ( Li ).
i =1
Dimostrazione: per j = 0, … , h – 1, sia mj il numero di occorrenze del
carattere j in P, e mj(i) il numero di occorrenze del carattere j in Ri; inoltre sia
22
β = m0/|P|. Consideriamo l’algoritmo che codifica P usando –log(β) bit per il
carattere ‘0’, e –log(1 – β) + 2log(j + 1) bit per il carattere j, per j = 1, … , h –
1. Poiché anche in questo caso le lunghezze dei codici ottenuti soddisfano la
disuguaglianza di Kraft, ossia
⎛1⎞
⎜ ⎟
⎝2⎠
− log( β )
h −1
⎛1⎞
+ ∑⎜ ⎟
j =1 ⎝ 2 ⎠
− log(1− β )+ 2 log( j +1)
≤ 1,
(15)
possiamo scrivere
h −1
P H 0 ( P) ≤ − m0 log( β ) − ( P − m0 ) log(1 − β ) + 2∑ m j log( j + 1)
j =1
t
h−1
= P 01 H 0 ( P 01 ) + 2∑∑ m(ji ) log( j + 1).
i =1 j =1
Ora ci resta solo da dimostrare che ∑j=1h-1 mj(i)log(j + 1) ≤ |Li|H0(Li): ma
questo ce lo dice la (14), con l’estremo della somma i = 0 che non contribuisce
alla quantità a sinistra della disequazione. Il lemma è dimostrato. □
Con quest’ultimo lemma abbiamo trovato un limite superiore all’entropia di P
= R1…Rt, con Ri = mtf(Li). Questo risultato però non è ancora sufficiente: per
il nostro risultato finale sarà necessario, come vedremo più avanti, limitare
l’entropia di R = mtf(L) = mtf(L1…Lt). La differenza è molto sottile, ed è
dovuta al cambiamento di stato della lista Y. Infatti quando viene calcolato
mtf(L1…Lt), noi codifichiamo Li (per i > 1) usando la Y indotta dalla
scansione delle partizioni precedenti L1…Li-1; mentre quando calcoliamo
separatamente i singoli mtf(Li), ognuna delle Li induce una diversa Y in base
ai caratteri in essa contenuti. Tuttavia, la differenza dello stato iniziale della
lista Y influenza soltanto la codifica della prima occorrenza di ogni simbolo in
Li: pertanto la codifica di Li in R differisce da mtf(Li) al più in h posizioni.
Questa osservazione sarà necessaria per dimostrare l’ultimo lemma.
LEMMA 4.2.5 Per i = 1, … , t, siano Ri = mtf(Li), L = L1…Lt, P = R1…Rt,
e R = mtf(L). Allora
R H 0 ( R) ≤ P
01
t
L
i =1
300
H 0 ( P ) + 2∑ Li H 0 ( Li ) +
01
+ t (9 + 2h log h).
Dimostrazione: dall’osservazione precedente, ricaviamo che le stringhe R e P
differiscono in al più th posizioni (h posizioni per ogni i = 1, … , t). Quindi,
ripetendo la dimostrazione del Lemma 4.2.4, otteniamo
t
R H 0 ( R ) ≤ R 01 H 0 ( R 01 ) + 2∑ Li H 0 ( Li ) + 2th log h.
i =1
Ora ci resta solo da provare che
23
R 01 H 0 ( R 01 ) − P 01 H 0 ( P 01 ) −
L
≤ 9t.
300
(16)
Osserviamo che, a parte il primo carattere di R, ogni ‘0’ in R corrisponde a
una ripetizione dello stesso carattere in L. Per quanto riguarda gli zeri ci può
essere quindi una differenza tra R e P soltanto nelle posizioni corrispondenti il
primo carattere di ogni Li per i > 1. In queste posizioni, c’è sempre uno ‘0’ in
P mentre in R c’è uno ‘0’ solo se il primo carattere di Li è uguale all’ultimo
carattere di Li-1. Dunque P contiene r ‘0’ in più di R, con r < t. Denotiamo con
n0 il numero di ‘0’ e con n1 il numero di ‘1’ in R01: usando la (7) e la (10)
otteniamo
R 01 H 0 ( R 01 ) − P 01 H 0 ( P 01 ) −
L
n +n
n +r
.
= G (n0 , n1 ) − G (n0 + r , n1 − r ) − 0 1 ≤ G (n0 , r ) − 0
300
300
300
Ora per dimostrare la (16) ci basta provare che l’ultima espressione è limitata
superiormente da 9r (ricordiamo r < t). Usando la (7) e ponendo t = n0/r,
otteniamo, dopo diversi passaggi che non riportiamo,
G (n0 , r ) n0 + r
1+ t
.
−
= (1 + t ) log(1 + t ) − t log t −
r
300r
300
Si può verificare che la parte destra dell’equazione ha un massimo in –
log(21/300 – 1), minore di 9, e il lemma è dimostrato. □
Possiamo finalmente formulare e dimostrare il primo teorema della nostra
analisi.
TEOREMA 4.2.6 Sia L una stringa definita su un alfabeto X = {α1, … , αh},
e R = mtf(L). Per ogni partizione L1…Lt di L, si ha
t
R H 0 ( R ) ≤ 8∑ Li H 0 ( Li ) +
i =1
2
L + t (2h log h + 9).
25
(17)
Dimostrazione: per i = 1, … , t, sia Ri = mtf(Li) e P = R1…Rt. Dal Lemma
4.2.5 deduciamo che è sufficiente dimostrare che
t
P 01 H 0 ( P 01 ) ≤ 6∑ Li H 0 ( Li ) +
i =1
3
R.
40
(18)
Assumiamo innanzitutto che in ogni Ri01 il numero di ‘0’ sia almeno pari al
numero di ‘1’. Dal Lemma 4.2.1 otteniamo
t
P 01 H 0 ( P 01 ) ≤ 3∑ Ri01 H 0 ( Ri01 ) +
i =1
e la (17) segue dal Lemma 4.2.3:
Ri01 H 0 ( Ri01 ) ≤ 2 Li H 0 ( Li ).
1
R
40
24
Se invece il numero di ‘0’ in alcune Ri01 è minore rispetto al numero di ‘1’:
assumiamo che tali stringhe siano R101, R201, … , Rk01. Dal Lemma 4.2.2 e dal
Lemma 4.2.1 otteniamo
P 01 H 0 ( P 01 ) ≤ 4.85 R101
k
Rk01 + Rk01+1
Rt01 H 0 ( Rk01+1
Rt01 ) +
1 01
P
20
1
1
P+
P
40
20
i = k +1
t
3
Li + 6 ∑ Li H 0 ( Li ) +
P.
40
i = k +1
t
≤ 4.85∑ Li + 3 ∑ Li H 0 ( Li ) +
i =1
k
≤ 4.85∑
i =1
Per j = 1, … , k, sia infine mj il numero di occorrenze del carattere più
frequente in Lj. L’ipotesi fatta su Rj01 implica mj ≤ (3/4)|Lj| (infatti se il
carattere avesse frequenza maggiore del 75% la sua trasformazione con MTF
avrebbe in più del 50% delle posizioni degli ‘0’); dunque, usando la (12),
otteniamo
⎛ Lj ⎞
⎛ Lj
⎟ + L j − m j log ⎜
L j H 0 ( L j ) ≥ m j log ⎜
⎜ mj ⎟
⎜ Lj − m j
⎝
⎠
⎝
(
)
⎞
⎟ ≥ γ Lj ,
⎟
⎠
(19)
Dove γ = -[(3/4)log(3/4) + (1/4)log(1/4)]. Poiché (4.85/γ) ≤ 6, abbiamo
4.85|Lj| ≤ 6|Lj|H0(Lj) e il teorema è dimostrato. □
Il teorema appena dimostrato stabilisce che se usiamo il MTF l’entropia della
stringa in output non è molto distante da |L|H0(L). Nonostante la costante 8 sia
elevata per convincersi della bontà “pratica” dell’algoritmo, questo limite
superiore ha una validità matematica essendo il primo limite non banale
scoperto per BW0. Si può dimostrare che un limite del tipo |R|H0(R) ≤
λ[∑i|Li|H0(Li)] + c non è valido: pensando ad esempio alle stringhe L1 = an e
L2 = bn, avremmo |R|H0(R) ≤ c, che per n tendente a +∞ non può stare in
piedi. Passiamo dunque al risultato finale della nostra analisi di BW0:
TEOREMA 4.2.7 Per ogni stringa S definita su un alfabeto X = {α1, … , αh}
e ogni k ≥ 0, si ha
2 ⎞
⎛
BW 0( S ) ≤ 8 H k ( S ) + ⎜ μ + ⎟ S + h k (2h log h + 9),
25 ⎠
⎝
dove μ è definita come nella (6).
Dimostrazione: sia L = bwt(S) e R = mtf(L). Dalla (6) abbiamo che
BW 0( S ) = H ( R ) ≤ R H 0 ( R ) + μ S .
Dalle proprietà della BWT sappiamo che esiste un t ≤ hk ed una partizione
L1…Lt di L tali che
(20)
25
t
H k ( S ) = ∑ Li H 0 ( Li ),
i =1
e il teorema segue dalla (17). □
Abbiamo dimostrato dunque che l’algoritmo BW0 produce un rapporto di
compressione lineare rispetto all’entropia di ordine k, a meno di un fattore
costante, per ogni k ≥ 0.
Manzini analizzò anche l’algoritmo BW0RL, e il risultato che ottenne fu il
seguente: per ogni k ≥ 0 esiste una costante gk tale che per ogni stringa S si ha
BW 0 RL ( S ) ≤ (5 + 3μ ) H k* ( S ) + g k .
Egli dimostrò questo teorema usando la definizione di entropia empirica
modificata e il concetto di “λ-ottimalità locale”. Per l’analisi completa
dell’algoritmo BW0RL rimandiamo al [2], capitolo 5.
Nel prossimo capitolo confronteremo le prestazioni in termini di velocità e di
compressione dell’algoritmo BW0 con quelle di altri noti algoritmi di
compressione.
26
27
5 Prestazioni della BWT
Un buon algoritmo di compressione deve avere due caratteristiche principali:
avere un rapporto di compressione il più vicino possibile all’entropia della
stringa in input, ed essere veloce. I risultati che seguono prendono in esame
l’algoritmo BW0RL, con un’unica piccola modifica: viene calcolato un nuovo
albero di Huffman per ogni blocco di 16kB in input, invece che un albero
singolo per l’intero input.
Figura 3: i 14 files del Calgary Compression corpus ([4]).
28
Applichiamo l’algoritmo a quattordici files del Calgary Compression corpus,
una collezione di esempi di input, testuali (in lingua inglese) e non, che
spaziano tra i vari tipi di files di fronte ai quali un qualsiasi programma di
compressione potrebbe trovarsi. Più precisamente, vengono rappresentati nove
tipi diversi di testo, alcuni dei quali con più di un’istanza al fine di confermare
la consistenza dello schema di compressione in esame:
•
•
•
•
•
•
•
•
•
un romanzo, book1;
un libro scientifico, book2, e due articoli scientifici, paper1 e paper2;
una bibliografia, bib;
una raccolta di articoli giornalistici, news;
tre programmi in linguaggio artificiale, progc, progl e progp;
una trascrizione di una sessione terminale, trans;
due files di codice eseguibile, obj1 e obj2;
un insieme di dati di tipo geofisico, geo;
un’immagine bitmap in bianco e nero, pic.
I primi sei tipi di testo usano la codifica ASCII, gli ultimi tre no. Il file geo è
particolarmente difficile da comprimere in quanto contiene un vasto range di
valori; al contrario, il file pic è facile da comprimere in quanto lo spazio
bianco dell’immagine è rappresentato da lunghe sequenze di zeri. Maggiori
informazioni si trovano sul sito web [9].
Nella tabella seguente vediamo come il nostro algoritmo si comporta con i
quattordici files appena descritti. La misurazione del tempo di CPU (piuttosto
che il tempo reale, al fine di escludere le operazioni di I/O) è stata effettuata
su una DECstation 5000/200 con processore MIPS R3000 sincronizzato a
25MHz con cache di 64kB.
Figura 4: risultato della compressione dei 14 files del Calgary Compression
corpus ([1]).
29
Notiamo che effettivamente il file geo è quello che viene compresso di meno,
mentre il file pic compresso occupa meno di un bit per carattere. Per quanto
riguarda la velocità di compressione, la fase di codifica è molto più onerosa in
termini di calcolo rispetto a quella di decodifica (come vedremo ciò sarà
dovuto al problema dell’ordinamento delle rotazioni dell’input). Nella
prossima tabella invece vediamo come varia la compressione in rapporto alla
dimensione dell’input, prendendo come file di test book1 e l’intero Hector
corpus (un altro dei tanti corpus esistenti che contiene circa 100 MB di testo
in inglese).
Figura 5: risultato della variazione della dimensione del blocco in input sulla
compressione ([1]).
Come si può vedere la compressione migliora all’aumentare del blocco in
input; tuttavia, se aumentiamo la dimensione del blocco sopra i dieci milioni
di caratteri il miglioramento risulta esiguo. Se la dimensione del blocco
aumentasse indefinitamente, potremmo pensare che prima o poi la
compressione raggiungerà il punto di ottimo: questo però non è possibile, in
quanto il codificatore MTF introduce sempre qualche piccola perdita. Si
potrebbe raggiungere l’ottimo asintoticamente con un algoritmo migliore, ma
questo non avrebbe molta importanza pratica.
Paragoniamo infine le prestazioni del nostro algoritmo (Alg-C/D in figura) sul
Calgary Compression corpus con altri noti programmi di compressione,
prendendo la media dei bit per carattere di tutti i singoli file compressi con lo
stesso programma. Gli altri programmi presi in esame usano una versione
dell’algoritmo LZW (compress), una versione dell’algoritmo LZ77 (gzip) e un
compressore statistico (comp-2).
Figura 6: confronto di BW0RL con altri programmi di compressione ([1]).
30
Notiamo che il rapporto di compressione di BW0RL è molto vicino a quella
raggiunto da un compressore statistico, ma allo stesso tempo il programma ha
velocità di codifica e decodifica molto maggiori di quest’ultimo, paragonabili
a quelle degli algoritmi basati sulla tecnica di Lempel e Ziv. Si può dire quindi
che l’introduzione della BWT ha preso i pregi dell’una e dell’altra tecnica.
In conclusione, possiamo dunque tranquillamente affermare che gli algoritmi
che usano la BWT si comportano bene su input sia testuali che non,
soprattutto su blocchi di decine di migliaia di caratteri.
31
6 Implementazione efficiente della BWT
In quest’ultimo capitolo diamo un’implementazione degli algoritmi visti nel
capitolo 2 e 3, soffermandoci con particolare attenzione sul problema
dell’ordinamento delle rotazioni di S nell’algoritmo C.
6.1 Il problema dell’ordinamento
Il tempo speso per effettuare l’ordinamento delle rotazioni dell’input
nell’algoritmo C è il fattore più importante per la velocità di compressione.
Risulta chiaro dunque che la scelta di un algoritmo di ordinamento rispetto a
un altro può determinare la qualità dell’algoritmo di compressione BWTbased in termini di velocità. Un’applicazione semplice di QuickSort (vedi [7],
pag. 501-507) avrebbe nel caso peggiore complessità quadratica in relazione
alla dimensione dell’input, e sarebbe quindi poco efficiente. Un modo più
veloce per implementare l’algoritmo C è quello di ridurre il problema di
ordinare le rotazioni al problema di ordinare i suffissi di S: infatti se formiamo
una stringa S’ concatenando alla fine di S un carattere non appartenente a X
lessicograficamente minore rispetto a tutti i caratteri in X , chiamato EOF,
notiamo che i confronti tra i suffissi di S’ risultano equivalenti ai confronti
delle rotazioni di S’. Infatti, date due rotazioni, il confronto termina
necessariamente alla prima occorrenza di EOF, in quanto esso appare una sola
volta in entrambe le stringhe in posizioni differenti: ciò si riduce dunque al
confronto tra suffissi di S’. Ciò può essere svolto in tempo e spazio lineare
nella somma delle lunghezze delle rotazioni costruendo un Suffix Tree ([7],
pag. 562-565), che poi potrà essere percorso in ordine lessicografico per
formare la lista dei suffissi ordinati. Il problema di quest’approccio è che gli
algoritmi che si basano sul Suffix Tree necessitano più di quattro parole
macchina di spazio per carattere in input. L’algoritmo più veloce sperimentato
da Burrows e Wheeler nel [1] è una variante di QuickSort che genera la lista
ordinata dei suffissi: usa meno spazio del Suffix Tree (due parole macchina
per carattere in input) e la sua performance è molto migliore rispetto a
quest’ultimo su input testuali, anche se per altri tipi di dati le prestazioni
possono decadere. Presentiamo ora quest’algoritmo, che chiameremo
semplicemente Sort.
6.1.1 Algoritmo Sort
L’algoritmo ordina i suffissi della stringa S di N caratteri, applicando una
versione modificata di QuickSort. La versione più efficiente è la seguente:
innanzitutto formiamo la stringa S’, concatenando alla fine di S il carattere
EOF. Inizializziamo un vettore V di N + 1 interi che contiene gli indici di tutte
le rotazioni di S’, con V[i] = i per i = 0, ... , N. Quando l’algoritmo terminerà,
V[i] sarà l’indice dell’i-esima rotazione di S’ in ordine lessicografico. Sia ora
k il numero di caratteri che entrano in una parola macchina. Partendo da S’
costruiamo un ulteriore vettore W di N+1 parole macchina in modo tale che
W[i] sia costituito dai caratteri S[i, ... , (i+k-1)mod (N+1)]. Ad esempio, se k =
4 e S’ = ‘abracadabra§’ (dove § è il carattere EOF),
32
W = [‘abra’ ‘brac’ ‘raca’ ‘acad’ ‘cada’ ‘adab’
‘dabr’ ‘abra’ ‘bra§’ ‘ra§a’ ‘a§ab’ ‘§abr’].
Il vettore W è utilizzato per effettuare velocemente i confronti tra le rotazioni
di S: infatti ogni confronto tra due parole macchina corrisponde a quello tra
due serie di k caratteri. Terminata la fase di inizializzazione, ordiniamo le
rotazioni puntate da V con l’algoritmo RadixSort ([7], pag. 516-517),
utilizzando come chiave d’ordinamento i primi due caratteri di ciascuna
rotazione e ottenendo così un ordinamento parziale sui primi due caratteri. Per
ottenere l’ordinamento totale di tutte le rotazioni, per ogni carattere ch in
ordine lessicografico eseguiamo in ordine i seguenti passi:
• Per ogni carattere ch' ≠ ch dell'alfabeto, applichiamo QuickSort ai suffissi
indicizzati da V che iniziano per chch'.
• Ordiniamo, se esistono, i suffissi che iniziano con chch che sono
lessicograficamente minori di una sequenza infinita di caratteri ch. Per
esempio, il suffisso 'bbba' è minore di 'bb…bb'. Questo viene fatto sfruttando i
suffissi che iniziano per chch' tali che ch' < ch. Poiché per il passo precedente
tali suffissi sono già stati ordinati, possiamo effettuare l'ordinamento di chch
in modo più veloce con un semplice ciclo. Sia V[i] il più piccolo suffisso in
ordine lessicografico che inizia per ch e sia j il valore più piccolo tale che il
suffisso V[j] inizi per chch. Il ciclo da effettuare è il seguente:
while (i ≠ j) do
if (V[i] > 0 and S[V[i] - 1] = ch) then
V[j] Å V[i] – 1;
j Å j + 1;
i Å i + 1;
end;
Questo ciclo controlla che il carattere che precede in S il primo carattere del
suffisso più piccolo che inizia per ch sia ch. Supponiamo che il suffisso più
piccolo che inizia per ch sia chch'a , allora il suffisso chchch'a è il più piccolo
fra i suffissi che iniziano per chch. Iterando questo procedimento sulla
seconda rotazione che inizia per ch possiamo ricavare la seconda rotazione
che inizia per chch e così via. Il ciclo while pone nella posizione V[j] l'indice
della rotazione più piccola che inizia per chch, quindi in V[j+1] la seconda più
piccola, e così via. Alla fine di questo ciclo tutti i suffissi che iniziano per
chch lessicograficamente minori di una sequenza infinita di caratteri ch sono
ordinati.
• Ordiniamo ora, se esistono, tutti i suffissi che iniziano per chch che sono
lessicograficamente maggiori di una sequenza infinita di caratteri ch. Questo
viene fatto, specularmente al caso precedente, sfruttando i suffissi che iniziano
per chch' tali che ch' > ch. Poiché tali suffissi sono già stati ordinati, possiamo
effettuare quest'ordinamento invertendo semplicemente la direzione del ciclo
descritto nel passo precedente. Sia quindi V[i] il più grande suffisso in ordine
lessicografico che inizia per ch e sia j il valore più grande tale che V[j] inizia
per chch. In questo caso il ciclo da eseguire è il seguente:
33
while (i ≠ j) do
if (V[i] > 0 and S[V[i] - 1] = ch) then
V[j] Å V[i] – 1;
j Å j – 1;
i Å i – 1;
end;
Al termine di questo ciclo anche i suffissi che iniziano per chch sono ordinati.
• Per ogni indice V[i] corrispondente ad una rotazione che inizia per ch,
poniamo W[V[i]] a un valore con ch nei suoi bit di ordine alto, ed i nei suoi
bit di ordine basso. Il nuovo valore di W[V[i]] ha la proprietà di essere
distinto da tutti gli altri valori di W: questo velocizzerà i confronti tra stringhe
quando esso verrà utilizzato per confrontare sottosequenze uguali.
L’algoritmo Sort manda infine in output il vettore V, di lunghezza N + 1 a
causa dell’aggiunta del carattere terminatore in S’, da usare per costruire la
stringa L. Nella figura che segue mostriamo un esempio di applicazione
dell’algoritmo, per la stringa S’ = ‘caabbbbaabe§’.
Figura 7: esempio di applicazione dell’algoritmo Sort.
34
6.2 Decompressione efficiente
Come descritto in [1], l’algoritmo D può essere efficientemente implementato
con due sole scansioni della stringa L e una scansione dell’alfabeto X. Questa
tecnica consiste di due step. Nel primo step costruiamo due vettori: C[X] e
P[0, … , N-1]. C[ch] contiene il numero totale di occorrenze di caratteri in L
che precedono il carattere ch nell’alfabeto, mentre P[i] è il numero di
occorrenze del carattere L[i] nel prefisso L[0, … , i-1] di L. Dati ora i vettori
L, C e P, il vettore T definito nel paragrafo 2.2 è dato nel seguente modo: T[i]
= P[i] + C[L[i]], per ogni i = 0, … , N-1. Vediamo tutto ciò nel solito esempio:
L = ‘rdarcaaaabb’ Æ C = [0a 5b 7c 8d 9r], P = [0 0 0 1 0 1 2 3 4 0 1]
T[0] = P[0] + C[r] = 0 + 9 = 9
T[1] = P[1] + C[d] = 0 + 8 = 8
T[2] = P[2] + C[a] = 0 + 0 = 0
T[3] = P[3] + C[r] = 1 + 9 = 10
T[4] = P[4] + C[c] = 0 + 7 = 7
T[5] = P[5] + C[a] = 1 + 0 = 1
T[6] = P[6] + C[a] = 2 + 0 = 2
T[7] = P[7] + C[a] = 3 + 0 = 3
T[8] = P[8] + C[a] = 4 + 0 = 4
T[9] = P[9] + C[b] = 0 + 5 = 5
T[10] = P[10] + C[b] = 1 + 5 = 6
Ö T = [9 8 0 10 7 1 2 3 4 5 6].
Nel secondo step usiamo l’indice I per completare l’algoritmo D e generare
dunque S, come descritto nel paragrafo 2.2.
6.3 Implementazione in pseudocodice
Diamo ora l’implementazione in pseudocodice degli algoritmi presentati nel
capitolo 2 e di alcuni algoritmi del capitolo 3 (esclusi quelli di ovvia
implementazione come il RLE).
6.3.1 Algoritmo C
Input:
Output:
una stringa S di N caratteri S[0], … , S[N-1] appartenenti ad un
alfabeto X
una stringa L di N + 1 caratteri L[0], … , L[N] appartenenti a X’
= X ∪ {EOF} e un indice I, 0 ≤ I ≤ N.
35
BWT_C(S)
N Å length(S);
V Å Sort(S);
for i Å 0 to N do
// ricordiamo che V ha lunghezza N + 1
if (V[i] = 0) then
I Å i;
L[i] Å S[(V[i] – 1) mod N+1];
return L, I;
6.3.2
Algoritmo D
Input:
una stringa L di N caratteri L[0], … , L[N-1] appartenenti
all’alfabeto X’ = X ∪ {EOF} e un indice I, 0 ≤ I ≤ N-1
una stringa S di N - 1 caratteri S[0], … , S[N-2] appartenenti a X.
Output:
BWT_D(L, I)
N Å length(L);
for each ch ∈ X’ do
C[ch] Å 0;
for i Å 0 to N – 1 do
P[i] Å C[L[i]];
C[L[i]]++;
sum Å 0;
for ch Å FIRST(X’) to LAST(X’) do // scansione in ordine lessicografico
sum Å sum + C[ch];
C[ch] Å sum – C[ch];
i Å I;
for j Å N-1 downto 0 do
S’[j] Å L[i];
i Å P[i] + C[L[i]];
// il carattere terminatore viene tolto
S Å S’.substring(0, N-2);
return S;
6.3.3
Inizializzazione della lista Y
Input:
Output:
un alfabeto X
un puntatore ad una linked list Y.
Initialize(X)
head Å null;
for ch Å LAST(X) downto FIRST(X) do
node Å newnode();
symbol[node] Å ch;
link[node] Å head; // i nodi saranno linkati in ordine lessicografico
head Å node;
// head punta il primo nodo della lista
return head;
36
6.3.4
Algoritmo M
Input:
una stringa L di N caratteri L[0], … , L[N-1] appartenenti ad un
alfabeto X
un vettore R di N interi positivi o nulli R[0], … , R[N-1].
Output:
BWT_M(L, X)
head Å Initialize(X);
N Å length(L);
for i Å 0 to N – 1 do
temp Å head;
pos Å 0;
if symbol[temp] ≠ L[i]
// scorro la lista finché non trovo L[i]
then repeat
pos++;
prec Å temp;
temp Å link[temp];
until symbol[temp] = L[i];
link[prec] Å link[temp];
// collego il precedente al successivo
link[temp] Å head;
// collego il nodo con L[i] alla testa
head Å temp;
// il nodo con L[i] diventa la nuova testa
R[i] Å pos;
return R;
6.3.5
Algoritmo W
Input:
un vettore R di N interi positivi o nulli R[0], … , R[N-1] e un
alfabeto X
una stringa L di N caratteri L[0], … , L[N-1] appartenenti a X.
Output:
BWT_W(R, X)
head Å Initialize(X);
N Å length(R);
for i Å 0 to N – 1 do
temp Å head;
pos Å R[i];
for j Å 0 to pos – 1 do
// scorro la lista di R[i] posizioni
prec Å temp;
temp Å link[temp];
link[prec] Å link[temp];
link[temp] Å head;
head Å temp;
L[i] Å symbol[temp];
return L;
37
6.3.6
Costruzione dell’albero di Huffman
Input:
un vettore R di N interi positivi o nulli R[0], … , R[N-1] e un
alfabeto X
un puntatore alla radice dell’albero di Huffman di R.
Output:
HUFFTREE(R, X)
n Å size(X);
for i Å 0 to n – 1 do
F[i] Å 0;
// inizializzazione della lista delle frequenze
m Å length(R);
for i Å 0 to m – 1 do
F[R[i]]++;
// conteggio della frequenza dei caratteri
// Q è una min-priority queue contenente i
Q Å X;
for i Å 1 to n – 1 do
caratteri di X con chiave la loro frequenza in F
z Å newbinarytreenode();
left[z] Å x Å EXTRACT_MIN(Q);
right[z] Å y Å EXTRACT_MIN(Q);
F[z] Å F[x] + F[y];
INSERT(Q, z);
return EXTRACT_MIN(Q);
38
39
7 Conclusioni
Abbiamo visto una tecnica di compressione lossless che applica una
trasformazione reversibile a un blocco di testo per facilitarne la compressione
effettiva. L’algoritmo raggiunge al caso peggiore un rapporto di compressione
lineare rispetto all’entropia, a meno di un fattore costante. Esso funziona bene
sia su input testuali che non, e la compressione che può raggiungere cresce
all’aumentare delle dimensioni dei blocchi in input; i ottiene una
compressione confrontabile con ottimi modelli statistici, ed è molto vicino in
velocità ai codificatori basati sull'algoritmo di Lempel e Ziv.
I programmatori potrebbero stupirsi del fatto che la BWT non è coperta da
alcun brevetto software. È stato chiesto a Michael Burrows e David Wheeler
se una pratica per qualche brevetto fosse stata iniziata, ed entrambi hanno
risposto di no. Poiché quest’algoritmo è stato pubblicato nel 1994, ciò
significa che la tecnica è di dominio pubblico. In ogni caso i programmatori
devono essere cauti nello scegliere il codificatore per il passo finale della
compressione: i codici aritmetici offrono un'eccellente compressione ma sono
ricoperti da qualche brevetto, mentre i codici di Huffman, pur essendo
leggermente meno efficienti, sembrano meno atti a provocare battaglie legali.
In seguito all’introduzione della BWT, gli sforzi dei ricercatori si sono diretti
non tanto sulla trasformata in sé quanto sul miglioramento della velocità di
compressione (e dunque sul problema dell’ordinamento delle rotazioni
dell’input) e sulla rifinitura dei due blocchi basilari per la compressione, ossia
RLE e codifica di ordine zero. Sono stati studiati anche algoritmi sostitutivi al
Move-To-Front, come Inversion Frequencies (Arnavut-Magliveras, 1997),
Distance Coding (Binder, 2000) e Weighted Frequency Count (Deorowicz,
2001).
La BWT rappresenta oggi il cuore dell’algoritmo BZIP2, diventato lo standard
per la compressione lossless. Essa, inoltre, è una seria candidata nel campo
della bioinformatica, poiché può essere usata per comprimere sequenze
proteiche e per la ricerca e l’indicizzazione nelle sequenze di DNA e di
proteine.
40
41
Bibliografia
[1]
M. Burrows and D. Wheeler. A Block-sorting Lossless Data
Compression Algorithm, Technical Report 124, Digital Equipment
Corporation, Systems Research Center, 1994.
[2]
G. Manzini. An Analisys of the Burrows-Wheeler Transform,
Journal of the ACM, Vol.48, No.3, 3 May 2001, pp. 407-430.
[3]
J. Bentley, D. Sleator, R. Tarjan, and V. Wei. A Locally Adaptive
Data Compression Scheme, Communications of the ACM, Vol. 29,
No.4, April 1986, pp. 320-330.
[4]
T. Bell, I. Witten, and J. Cleary. Modeling for Text Compression,
ACM Computing Surveys, Vol.21, No.4, December 1989, pp. 557589.
[5]
M. Nelson. Data Compression with the Burrows-Wheeler
Transform, Dr. Jobb’s Journal, September 1996.
[6]
T. Cormen, C. Leiserson, R. Rivest, and C. Stein. Introduction to
Algorithms – 2nd Edition, McGraw Hill/MIT Press, Cambridge
Mass, USA, 2001.
[7]
M. Goodrich, and R. Tamassia. Data Structures and Algorithms in
Java – 3rd Edition, John Wiley and Sons, 2004.
Siti web
[8]
http://www.data-compression.info
[9]
ftp://ftp.cpsc.ucalgary.ca/pub/projects/text.compression.corpus
[10]
http://michael.dipperstein.com