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