Codifica di Huffman - codici prefissi, algoritmo e ottimalità
In questa pagina 5
Applicazione notevole del paradigma greedy (Paradigma greedy - scelta golosa, sottostruttura e selezione delle attivitàIl paradigma greedy compie subito una scelta localmente migliore (scelta greedy), pulisce l'istanza (clean-up) e risolve l'unica sottoistanza residua, con ricorsione di coda eliminabile; il codice è facile, la correttezza no e richiede due proprietà, la scelta greedy (esiste un ottimo che la contiene) e la sottostruttura ottima (cut-and-paste), da cui la correttezza segue per induzione. Caso di studio: la selezione delle attività, con l'algoritmo del primo che finisce, correttezza completa, complessità Theta(n) dopo l'ordinamento e controesempi per le scelte greedy sbagliate.Paradigma greedy - scelta golosa, sottostruttura e selezione delle attività →). Il punto di vista della teoria dell'informazione (entropia, limiti di Shannon, efficienza) è in Codici di Shannon-Fano e di HuffmanIn un codice ottimo le parole più probabili non sono più lunghe di quelle meno probabili e le due parole più lunghe differiscono solo per l'ultimo simbolo. Shannon-Fano costruisce l'albero dall'alto dividendo ripetutamente i simboli in due gruppi di probabilità quasi uguali; Huffman lo costruisce dal basso unendo ogni volta i due simboli meno probabili ed è sempre ottimo tra i codici a prefisso. La lunghezza media $L_y$ è la somma delle probabilità dei nodi uniti, l'efficienza è $\eta=\frac{H}{L_y}$.Codici di Shannon-Fano e di Huffman →, Codici a prefisso - Kraft-McMillan e limiti di Shannon sulla lunghezza mediaLa codifica di sorgente senza perdite associa a ogni parola emessa dalla sorgente una parola di codice binaria (o $M_y$-aria) di lunghezza variabile, in modo invertibile; la qualità si misura con la lunghezza media $L_y=\sum p_{\underline x}(\underline a),l(\underline a)$. In un codice a prefisso nessuna parola è l'inizio di un'altra, quindi la decodifica è istantanea. Kraft-McMillan: ogni codice decodificabile ha $\sum M_y^{-l_i}\le1$, e se interi $l_i$ la soddisfano esiste un codice a prefisso con quelle lunghezze. Shannon: $\frac{H(\underline x)}{\log_2M_y}\le L_y<\frac{H(\underline x)}{\log_2M_y}+1$ per il codice a prefisso ottimo, con $L_y=\frac{H}{\log_2M_y}$ solo se le probabilità sono potenze di $\frac1{M_y}$.Codici a prefisso - Kraft-McMillan e limiti di Shannon sulla lunghezza media →, Codici di Shannon, Huffman ed Elias e codifica aritmeticaIl codice di Shannon(-Fano) assegna alla parola $i$ lunghezza $l_i=\lceil-\log_{M_y}p_i\rceil$, soddisfa Kraft-McMillan e ha $H\le L_y<H+1$; è ottimo solo se le probabilità sono potenze di $\frac1{M_y}$. Il codice ottimo (lunghezza media minima) dà parole più lunghe ai simboli meno probabili e ha almeno due parole di lunghezza massima che differiscono solo nell'ultimo simbolo: lo costruisce la procedura di Huffman, che unisce a ogni passo i due nodi meno probabili. Il codice di Elias prende le prime $l_i=\lceil-\log_2p_i\rceil+1$ cifre del punto medio dell'intervallo di ampiezza $p_i$ in $[0,1)$: è a prefisso ma subottimo ($H+1\le L_y<H+2$), e si adatta alla codifica aritmetica adattativa.Codici di Shannon, Huffman ed Elias e codifica aritmetica → e Codifica lossless - entropia, Huffman e codifiche a dizionarioLa codifica lossless rappresenta i simboli di una sorgente con parole di codice a lunghezza variabile in modo invertibile; si usano codici a prefisso (istantanei). L'entropia $H(X)=\sum p_i\log_2\frac1{p_i}$ è il limite: $H(X)\le\mathcal L^*<H(X)+1$ (Shannon), con uguaglianza se le probabilità sono potenze di 1/2. Il codice di Huffman è ottimo ma lascia fino a 1 bit di overhead per simbolo; raggruppando $K$ simboli (codifica a blocchi) si tende al tasso entropico $\mathcal H(X)\le H(X)$, ma la complessità cresce come $M^K$; la codifica aritmetica ($\mathcal L<H+2$ per messaggio) ha complessità lineare. Altre tecniche: dizionario (LZ, DEFLATE di ZIP e PNG, ANS in Zstandard), Exp-Golomb e categoria/ampiezza (usati in JPEG e nei codec video) per interi con probabilità decrescente col modulo, codifica predittiva (si codifica l'errore di predizione, che ha entropia molto più bassa).Codifica lossless - entropia, Huffman e codifiche a dizionario →. Qui contano la dimostrazione di ottimalità (scelta greedy e sottostruttura) e la complessità con la coda con priorità.
Codifica per carattere e codici a lunghezza fissa
Un file è una stringa su un alfabeto di caratteri. Una codifica per carattere associa a ogni una parola binaria (la codeword); la codifica del file è la concatenazione delle parole dei suoi caratteri, Si chiede che la codifica sia invertibile (altrimenti si distrugge l'informazione) e calcolabile in modo efficiente, con la compressione migliore possibile; la qualità si misura con la lunghezza in bit della stringa compressa rispetto alla codifica diretta (8 bit per carattere ASCII).
Caso di studio. Un file di milioni di caratteri ASCII (800 Mbit in codifica diretta) usa solo sei caratteri, con frequenze , , , , , . Si leggono con una scansione lineare del file e sommano sempre a . Il numero di occorrenze di è .
Con sei caratteri bastano 3 bit (): codice a lunghezza fissa , , , , , . Il file occupa Mbit (risparmio del su 800), più la tabella dei caratteri, e si comprime e decomprime in tempo lineare. Ma questo codice ignora le frequenze: l'idea è dare parole più corte ai caratteri più frequenti.
Attenzione: la lunghezza variabile, da sola, può rompere l'invertibilità. Il codice , , , , , costerebbe soltanto
ma non è iniettivo sulle stringhe: daadaa e ee danno entrambe 100100. Il risparmio sarebbe illusorio, perché non si può decomprimere.
Codici prefissi e trie
Definizione (codice prefisso). Una codifica è libera da prefissi (prefix-code) se nessuna parola è prefisso di un'altra.
Nel codice sbagliato di prima è prefisso di e lo è di .
Teorema. Un codice prefisso è invertibile sulle stringhe, e si decodifica con una sola scansione leggendo i bit fino a incontrare una parola.
Dimostrazione. Si legge dall'inizio il minimo numero di bit che forma una parola: questa è l'unica parola con tale inizio, perché una seconda parola, essendo anch'essa un inizio della stessa sequenza, sarebbe prefisso della prima o viceversa. Il primo carattere è dunque determinato; si ripete dal bit successivo.
Anche il codice a lunghezza fissa è un codice prefisso. La condizione è sufficiente, non necessaria, per la decodifica unica; ci si può però restringere ai codici prefissi senza perdere nulla in lunghezza media, per il teorema di Kraft-McMillan (Codici a prefisso - Kraft-McMillan e limiti di Shannon sulla lunghezza mediaLa codifica di sorgente senza perdite associa a ogni parola emessa dalla sorgente una parola di codice binaria (o $M_y$-aria) di lunghezza variabile, in modo invertibile; la qualità si misura con la lunghezza media $L_y=\sum p_{\underline x}(\underline a),l(\underline a)$. In un codice a prefisso nessuna parola è l'inizio di un'altra, quindi la decodifica è istantanea. Kraft-McMillan: ogni codice decodificabile ha $\sum M_y^{-l_i}\le1$, e se interi $l_i$ la soddisfano esiste un codice a prefisso con quelle lunghezze. Shannon: $\frac{H(\underline x)}{\log_2M_y}\le L_y<\frac{H(\underline x)}{\log_2M_y}+1$ per il codice a prefisso ottimo, con $L_y=\frac{H}{\log_2M_y}$ solo se le probabilità sono potenze di $\frac1{M_y}$.Codici a prefisso - Kraft-McMillan e limiti di Shannon sulla lunghezza media →).
Rappresentazione ad albero
Un codice prefisso si rappresenta con un trie: albero binario con archi etichettati verso il figlio sinistro e verso il destro, in cui a ogni nodo corrisponde la stringa dei bit lungo il cammino dalla radice. Vale che:
- le parole sono sulle foglie. Se un carattere fosse su un nodo interno, la sua parola sarebbe prefisso della parola di un discendente. Quindi il trie ha foglie;
- c'è una corrispondenza biunivoca tra codici prefissi su e trie binari con foglie, per cui si parla indifferentemente del codice o del trie;
- la profondità di nel trie è la lunghezza della sua parola: .
La decodifica con il trie parte dalla radice, segue 0 a sinistra e 1 a destra per ogni bit letto fino a una foglia, emette il carattere e ricomincia dalla radice.
Esempio. Con il codice prefisso , , , , , la stringa badface diventa , cioè 1010111110001001101 (19 bit, contro del codice fisso), e il trie la riporta a badface (verificato con Python).
Costo di un trie
Il numero di bit del file compresso è
Definizione (costo di un trie). (lunghezza media in bit per carattere).
Problema (codifica ottima di carattere). Dati e le frequenze , trovare il codice prefisso, cioè il trie binario con foglie, che minimizza .
Non serve conoscere il file, solo l'alfabeto e le frequenze (come per la catena di matrici bastavano le dimensioni, Matrix Chain Multiplication - parentesizzazione ottimadato il vettore p delle dimensioni di una catena di n matrici compatibili (A_i è p_(i-1) x p_i), si cerca la parentesizzazione che minimizza il numero di prodotti tra scalari. Le parentesizzazioni sono numeri di Catalan (esponenziali). Sottostruttura: i due sottoalberi dell'ultimo prodotto sono ottimi (cut-and-paste). Ricorrenza c(i,j) = min su k di c(i,k) + c(k+1,j) + p_(i-1) p_k p_j, c(i,i) = 0. D&C: 3^(n-1) - 1 operazioni. Bottom-up per diagonali crescenti con tabelle m e s: (n^3 - n)/3 prodotti, Theta(n^3). La memoizzazione ha lo stesso costo e non risparmia mai.Matrix Chain Multiplication - parentesizzazione ottima →).
Il trie ottimo è pieno. Un albero binario è pieno se ogni nodo interno ha esattamente due figli. Dimostrazione per assurdo: sia ottimo con un nodo interno con un solo figlio (diciamo il sinistro) e sia il sottoalbero di radice (può essere la sola foglia ). Si contraggono e in un solo nodo: il risultato è ancora un trie con le stesse foglie, quindi un codice valido, e l'arco , cioè un bit, scompare dal cammino di ogni foglia di , che ha almeno una foglia . Allora (le frequenze sono positive), mentre per gli altri caratteri . Dunque , assurdo.
Per esempio il codice a lunghezza fissa dei 6 caratteri ha un trie non pieno (le parole e sono inutilizzate): non è ottimo. Un albero binario pieno con foglie ha sempre nodi interni (Divide and conquer - proprietà di sottostruttura e albero delle chiamateUn algoritmo divide and conquer nasce da una proprietà di sottostruttura (funzioni Divide e Conquer, base n0) con sottoistanze di taglia strettamente minore; l'albero delle chiamate ne descrive l'esecuzione, e il D&C non ha memoria (ripete le sottoistanze uguali). Esempio completo: MAX di un array, con correttezza per induzione, ricorrenza e conteggio esatto n-1 tramite il teorema sugli alberi binari pieni.Divide and conquer - proprietà di sottostruttura e albero delle chiamate →: contando gli archi, da cui ).
Costo come somma dei nodi interni. Se a ogni nodo si dà un peso = somma delle frequenze delle foglie sotto di lui, vale perché la frequenza di una foglia compare nel peso di ciascuno dei suoi antenati propri. È la formula con cui si controlla a mano un risultato.
L'algoritmo di Huffman
Idea. Si costruisce il trie dal basso. Si parte da una foresta di alberi, uno per carattere (la foglia con la sua frequenza). A ogni passo si fondono due alberi creando una nuova radice che li ha come figli sinistro e destro e come peso la somma dei pesi: la foresta perde un albero, quindi dopo fusioni resta un albero pieno con foglie, il trie.
Ogni fusione aumenta di 1 la profondità di tutte le foglie dei due alberi, quindi allunga di un bit le loro parole; l'impatto sul costo è il peso dell'albero. La scelta greedy è dunque fondere i due alberi di peso minimo.
L'implementazione usa una coda con priorità (Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →, HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →) di nodi con chiave , con insert ed extractmin in se è il numero massimo di elementi. Un nodo ha i campi left, right, f e char (significativo solo per le foglie, che hanno left e right nulli).
HUFFMAN(C, f)
n <- |C|
Q <- {}
for c in C do
z <- newnode(); z.f <- f(c); z.char <- c; z.left <- null; z.right <- null
Q.insert(z)
for i <- 1 to n-1 do
x <- Q.extractmin()
y <- Q.extractmin()
z <- newnode()
z.f <- x.f + y.f
z.left <- x; z.right <- y // convenzione: il sinistro e' il piu' leggero
Q.insert(z)
return Q.extractmin() // la radice del trieLa codifica del file si fa dopo, leggendo le parole dai cammini radice-foglia. Convenzione: il figlio sinistro (arco ) è quello estratto per primo, cioè di frequenza non superiore; con frequenze uguali il trie può cambiare ma il costo no.
Esempio eseguito a mano
Frequenze in percentuale: , , , , , . La coda a ogni passo, con il nodo creato:
| passo | estratti (sx, dx) | nodo creato | coda dopo il passo |
|---|---|---|---|
| inizio | |||
| 1 | |||
| 2 | |||
| 3 | |||
| 4 | |||
| 5 |
Il trie (i numeri sono i pesi, gli archi sono a sinistra e a destra):
(100)
0/ \1
a:45 (55)
0/ \1
(25) (30)
0/ \1 0/ \1
c:12 b:13 (14) d:16
0/ \1
f:5 e:9Parole: , , , , , . Costo cioè Mbit per il file, contro i del codice fisso. Controllo con i nodi interni: (in centesimi). L'entropia della sorgente è bit, e vale (Codici a prefisso - Kraft-McMillan e limiti di Shannon sulla lunghezza mediaLa codifica di sorgente senza perdite associa a ogni parola emessa dalla sorgente una parola di codice binaria (o $M_y$-aria) di lunghezza variabile, in modo invertibile; la qualità si misura con la lunghezza media $L_y=\sum p_{\underline x}(\underline a),l(\underline a)$. In un codice a prefisso nessuna parola è l'inizio di un'altra, quindi la decodifica è istantanea. Kraft-McMillan: ogni codice decodificabile ha $\sum M_y^{-l_i}\le1$, e se interi $l_i$ la soddisfano esiste un codice a prefisso con quelle lunghezze. Shannon: $\frac{H(\underline x)}{\log_2M_y}\le L_y<\frac{H(\underline x)}{\log_2M_y}+1$ per il codice a prefisso ottimo, con $L_y=\frac{H}{\log_2M_y}$ solo se le probabilità sono potenze di $\frac1{M_y}$.Codici a prefisso - Kraft-McMillan e limiti di Shannon sulla lunghezza media →).
L'ottimo non è unico. Con frequenze ci sono due esecuzioni valide (dopo la fusione si hanno tre alberi di peso 2): fondendo i due originali da 2 le parole hanno tutte lunghezza 2, fondendone uno con il nodo creato si ottengono lunghezze ; il costo è in entrambi i casi (forza bruta: è il minimo).
Complessità
Il modello conta i confronti tra frequenze; .
- La prima fase fa inserzioni, ciascuna : (con la costruzione bottom-up dello heap, Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →, scenderebbe a ).
- Il ciclo ha iterazioni e ognuna fa 2 estrazioni e 1 inserzione, cioè .
- Il
returnfa un'ultima estrazione.
In totale le estrazioni del minimo sono (per : ) e anche le inserzioni sono . La complessità è (Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica →). Se i caratteri arrivano già ordinati per frequenza si può scendere a con due code ordinate, una per le foglie e una per i nodi creati (che nascono in ordine non decrescente); ordinare i caratteri costa però .
import heapq, itertools
def huffman(freq): # freq: dict carattere -> frequenza
cnt = itertools.count() # spareggio per l'heap: i nodi non si confrontano mai
Q = [(f, next(cnt), (c,)) for c, f in freq.items()] # un nodo foglia e' (carattere,)
heapq.heapify(Q)
for _ in range(len(freq) - 1):
fx, _, x = heapq.heappop(Q)
fy, _, y = heapq.heappop(Q)
heapq.heappush(Q, (fx + fy, next(cnt), (x, y))) # un nodo interno e' (sinistro, destro)
return Q[0][2]
def codici(t, p=""):
if len(t) == 1: return {t[0]: p or "0"}
return {**codici(t[0], p + "0"), **codici(t[1], p + "1")}Correttezza
Lo spazio delle scelte è l'insieme degli alberi della foresta, ciascuno con la frequenza somma delle sue foglie; ogni scelta è la coppia da fondere, e dopo la fusione i due alberi sono sostituiti da uno di frequenza uguale alla somma. Ogni albero della foresta si può vedere come un supercarattere con quella frequenza, di cui calcolare la codifica ottima: la parola del supercarattere sarà il prefisso comune di tutte le foglie del sottoalbero. La foresta dopo una fusione è la sottoistanza residua, con un carattere in meno.
Notazione: sono i due caratteri di frequenza minima, per ogni altro ; con è l'alfabeto residuo.
Lemma di scambio. Siano due foglie di un trie con e e sia il trie ottenuto scambiando i caratteri e . Allora .
Dimostrazione. Dopo lo scambio sta alla profondità di e viceversa, e gli altri caratteri non si muovono: perché i due fattori sono entrambi .
In parole: il carattere più raro può solo guadagnare scendendo più in basso, e quello più frequente salendo.
Proprietà di scelta greedy. Esiste un trie ottimo in cui e sono foglie sorelle (figlie dello stesso padre) alla profondità massima.
Dimostrazione. Sia un trie ottimo; è pieno. Sia la profondità massima e sia una foglia a profondità : il suo fratello è una foglia (se avesse discendenti ci sarebbe una foglia più profonda), a profondità .
- Se si scambiano e : valgono ( ha frequenza minima) e , quindi per il lemma il costo non aumenta. (Se si chiama la foglia .) Ora è a profondità con fratello (foglia).
- Se si scambiano e : valgono (perché e è il più raro tra i caratteri diversi da ) e , quindi il costo non aumenta. Lo scambio non sposta , che ha ora per fratello .
Il trie ottenuto ha sorelle alla profondità massima e costo , quindi, per l'ottimalità di , uguale: è ottimo.
(Il testo del corso scambia direttamente le due coppie e e calcola la differenza con la formula completa; il passaggio in due tempi evita di trattare a parte i casi in cui , , , non sono distinti.)
Proprietà di sottostruttura ottima. Sia un trie per in cui e sono foglie sorelle e sia il trie per ottenuto potando le foglie e : il loro padre diventa la foglia . Gli altri caratteri hanno la stessa profondità in e in , e . Quindi Questa relazione tra costo e costo della residua vale per ogni coppia , ed è biunivoca: ogni trie per si ottiene da un unico trie per facendo di il padre di due foglie . Perciò il minimo di sui trie per con sorelle è , e per la scelta greedy è anche : (Per assurdo, con cut-and-paste: se in un ottimo con sorelle il trie potato non fosse ottimo per , sostituendolo con un ottimo e rimettendo le foglie sotto si avrebbe un trie per di costo .)
Teorema. ritorna un trie ottimo, per ogni alfabeto con caratteri.
Dimostrazione. Induzione su . Base : un solo nodo, costo (o con parola di un bit per convenzione), l'unico trie. Passo : la prima iterazione fonde in e quello che resta è esattamente l'esecuzione di , che per ipotesi induttiva (alfabeto di caratteri) ritorna un trie ottimo per . L'algoritmo ritorna il trie ottenuto da rendendo padre di . Per la relazione dei costi
Ancora una volta il codice è semplice e la dimostrazione di correttezza è la parte difficile.
Domande d'esame
- Il numero complessivo di estrazioni del minimo effettuate da HUFFMAN su un alfabeto di caratteri. Risposta: iterazioni con estrazioni, più nel
return: . Vedi Esercizio - domande di prima parte su greedy e Huffman (simulazioni d'esame). - Perché il trie di un codice ottimo è pieno? Quanti nodi interni ha? Traccia: se un nodo interno ha un solo figlio si contrae l'arco e si accorcia di un bit almeno una parola, quindi il costo diminuisce; un albero pieno con foglie ha nodi interni.
- Enunciare e dimostrare la proprietà di scelta greedy per Huffman. Traccia: lemma di scambio ; si portano e sulle due foglie sorelle più profonde.
- Frequenze : forma del trie e parole. Vedi Esercizio - codice di Huffman con frequenze in progressione geometrica, trie e codeword (compito del 29 gennaio 2026, es. 2).
Versione ripasso
- Codifica per carattere , file concatenazione delle parole. Lunghezza fissa: bit (6 caratteri: 3 bit, Mbit su ). La lunghezza variabile deve restare iniettiva: , , , , , non lo è (
daadaaeeedanno100100), il suo Mbit è illusorio. - Codice prefisso: nessuna parola prefisso di un'altra, quindi decodifica unica con una scansione (si legge fino a una parola, che è l'unica con quell'inizio). È un trie binario (0 a sinistra, 1 a destra) con le parole solo sulle foglie (un nodo interno sarebbe prefisso dei discendenti); corrispondenza biunivoca con i trie a foglie; . Teoria dell'informazione: Codici a prefisso - Kraft-McMillan e limiti di Shannon sulla lunghezza mediaLa codifica di sorgente senza perdite associa a ogni parola emessa dalla sorgente una parola di codice binaria (o $M_y$-aria) di lunghezza variabile, in modo invertibile; la qualità si misura con la lunghezza media $L_y=\sum p_{\underline x}(\underline a),l(\underline a)$. In un codice a prefisso nessuna parola è l'inizio di un'altra, quindi la decodifica è istantanea. Kraft-McMillan: ogni codice decodificabile ha $\sum M_y^{-l_i}\le1$, e se interi $l_i$ la soddisfano esiste un codice a prefisso con quelle lunghezze. Shannon: $\frac{H(\underline x)}{\log_2M_y}\le L_y<\frac{H(\underline x)}{\log_2M_y}+1$ per il codice a prefisso ottimo, con $L_y=\frac{H}{\log_2M_y}$ solo se le probabilità sono potenze di $\frac1{M_y}$.Codici a prefisso - Kraft-McMillan e limiti di Shannon sulla lunghezza media →, Codici di Shannon-Fano e di HuffmanIn un codice ottimo le parole più probabili non sono più lunghe di quelle meno probabili e le due parole più lunghe differiscono solo per l'ultimo simbolo. Shannon-Fano costruisce l'albero dall'alto dividendo ripetutamente i simboli in due gruppi di probabilità quasi uguali; Huffman lo costruisce dal basso unendo ogni volta i due simboli meno probabili ed è sempre ottimo tra i codici a prefisso. La lunghezza media $L_y$ è la somma delle probabilità dei nodi uniti, l'efficienza è $\eta=\frac{H}{L_y}$.Codici di Shannon-Fano e di Huffman →.
- Costo: , bit del file . Il trie ottimo è pieno (se ha un solo figlio si contrae con il figlio e almeno una foglia perde un bit, quindi scende) e ha nodi interni (Divide and conquer - proprietà di sottostruttura e albero delle chiamateUn algoritmo divide and conquer nasce da una proprietà di sottostruttura (funzioni Divide e Conquer, base n0) con sottoistanze di taglia strettamente minore; l'albero delle chiamate ne descrive l'esecuzione, e il D&C non ha memoria (ripete le sottoistanze uguali). Esempio completo: MAX di un array, con correttezza per induzione, ricorrenza e conteggio esatto n-1 tramite il teorema sugli alberi binari pieni.Divide and conquer - proprietà di sottostruttura e albero delle chiamate →). Vale , con somma delle frequenze delle foglie sotto .
- Problema: dati e , trovare il codice prefisso, cioè il trie binario con foglie, che minimizza . Servono solo l'alfabeto e le frequenze, non il file.
- HUFFMAN: foresta di foglie in una coda con priorità (Code con prioritàEntry chiave-valore; ADT coda con priorità (insert, min, removeMin) con chiave minima = priorità massima; esempio di esecuzione; applicazioni; implementazioni con lista non ordinata e ordinata e relativi costi; ordinamento tramite coda con priorità.Code con priorità →, HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →); per :
x <- extractmin,y <- extractmin, nodozconz.f <- x.f + y.f,z.left <- x,z.right <- y,insert(z); ritorna l'ultimoextractmin. Il sinistro è quello di frequenza . - Esempio , , , , , : fusioni , , , , . Parole , , , , , ; ( Mbit contro ), uguale alla somma dei nodi interni . Il trie ha come figlio sinistro della radice e a destra il nodo , che ha a sinistra (con e ) e a destra (con , cioè ed , e ). Decodifica con il trie dalla radice, bit per bit:
badfacedà1010111110001001101. - Complessità:
insertiniziali, poi iterazioni con 2extractmine 1insert: in totale estrazioni (per : ) e inserzioni, ciascuna , quindi (Notazione asintoticaDefinizioni di O, Omega, Theta e o piccolo con le costanti c ed n0; esempi con costanti esplicite; proprietà (polinomi, esponenziali, logaritmi, somme, implicazioni tra notazioni); sommatorie notevoli; terminologia (logaritmica, lineare, polinomiale, esponenziale).Notazione asintotica →). Con heapify (Costruzione di uno heapCostruire uno heap da un array di n entry: approccio top-down con n-1 insert, Theta(n log n); approccio bottom-up con down-heap dalle foglie verso la radice, Theta(n), con dimostrazione della somma; esempi svolti, in loco; unione di due alberi con heap-order.Costruzione di uno heap →) la fase iniziale è ; con dati ordinati e due code ordinate l'intero algoritmo è . - Non unicità: con frequenze ci sono due trie con lunghezze e , entrambi di costo .
- Lemma di scambio: se e , scambiando e si ha (il più raro scende, il più frequente sale).
- Scelta greedy: i due caratteri più rari sono foglie sorelle di profondità massima in qualche trie ottimo. Prova in due scambi: se non è già tra le due foglie sorelle più profonde si scambia (, ); poi si scambia (, ). Il costo non cresce, quindi resta ottimo.
- Sottostruttura: , . Per ogni trie con sorelle, potato in : , perché ; corrispondenza biunivoca tra tali e i trie per , dunque .
- Correttezza: induzione su ; base ; passo: la prima fusione è la scelta greedy, il resto è , ottimo per ipotesi induttiva, e .
- Errori tipici: credere che meno bit significhi meglio senza controllare il prefisso; mettere un carattere su un nodo interno; scrivere fusioni o estrazioni di troppo, o dimenticare quella del
return; mettere a sinistra il nodo più pesante; pensare che l'ottimo sia unico; in dimenticare l'addendo .