Salta al contenuto
Note per Studenti Esercizio - codice di Huffman con frequenze in progressione geometrica, trie e codeword (compito del 29 gennaio 2026, es. 2)

Esercizio - codice di Huffman con frequenze in progressione geometrica, trie e codeword (compito del 29 gennaio 2026, es. 2)

Esame
In questa pagina 4

Testo (compito d'esame di Algoritmi per l'Ingegneria, Ing. Informatica UniPD, 29 gennaio 2026, seconda parte, esercizio 2, 16 punti; nel testo originale i due punti sono numerati 1.(a) e 1.(b)). Si consideri un file FnF_n definito su un alfabeto Σn={c1,…,cn}\Sigma_n = \{c_1, \dots, c_n\} di nn caratteri e si supponga che, per 1≤j≤n1 \le j \le n, il carattere cjc_j abbia frequenza fj=2j−12n−1.f_j = \frac{2^{j-1}}{2^n - 1} .

(a) [6 punti] Si disegni il trie associato alla codifica di Huffman di FnF_n per n=4n = 4 e si elenchino le quattro parole di codice per i caratteri c1,c2,c3,c4∈Σ4c_1, c_2, c_3, c_4 \in \Sigma_4.

(b) [10 punti] Per il caso di nn generico, si determini la forma del trie e la codeword associata al generico carattere cj∈Σnc_j \in \Sigma_n, per 1≤j≤n1 \le j \le n. Le risposte vanno giustificate formalmente.

Attenzione: nello svolgimento ci si attenga alle convenzioni sul trie adottate nel testo e a lezione: il figlio sinistro ha frequenza non superiore a quella del figlio destro; il lato che congiunge un nodo al figlio sinistro ha etichetta 0 e quello che lo congiunge al figlio destro etichetta 1.


Prima di cominciare: le frequenze sono ammissibili

Le fjf_j sono positive e ∑j=1nfj=12n−1∑j=1n2j−1=2n−12n−1=1,\sum_{j=1}^{n} f_j = \frac{1}{2^n - 1} \sum_{j=1}^{n} 2^{j-1} = \frac{2^n - 1}{2^n - 1} = 1, per la somma di una progressione geometrica (SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →). Ogni frequenza è il doppio della precedente: fj+1=2fjf_{j+1} = 2 f_j. È la proprietà che governa tutto l'esercizio.

Punto (a): n=4n = 4

Qui 24−1=152^4 - 1 = 15 e (f1,f2,f3,f4)=(115,215,415,815).(f_1, f_2, f_3, f_4) = \left( \tfrac{1}{15}, \tfrac{2}{15}, \tfrac{4}{15}, \tfrac{8}{15} \right). Si indica con zjz_j la foglia del carattere cjc_j e si esegue HUFFMAN (Codifica di Huffman - codici prefissi, algoritmo e ottimalitàCompressione con codifica per carattere: la lunghezza variabile risparmia bit ma deve essere iniettiva, e i codici prefissi lo garantiscono; un codice prefisso è un trie binario con una foglia per carattere, di costo B(T) = somma di f(c) d_T(c), e quello ottimo è un albero pieno con n-1 nodi interni. L'algoritmo di Huffman fonde ripetutamente i due alberi di frequenza minima usando una coda con priorità (2n-1 estrazioni, costo O(n log n)); l'ottimalità si prova con la scelta greedy (i due caratteri più rari sono foglie sorelle di profondità massima, per scambio) e la sottostruttura (B(T) = B(T') + f(x) + f(y)). Esempio completo con 6 caratteri, B = 2,24.Codifica di Huffman - codici prefissi, algoritmo e ottimalità →): n−1=3n - 1 = 3 iterazioni, ciascuna fonde i due nodi di frequenza minima, con il più leggero a sinistra.

iterazione coda prima estratti (sx, dx) nodo creato coda dopo
1 z1 115, z2 215, z3 415, z4 815z_1\,\frac{1}{15},\ z_2\,\frac{2}{15},\ z_3\,\frac{4}{15},\ z_4\,\frac{8}{15} z1, z2z_1,\ z_2 z1..2z_{1..2} di peso 315\frac{3}{15} z1..2 315, z3 415, z4 815z_{1..2}\,\frac{3}{15},\ z_3\,\frac{4}{15},\ z_4\,\frac{8}{15}
2 (quella sopra) z1..2, z3z_{1..2},\ z_3 (315<415\frac{3}{15} < \frac{4}{15}) z1..3z_{1..3} di peso 715\frac{7}{15} z1..3 715, z4 815z_{1..3}\,\frac{7}{15},\ z_4\,\frac{8}{15}
3 (quella sopra) z1..3, z4z_{1..3},\ z_4 (715<815\frac{7}{15} < \frac{8}{15}) z1..4z_{1..4} di peso 1515=1\frac{15}{15} = 1 z1..4z_{1..4}

Il trie (i pesi sono in quindicesimi, 0 a sinistra e 1 a destra):

                         (15)
                       0/    \1
                  (7)          c4: 8
                0/   \1
            (3)        c3: 4
          0/   \1
      c1: 1     c2: 2

Parole di codice: e(c1)=000e(c_1) = 000, e(c2)=001e(c_2) = 001, e(c3)=01e(c_3) = 01, e(c4)=1e(c_4) = 1. Costo (lunghezza media): B(T)=115⋅3+215⋅3+415⋅2+815⋅1=3+6+8+815=2515=53≈1,67 bit per carattere.B(T) = \tfrac{1}{15} \cdot 3 + \tfrac{2}{15} \cdot 3 + \tfrac{4}{15} \cdot 2 + \tfrac{8}{15} \cdot 1 = \tfrac{3 + 6 + 8 + 8}{15} = \tfrac{25}{15} = \tfrac{5}{3} \approx 1{,}67 \text{ bit per carattere}. Controllo con la somma dei pesi dei nodi interni: 3+7+1515=2515\frac{3 + 7 + 15}{15} = \frac{25}{15}. Il codice a lunghezza fissa ne userebbe 22.

Nota sulla soluzione del corso: riporta B(T)=2515≈1,78B(T) = \frac{25}{15} \approx 1{,}78, ma 2515=1,6‾\frac{25}{15} = 1{,}\overline{6}; l'approssimazione 1,781{,}78 è un errore di stampa (la frazione è giusta).

Punto (b): nn generico

Passo 1: ogni frequenza supera la somma di tutte le precedenti. Per 1≤k≤n−11 \le k \le n - 1, ∑j=1kfj=12n−1∑j=1k2j−1=2k−12n−1<2k2n−1=fk+1.(∗)\sum_{j=1}^{k} f_j = \frac{1}{2^n - 1} \sum_{j=1}^{k} 2^{j-1} = \frac{2^k - 1}{2^n - 1} < \frac{2^k}{2^n - 1} = f_{k+1}. \tag{$\ast$} La disuguaglianza è stretta, quindi non ci sono mai pareggi nelle scelte dell'algoritmo, e il trie è univocamente determinato.

Passo 2: lo svolgimento di Huffman. Si indica con z1..kz_{1..k} il nodo che ha per foglie c1,…,ckc_1, \dots, c_k (con z1..1=z1z_{1..1} = z_1) e con w(z1..k)=∑j≤kfj=2k−12n−1w(z_{1..k}) = \sum_{j \le k} f_j = \frac{2^k - 1}{2^n - 1} il suo peso.

Affermazione. Per 1≤k≤n−11 \le k \le n - 1, all'inizio dell'iterazione kk la coda contiene esattamente i nodi z1..k,zk+1,zk+2,…,znz_{1..k}, z_{k+1}, z_{k+2}, \dots, z_n. L'iterazione kk estrae per primo z1..kz_{1..k} (diventa il figlio sinistro), poi zk+1z_{k+1} (figlio destro), e crea z1..k+1z_{1..k+1} di peso 2k+1−12n−1\frac{2^{k+1} - 1}{2^n - 1}.

Dimostrazione per induzione su kk.

  • k=1k = 1: la coda contiene le nn foglie z1,z2,…,znz_1, z_2, \dots, z_n con pesi f1<f2<⋯<fnf_1 < f_2 < \dots < f_n (la sequenza è strettamente crescente): i due minimi sono z1=z1..1z_1 = z_{1..1} e z2z_2, estratti in questo ordine; nasce z1..2z_{1..2} con peso f1+f2=32n−1f_1 + f_2 = \frac{3}{2^n - 1}.
  • Passo k→k+1k \to k + 1 (con k+1≤n−1k + 1 \le n - 1): alla fine dell'iterazione kk la coda contiene z1..k+1,zk+2,…,znz_{1..k+1}, z_{k+2}, \dots, z_n (i nodi z1..kz_{1..k} e zk+1z_{k+1} sono stati sostituiti da z1..k+1z_{1..k+1}). All'inizio dell'iterazione k+1k + 1 i pesi sono w(z1..k+1)=2k+1−12n−1w(z_{1..k+1}) = \frac{2^{k+1} - 1}{2^n - 1} e fk+2<fk+3<⋯<fnf_{k+2} < f_{k+3} < \dots < f_n. Per (∗)(\ast) con k+1k+1 al posto di kk vale w(z1..k+1)<fk+2w(z_{1..k+1}) < f_{k+2}, quindi il minimo è z1..k+1z_{1..k+1} e il secondo minimo è zk+2z_{k+2}, senza pareggi. Si fondono nell'ordine (sinistro, destro) =(z1..k+1,zk+2)= (z_{1..k+1}, z_{k+2}), con peso w(z1..k+1)+fk+2=2k+2−12n−1w(z_{1..k+1}) + f_{k+2} = \frac{2^{k+2} - 1}{2^n - 1}. □\square

Dopo l'iterazione n−1n - 1 resta il solo z1..nz_{1..n}, di peso 2n−12n−1=1\frac{2^n - 1}{2^n - 1} = 1 (controllo), la radice. Il confronto di sinistra e destra rispetta la convenzione del testo, perché il figlio sinistro è sempre quello di frequenza minore.

Passo 3: la forma del trie. I nodi interni sono z1..2,z1..3,…,z1..nz_{1..2}, z_{1..3}, \dots, z_{1..n} (n−1n - 1 nodi, come deve essere per un albero pieno con nn foglie) e il figlio sinistro di z1..k+1z_{1..k+1} è z1..kz_{1..k}, il destro è la foglia zk+1z_{k+1}. Quindi il trie è una catena sinistra: un unico cammino dalla radice z1..nz_{1..n} alla foglia più a sinistra z1z_1 che passa per z1..n,z1..n−1,…,z1..2,z1z_{1..n}, z_{1..n-1}, \dots, z_{1..2}, z_1, in cui ogni nodo interno ha come figlio destro una foglia: znz_n sotto la radice, zn−1z_{n-1} sotto z1..n−1z_{1..n-1}, ..., z2z_2 sotto z1..2z_{1..2}.

 radice z_{1..n} --0--> z_{1..n-1} --0--> ... --0--> z_{1..2} --0--> c_1
     |1                      |1                          |1
    c_n                    c_{n-1}                      c_2

Passo 4: le parole di codice. Il nodo z1..kz_{1..k} sta a profondità n−kn - k (la radice z1..nz_{1..n} a profondità 00, e ogni passo verso sinistra scende di un livello), e il suo cammino dalla radice è 0n−k0^{n-k}. Allora:

  • per 2≤j≤n2 \le j \le n la foglia cjc_j è il figlio destro di z1..jz_{1..j} (a profondità n−jn - j): la parola è 0n−j0^{n-j} seguita da 11, lunga n−j+1n - j + 1;
  • la foglia c1c_1 è il figlio sinistro di z1..2z_{1..2} (a profondità n−2n - 2): la parola è 0n−2 0=0n−10^{n-2}\,0 = 0^{n-1}, lunga n−1n - 1.

e(c1)=0n−1,e(cj)=0n−j 1(2≤j≤n).e(c_1) = 0^{n-1}, \qquad e(c_j) = 0^{n-j}\,1 \quad (2 \le j \le n).

Per n=4n = 4: e(c1)=000e(c_1) = 000, e(c2)=001e(c_2) = 001, e(c3)=01e(c_3) = 01, e(c4)=1e(c_4) = 1, come nel punto (a). In particolare il carattere più frequente ha una parola di un solo bit, e le lunghezze sono ∣e(cj)∣=n−j+1\lvert e(c_j) \rvert = n - j + 1 per j≥2j \ge 2, cioè la parola si accorcia di un bit ogni volta che la frequenza raddoppia.

Costo. Con la somma dei pesi dei nodi interni, w(z1..k)=2k−12n−1w(z_{1..k}) = \frac{2^k - 1}{2^n - 1} per k=2,…,nk = 2, \dots, n: B(Tn)=∑k=2n2k−12n−1=(2n+1−4)−(n−1)2n−1=2n+1−n−32n−1.B(T_n) = \sum_{k=2}^{n} \frac{2^k - 1}{2^n - 1} = \frac{(2^{n+1} - 4) - (n - 1)}{2^n - 1} = \frac{2^{n+1} - n - 3}{2^n - 1} . Per n=4n = 4: 32−715=2515\frac{32 - 7}{15} = \frac{25}{15}, come nel punto (a). Per n→∞n \to \infty tende a 22 bit per carattere, anche se il cammino più lungo ha n−1n - 1 bit.

Ottimalità. Il trie ottenuto è quello ritornato da HUFFMAN, quindi è ottimo per il teorema di correttezza dell'algoritmo; la forma a catena è l'unica possibile con questo input perché le scelte sono forzate dalle disuguaglianze strette.

Verifica

Con Python (HUFFMAN con heapq e frazioni esatte) per ogni nn da 22 a 2020: le parole sono esattamente 0n−10^{n-1} e 0n−j10^{n-j}1, il figlio sinistro ha sempre frequenza ≤\le del destro, e il costo è 2n+1−n−32n−1\frac{2^{n+1} - n - 3}{2^n - 1}, uguale alla somma dei pesi dei nodi interni; per n=4n = 4 vengono 000,001,01,1000, 001, 01, 1 e 2515=53\frac{25}{15} = \frac{5}{3}.

Versione ripasso

  • Dati: fj=2j−12n−1f_j = \frac{2^{j-1}}{2^n - 1} per 1≤j≤n1 \le j \le n; fj+1=2fjf_{j+1} = 2 f_j e ∑jfj=2n−12n−1=1\sum_j f_j = \frac{2^n - 1}{2^n - 1} = 1 (SommatorieIl simbolo di sommatoria, le sue proprietà (linearità, additività, cambio di indice) e le somme notevoli di Gauss e geometrica.Sommatorie →). Convenzioni: il figlio sinistro ha frequenza ≤\le del destro; 0 a sinistra e 1 a destra. Richieste: (a) trie e parole per n=4n = 4 (6 punti); (b) forma del trie e parola di cjc_j per nn generico, con giustificazione formale (10 punti).
  • (a) n=4n = 4: frequenze (1,2,4,8)/15(1, 2, 4, 8)/15. Fusioni: z1+z2=z1..2z_1 + z_2 = z_{1..2} di peso 315\frac{3}{15}; z1..2z_{1..2} (315\frac{3}{15}) con z3z_3 (415\frac{4}{15}) dà z1..3z_{1..3} di peso 715\frac{7}{15}; z1..3z_{1..3} con z4z_4 (815\frac{8}{15}) dà la radice 11. Parole e(c1)=000e(c_1) = 000, e(c2)=001e(c_2) = 001, e(c3)=01e(c_3) = 01, e(c4)=1e(c_4) = 1. B=3+6+8+815=2515=53≈1,67B = \frac{3 + 6 + 8 + 8}{15} = \frac{25}{15} = \frac{5}{3} \approx 1{,}67 (il corso scrive 1,781{,}78, errore di stampa); controllo con i nodi interni 3+7+1515\frac{3 + 7 + 15}{15}. Algoritmo e convenzioni in Codifica di Huffman - codici prefissi, algoritmo e ottimalitàCompressione con codifica per carattere: la lunghezza variabile risparmia bit ma deve essere iniettiva, e i codici prefissi lo garantiscono; un codice prefisso è un trie binario con una foglia per carattere, di costo B(T) = somma di f(c) d_T(c), e quello ottimo è un albero pieno con n-1 nodi interni. L'algoritmo di Huffman fonde ripetutamente i due alberi di frequenza minima usando una coda con priorità (2n-1 estrazioni, costo O(n log n)); l'ottimalità si prova con la scelta greedy (i due caratteri più rari sono foglie sorelle di profondità massima, per scambio) e la sottostruttura (B(T) = B(T') + f(x) + f(y)). Esempio completo con 6 caratteri, B = 2,24.Codifica di Huffman - codici prefissi, algoritmo e ottimalità →.
  • (b) Chiave: per 1≤k≤n−11 \le k \le n-1, ∑j≤kfj=2k−12n−1<2k2n−1=fk+1\sum_{j \le k} f_j = \frac{2^k - 1}{2^n - 1} < \frac{2^k}{2^n - 1} = f_{k+1}, disuguaglianza stretta: nessun pareggio, il trie è univocamente determinato.
  • Affermazione (induzione su kk): all'inizio dell'iterazione kk la coda contiene z1..k,zk+1,…,znz_{1..k}, z_{k+1}, \dots, z_n; l'iterazione estrae z1..kz_{1..k} (sinistro) e zk+1z_{k+1} (destro) e crea z1..k+1z_{1..k+1} di peso 2k+1−12n−1\frac{2^{k+1} - 1}{2^n - 1}. Base k=1k = 1: f1<f2<⋯<fnf_1 < f_2 < \dots < f_n, i minimi sono z1z_1 e z2z_2. Passo: w(z1..k+1)<fk+2<fk+3<…w(z_{1..k+1}) < f_{k+2} < f_{k+3} < \dots per la chiave sopra.
  • Forma del trie: catena sinistra con n−1n - 1 nodi interni z1..n,z1..n−1,…,z1..2z_{1..n}, z_{1..n-1}, \dots, z_{1..2}; il figlio sinistro di z1..k+1z_{1..k+1} è z1..kz_{1..k} e il destro è la foglia ck+1c_{k+1}; la foglia più a sinistra è c1c_1.
  • Parole: z1..kz_{1..k} è a profondità n−kn - k con cammino 0n−k0^{n-k}; cjc_j (2≤j≤n2 \le j \le n) è figlio destro di z1..jz_{1..j}, quindi e(cj)=0n−j 1e(c_j) = 0^{n-j}\,1 (lunghezza n−j+1n - j + 1); c1c_1 è figlio sinistro di z1..2z_{1..2} (profondità n−2n - 2), quindi e(c1)=0n−1e(c_1) = 0^{n-1}. Per n=4n = 4 si ritrova 000,001,01,1000, 001, 01, 1: la parola si accorcia di un bit ogni volta che la frequenza raddoppia.
  • Costo: somma dei pesi dei nodi interni, B(Tn)=∑k=2n2k−12n−1=(2n+1−4)−(n−1)2n−1=2n+1−n−32n−1→2B(T_n) = \sum_{k=2}^{n} \frac{2^k - 1}{2^n - 1} = \frac{(2^{n+1} - 4) - (n - 1)}{2^n - 1} = \frac{2^{n+1} - n - 3}{2^n - 1} \to 2; per n=4n = 4 vale 2515\frac{25}{15}. Il trie è ottimo perché è quello di HUFFMAN.
  • Verifica: con Python per n=2..20n = 2..20 parole, convenzione sinistra/destra e costo coincidono con le formule.
  • Errori tipici: scambiare destra e sinistra (il più leggero, z1..kz_{1..k}, va a sinistra); scrivere 0n−10^{n-1} anche per c2c_2 invece di 0n−210^{n-2}1; dimenticare di motivare l'assenza di pareggi con la disuguaglianza stretta; contare nn nodi interni invece di n−1n-1; dare la forma per nn generico senza induzione.

Teoria collegata