Tema 4

Termen de predare: Luni, 25 Mai 2015, ora 23:55

Deadline hard: Joi, 28 Mai, ora 23:55

1 Mai Adaugat precizare legata de reprezentantul unui nod. Actualizat arhiva de teste.

23 Mai Modificare deadline: 25 Mai.

24 Mai Update script testare: limita de timp a fost marita la 100 de secunde.

Obiective

În urma realizării acestei teme, studentul va fi capabil:

  • să lucreze cu structuri arborescente de tip heap şi cu cozi prioritare bazate pe heap-uri
  • să implementeze algoritmul de compresie Huffman
  • să folosească algoritmii de compresie pentru reprezentarea compactă a imaginilor în format bitmap

Cunoştinţe necesare

Pentru a putea rezolva această temă, studentul trebuie să fie familiar cu următoarele noţiuni:

  • structura de heap, folosită pentru implementarea unei cozi prioritare;
  • structura de arbore binar;
  • codificarea Huffman - o documentaţie accesibilă poate fi găsită aici.

Cerinţă

Enunt

Sarcina voastră este să implementaţi un program care să comprime / decomprime un fişier arbitrar, aplicând algoritmul Huffman. Aici este descris, pe scurt, algoritmul şi aveţi şi un exemplu pentru compresia unui şir de caractere (fişier text). Algoritmul funcţionează pentru orice tip de fişier (nu neapărat text): se consideră fişierul ca un şir de octeţi.

Executabil

Programul vostru va trebui să primească la intrare numele unui fişier şi o serie de opţiuni, în felul următor:

./comp -c <nume_fisier_in>

sau

./comp -d <nume_fisier_in>.cmp

unde:

  • -c indică faptul că programul va realiza o compresie. Numele fişierului de ieşire va fi <nume_fisier_in>.cmp
  • -d indică faptul că programul va realiza decompresia pentru un fişier comprimat anterior. Numele fisierului de iesire va fi decompressed_<nume_fisier_in>
  • <nume_fisier_in> reprezintă fişierul asupra căruia se aplică compresia.

Astfel, daca numele fişierului este poza.bmp, fişierul comprimat se va numi poza.bmp.cmp, iar fişierul decomprimat decompressed_poza.bmp

Compresie

Se va considera şirul de octeti din fişier ca pe un şir de simboluri. Acestui şir de simboluri va trebui să-i aplicaţi o codificare Huffman, (a se vedea documentaţia pusă la dispoziţie) asociind fiecărui simbol o probabilitate de apariţie egală cu frecvenţa simbolului în fişier (deci va trebui să parcurgeţi la început întregul fişier pentru a determina aceste probabilităţi). Pentru reprezentarea cozii prioritare din care veţi extrage nodurile cu costul cel mai mic, veţi folosi o structură de heap. Pentru heap, veţi avea nevoie de o funcţie care compară două noduri între ele, şi veţi folosi următoarele reguli (presupunem ca prioritatea este maximă în vârful cozii):

  • dacă probabilitate(Nod1) < probabilitate(Nod2), atunci Nod1 > Nod2 (ordinea se inversează), şi viceversa.
  • dacă probabilitate(Nod1) = probabilitate(Nod2), atunci ne uităm la înălţimea nodului în arbore, şi dacă înălţime(Nod1) < înălţime(Nod2), atunci Nod1 > Nod2 (ordinea se inversează), şi viceversa.
  • dacă probabilitate(Nod1) = probabilitate(Nod2), şi înălţime(Nod1) = înălţime(Nod2), atunci ne uitam la reprezentantii nodurilor (vezi definitie mai jos). Daca reprezentant(Nod1) < reprezentant(Nod2) atunci Nod1 > Nod2 (ordinea se inversează), si viceversa.

Practic, aceste reguli specifică ordinea în care se vor scoate elementele din coadă: se preferă cele cu probabilitate mică, iar dacă sunt mai multe cu aceeaşi probabilitate, le vom lua pe cele de pe nivelurile cele mai mici, pentru a păstra echilibrat arborele. Daca si inaltimile sunt egale, luam in considerare reprezentantul. Astfel vom avem un mod unic de a scoate elementele din coada prioritara.

Reprezentantul unui nod se defineste in felul urmator:

  • daca nodul este frunza, reprezentantul este caracterul(octetul) asociat frunzei respective (In exemplul de aici reprezentantii tuturor frunzelor sunt 'a', 'n', ' ', 'r', 'e', 'm').
  • altfel, reprezentant(Nod) = minim {reprezentant(Nod_stanga), reprezentant(Nod_dreapta)} unde Nod_stanga si Nod_dreapta sunt fii nodului Nod.

Serializare

Arborele Huffman construit va fi necesar şi in pasul de decompresie, astfel că este nevoie sa fie salvat in fişier. Reprezentarea arborelui în fişier trebuie să fie cel de vector de structuri de forma:

struct HuffmanSerializedNode {
  	uint8_t isTerminal;
  	union {
	    uint8_t value;
	    struct {
	      uint16_t leftChild;
	      uint16_t rightChild;
	    } __attribute__((__packed__)) childData;
  	} __attribute__((__packed__));
} __attribute__((__packed__));

Mai întâi se scrie în fişier un întreg de tip uint16_t care specifică numărul de elemente de tip HuffmanSerializedNode care urmează, apoi urmează vectorul de structuri, unde câmpurile din fiecare structură semnifică:

  • isTerminal specifică dacă nodul din arbore este terminal (valoare 1) sau nu (valoare 0); dacă nodul este terminal, va fi valid câmpul value din union;
  • value reprezintă valorea simbolului (valoarea octetului) din frunza arborelui;
  • childData reprezintă informaţii despre descendenţi, în cazul nodurilor neterminale; leftChild şi rightChild reprezintă indecşi în vectorul de structuri (indexarea porneşte de la 0), şi indică structurile ce codifică descendenţii stâng, respectiv drept, ai nodului. Trebuie remarcat faptul că într-un arbore Huffman, un nod fie este terminal, fie are exact doi descendenţi, deci cele două câmpuri vor fi valide simultan. De asemenea, structura de pe poziţia 0 din vector va fi rădăcina arborelui Huffman.

Observam ca reprezentantul unui nod nu este salvat in fisier deoarece nu mai avem nevoie de el. Acesta a fost folosit pentru a garanta o secventa unica de extragere a nodurilor din coada prioritara.

După ce este scris arborele Huffman se scrie şi dimensiunea fişierului de intrare ca un element de tip întreg fara semn(necesar la decomprimare pentru a şti câte simboluri trebuie decodificate). Urmează simbolurile scrise codificat în ordinea în care au fost citite din fişierul original. În urma aplicării arborelui Huffman asupra simbolurilor, se obţine un şir de biţi.

Se convine ca un bit '1' să reprezinte o trecere spre descendentul drept în arborele Huffman construit, în timp ce un bit '0' să însemne o trecere spre descendentul stâng.

Aceştia vor fi scrişi in fişerul rezultat, grupaţi pe octeţi, în felul următor:

[7 6 5 4 3 2 1 0] [15 14 13 12 11 10 9 8] [23 22 21….

Conform exemplului de aici sirul “ana are mere” se codifica astfel:

Bit index01234567891011121314151617181920212223242526272829
Value011100011110100101111101100010

Octetii care vor fi scrisi in fisier sunt

  • 10001110(2) = 142(10)
  • 10010111(2) = 151(10)
  • 10111110(2) = 190(10)
  • 00010001(2) = 17(10).

Atentie: in cazul in care numarul total de biti nu este multiplu de 8, se vor adauga biti de 0 ca padding.

Pentru operaţiile pe biţi folosiţi DOAR variabile unsigned.

Recomandarea noastră este să folosiţi tipurile de date uint8_t / uint16_t / uint32_t după caz.

Format fisier comprimat

Conform celor spuse mai sus, formatul unui fisier comprimat trebuie sa arate astfel (in aceasta ordine):

CampK V N L
Semnificatienumarul de noduri din arbore arborele serializat dimensiunea fisierului original fisier comprimat
Tip/Dimensiune 1 * uint16_t K * HuffmanSerializedNode 1 * uint32_t sir de biti ce reprezinta fisierul comprimat

Testare

Testarea temei se va face folosind vmchecker.

Arhiva voastră trebuie să conţina sursele, un fişier README şi un fişier Makefile cu 2 reguli build şi clean. Regula build va compila sursele şi va genera un executabil numit comp. Regula clean va sterge fisierele temporare generate in timpul compilării (dacă există) şi executabilul comp.

Fişierul README trebuie să conţină detalii despre implementarea temei.

Puteţi descărca checker-ul pentru testare locală sub linux de aici.

Folositi flag-ul de optimizare -O2 la compilare.

Alte precizari

  • Atunci cand se construieste un nod nou, nodul cel mai prioritar din coada va deveni fiul stang al noului nod.
  • Serializarea arborelui se va face printr-o parcurgere in preordine.
  • Se recomanda ca atat citirea cat si scrierea sa se realizeze cu functiile fread si fwrite.
  • Se recomanda folosirea tipurile de date uint8_t, uint16_t si uint32_t in cazul citirii si scrierii din fisiere.
  • Necesitatea folosirii a mai multor tipuri de structuri de date în acest program sugerează o implementare modulară a acestora. Separaţi funcţionalitatea heap-urilor şi a arborilor Huffman de restul programului, pentru a vă face codul reutilizabil. Abordarea unui stil de programare nestructurat va atrage după sine depunctări. Vor fi apreciate implementările ce folosesc mai multe fişiere sursă pentru a separa modulele programului, şi vor fi penalizate implementări în care structuri diferite sunt implementate în aceeaşi funcţie sau care folosesc excesiv variabile globale pentru a comunica date între module.
  • Organizarea fişierelor sursă este lasată la latitudinea voastră. Va trebui să furnizaţi un fişier Makefile cu target-ul “build”, care să compileze sursele şi să genereze în directorul curent un executabil cu numele “comp”.
  • Tema trebuie obligatoriu rezolvata folosind C++
    • Puteți folosi funcționalități ale C++, cu excepția claselor care implementeaza direct funcționalitatea cerută (cum ar fi priority_queue din STL).
sd-ca/2015/teme/tema04.txt · Last modified: 2016/02/22 22:48 by alexandru.olteanu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0