Programmazione Greedy I codici di Huffman Codifica dell informazione La rappresentazione ordinaria dell informazione prevede l impiego di un numero costante di bit; per esempio ad ogni carattere del codice ASCII è associata univocamente una stringa binaria di 8 bit. Relazioni di questo tipo prendono il nome di codici a lunghezza fissa. Supponiamo di avere un alfabeto A={a, b, c, d,e}, per poterne fornire una rappresentazione tramite codice a lunghezza fissa dobbiamo impiegare almeno 3 bit: a d b e c Prendiamo in esame una semplice stringa formata dai simboli dell alfabeto A, ad esempio aaabcde. La codifica complessiva della stringa richiede 2 bit (3 bit per ogni carattere). Ammettendo di considerare altre informazioni si può operare secondo una strategia diversa. Se si conoscono a priori le frequenze con cui i caratteri occorrono all interno di un file, allora conviene adottare un codice a lunghezza variabile il cui scopo è quello di assegnare meno bit in corrispendenza dei caratteri più frequenti e aumentare progressivamente il dispendio della memoria con il diminuire della frequenza. Tornando all esempio precedente relativo all alfabeto A e alla stringa aaabcde, una possibile codifica progressiva sarebbe: a (carattere più frequente) b d c e Adesso la codifica della stringa comporterebbe un costo sensibilmente minore: aaabcde (costo complessivo di bit) La cosa sembrerebbe funzionare perfettamente (più del 5% di risparmio), ma bisogna considerare anche la possibilità di recuperare l informazione compressa. Con questa scelta la decodifica è diventata un operazione indecidibile, con risultati disastrosi sul significato originale dei dati. A tal proposito basti considerare i primi 4 bit della codifica proposta: aaab (esatto) cd (sbagliato) cab (sbagliato) acb (sbagliato)...... Per questo motivo un codice a lunghezza variabile che possa essere felicemente impiegato nella compressione dei dati deve essere un codice prefisso. In un codice prefisso nessuna stringa di codifica
può essere il prefisso di una qualunque altra stringa di codifica, e questo elimina ogni ambiguita in fase di decodifica. Codici prefissi e alberi binari Una rappresentazione comoda per un codice variabile prefisso è quella che fa uso degli alberi binari. Tutti i caratteri dell alfabeto a cui appartiene il codice sono disposti come foglie dell albero e, dopo aver etichettato ogni arco dell albero indicando con il percorso verso il sottoalbero sinistro e con il percorso verso il sottoalbero destro, la codifica di un simbolo risulta come la stringa che etichetta il cammino dalla radice alla foglia che contiene quel simbolo. Esaminiamo il codice a lunghezza variabile presentato in precedenza per l alfabeto A: A C D E B Il codice non è prefisso perchè i caratteri non etichettano solo foglie, ma anche nodi intermedi (come nel caso di a e b). Inoltre un codice prefisso ottimo induce una rappresentazione completa, nella quale ogni nodo interno ha esattamente 2 figli, quindi, se A è l alfabeto di caratteri a cui appartiene il codice, allora l albero di codifica avrà A foglie e A - nodi interni. A questo proposito esaminiamo l albero del codice a lunghezza fissa presentato in precedenza: L albero non è completo; infatti il codice a lunghezza fissa non e un codice ottimo A B C D E In generale si può notare che, se T è un albero per un codice su un alfabeto A, f(x) è la frequenza relativa ad ogni carattere x di A e liv(x) è la profondità di x all interno di T, allora possiamo definire il costo dell albero T come: C ( T ) = x A f ( x) liv( x)
Algoritmo di Huffman Gli studi di Huffman forniscono un metodo algoritmico in grado di generare un codice prefisso ottimo per un file in cui compaiano n caratteri. I caratteri fanno parte di un alfabeto A ( A = n) e per ogni a di A è definita la frequenza di a, f(a), come il numero di occorenze di a all interno del file. L algoritmo di Huffman riceve in input l alfabeto da codificare e il vettore con le frequenze di ciascun carattere e costruisce l albero di codifica ottimo: albero_di_huffman (A, f[]) { n = A ; heap = costruisci_heap(a); for (n- volte) { x = estrai_minimo(heap); y = estrai_minimo(heap); z = nuovo_nodo(); figlio_sinistro[z] = x; figlio_destro[z] = y; f[z] = f[x]+f[y]; inserisci(heap, z); } } La costruzione dell albero è iterativa, vengono selezionati ogni volta i due elementi con frequenza minore, x e y, per essere fusi in unico albero. Il procedimento di fusione rende x e y figli di un nuovo elemento z la cui caratteristica è di ereditare la frequenza somma delle frequenze dei due elementi fusi. Nell implementazione in pseudocodice presentata in questo contesto l algoritmo organizza i simboli dell alfabeto A in una coda con priorità, secondo valori crescenti della frequenza. La coda è realizzata tramite un heap minimo. La complessità dell algoritmo è così quantificabile con semplici osservazioni: () la costruzione di un heap ha un costo O(n); (2) le operazioni di inserimento ed estrazione che avvengono all interno del for contribuiscono con O(log(n)); (3) il for viene ripetuto n volte; in definitiva il costo totale è pari a O(n log(n)). Correttezza dell algoritmo Per snellire la dimostrazione facciamo due osservazioni preliminari di cui si fornirà la prova in un secondo tempo: Osservazione : Se A è un alfabeto e x, y sono i due simboli di A con frequenza minore, allora esiste un codice prefisso ottimo per A il cui albero T presenta x e y come fratelli. Osservazione 2: Se T è un albero ottimo per l alfabeto A e x, y sono due caratteri che appaiono in T come foglie di uno stesso nodo z, allora, se f(z)= f(x)+f(y), l albero T = T-{x,y} è ancora un albero ottimo per l alfabeto A = A-{x,y}+{z}.
La dimostrazione è condotta per induzione sul numero n di caratteri che compongono l alfabeto da codificare. Base: se n=2, indipendentemente dalle frequenze di ciascun carattere, l algoritmo di Huffman costruisce uno dei due unici possibili alberi prefissi ottimi. Induzione: in presenza di n+ caratteri l algoritmo individua due simboli x e y con minor frequenza. Per l oss. possiamo stabilire che x e y sono figli di uno stesso nodo z; a questo punto l algoritmo lavora su z, riducendo di un simbolo l alfabeto e portandosi al livello n, dove l albero di codifica T è ottimo per ipotesi induttiva. Dato che il procedimento etichetta f[z] con f[x]+f[y] allora per l oss.2 se T è ottimo allora è ottimo anche l albero di livello n+ da cui T proviene. Concludiamo con la dimostrazione delle due osservazioni. (dimostrazione oss.) Supponiamo di avere un alfabeto A e due caratteri x,y A che hanno la minor frequenza tra tutti i simboli di A. Forniamo una prova costruttiva a partire da un albero T ottimo per A, dove x e y non sono fratelli. Mostriamo come sia possibile costruire un albero T, ancora ottimo per A, ma dove x e y siano fratelli. Individuiamo all interno di T le due foglie sorelle a e b che si trovino al livello più basso. Adesso scambiamo a con x e b con y e mostriamo che l albero T ottenuto è ancora una albero ottimo. Il ragionamento sarà condotto sullo scambio di a con x, ma in maniera del tutto simmetrica può essere esteso anche al caso di b e y. Lo spostamento di a e x ha provocato una variazione nel costo dell albero T, in particolare: C(T )=C(T)-f(x)liv(x)+f(x)liv(a)-f(a)liv(a)+f(a)liv(x) C(T )=C(T)+f(x)[liv(a)-liv(x)]-f(a)[liv(a)-liv(x)] C(T )=C(T)+[liv(a)-liv(x)][f(x)-f(a)] Considerando che per come abbiamo scelto a e x risulta: liv(x) liv(a) e f(a) f(x) Allora possiamo concludere che la variazione di costo fra T e T è minore o uguale a zero, il che implica che T è ancora un albero ottimo. (dimostrazione oss.2) Siano dati due alberi T e T, il primo ottimo per un alfabeto A e il secondo definito come T =T-{x,y}, dove x e y sono le due foglie di un nodo z in T. Posto f(z)=f(x)+f(y) vogliamo dimostrare che T è ottimo per l alfabeto A-{x,y} {z}. Mettiamo in relazione il costo di T e quello di T : C(T)=C(T )-f(z)liv(z)+f(x)liv(x)+f(y)liv(y) () Se poniamo liv(z)=l e consideriamo che x e y si trovano alla stessa altezza e sono figlie di z, allora possiamo scrivere liv(x)=liv(y)=l+; inoltre se notiamo che f(z)=f(x)+f(y), la () diventa: C(T)=C(T )-l(f(x)+f(y))+(l+)(f(x)+f(y)) C(T)=C(T )+f(x)+f(y) (2)
Adesso supponiamo che esista un albero T per l alfabeto A-{x,y} {z} tale che C(T )<C(T )(*). L esistenza di T genera un assurdo, se infatti a T aggiungiamo x e y, otteniamo un albero T per A tale che: C(T )=C(T )+f(x)+f(y) (3) Considerando in sequenza la (3), la (*) e la (2): C(T ) = C(T )+f(x)+f(y) < C(T )+f(x)+f(y) = C(T) C(T )<C(T) (4) L espressione (4) è un assurdo perchè abbiamo considerato per ipotesi che T era un albero ottimo per A, l assurdo deriva dall aver supposto che T non fosse ottimo per A-{x,y} {z}. Fabio Venditti, 999