This shows you the differences between two versions of the page.
sd-ca:teme:tema3-2021 [2021/05/13 00:41] gabriel_danut.matei |
sd-ca:teme:tema3-2021 [2021/05/30 19:42] (current) gabriel_danut.matei |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | <hidden> | + | ====== Tema 3 - Rope data structure ====== |
- | ====== Tema 2 - Count-distinct problem ====== | + | |
** Responsabili: ** | ** Responsabili: ** | ||
Line 6: | Line 5: | ||
*[[matei.danut.dm@gmail.com|Dănuţ Matei]] | *[[matei.danut.dm@gmail.com|Dănuţ Matei]] | ||
- | ** Data publicării : 26 martie, ora: 21:00 ** | + | ** Data publicării : 13 mai, ora: 23:55 ** |
- | ** Deadline: 17 aprilie, ora 23:55 ** | + | ** Deadline: 3 iunie, ora 23:55 ** |
== Modificări şi actualizări == | == Modificări şi actualizări == | ||
- | * **27 martie, ora 01:30** - adăugat clarificari in sectiunea **Introducere** | + | * **21 mai, ora 23:20** - actualizat secțiunea **Q&A** ca urmare a întrebărilor de pe **Forum** |
- | * **27 martie, ora 21:05** - adăugat **checker** in **enunt** si pe **vmchecker** | + | * **30 mai, ora 16:30** - configurat tema pe **vmchecker** |
- | * **27 martie, ora 23:40** - modificat fisier **Makefile** din **checker** | + | * **30 mai, ora 19:42** - actualizat secțiunea **Q&A** ca urmare a întrebărilor de pe **Forum** |
- | * **29 martie, ora 16:00** - adăugat precizari in **cerinta II** legate de **dimensiunea Hashtable-ului** | + | |
- | * **29 martie, ora 18:40** - adăugat corectare in **cerinta III** legata de **definitia lui m** | + | |
- | * **29 martie, ora 19:30** - modificat fisier **check.sh** din **checker**; acum partea de **valgrind** are comportamentul corect pentru toate cerintele | + | |
- | * **3 aprilie, ora 19:10** - reformulat **cerinta III** pentru a o face mai usor de inteles | + | |
- | * **11 aprilie, ora 12:05** - actualizat **deadline** | + | |
- | |||
- | |||
- | * **26 martie, ora 21:00** - adăugat **checker** | ||
===== Obiective ===== | ===== Obiective ===== | ||
În urma realizării acestei teme: | În urma realizării acestei teme: | ||
- | * veţi învăţa să lucraţi cu Dictionare | + | * veţi învăţa să lucraţi cu Arbori |
- | * vă veţi familiariza cu rezolvarea unei probleme reale prin structuri de date din ce in ce mai eficiente | + | * vă veţi familiariza cu o structură de date folosită în editoare de text reale |
===== Introducere ===== | ===== Introducere ===== | ||
- | **Problema estimarii cardinalitatii** (a numararii elementelor distincte) este, in esenta, gasirea numarului de elemente unice dintr-o colectie de elemente care se pot repeta. | + | Scopul acestei teme este implementarea unei structuri de date arborescente |
- | <note important> | + | numită **Rope**. Aceasta permite efectuarea eficientă de operații (inserare, concatenare, |
- | Pentru cerintele **I** si **II**, vom rezolva o problema si mai restrictiva: gasirea **numarului de aparitii** pentru fiecare element. Pentru cerinta **III**, vom vedea ca acest lucru e mai greu realizabil cand vine vorba de volume mari de date si, de aceea, ne vom rezuma la **gasirea numarului de elemente distincte**.</note> | + | ștergere etc.) pe un șir de caractere foarte lung (problemă caracteristică editoarelor de text |
+ | de orice tip). | ||
+ | Un Rope este un arbore binar cu //2 proprietăți//: | ||
- | **Conceptual, ne referim la:** | + | 1. **Frunzele** conțin //stringuri// de lungime >= 1 |
- | INPUT: | + | 2. Fiecare nod are o greutate asociată: |
- | ''1, 34, 2, 2, 2, 3'' | + | - pentru **frunze**, acest număr este //lungimea stringului// pe care îl conține fiecare |
- | OUTPUT: | + | - pentru **restul nodurilor**, acest număr este //suma greutăților frunzelor din |
+ | subarborele stâng// | ||
- | **Pentru cerintele I si II:** | + | Prin punerea cap la cap a stringurilor din frunze, de la stânga la dreapta, obținem |
+ | stringul pe care îl reprezintă Rope-ul. | ||
- | ''1 - 1'' | ||
- | ''2 - 3'' | + | <note important>Înainte să vă apucați de implementare, citiți și secțiunile de **Precizări** si **FAQ**.</note> |
- | ''3 - 1'' | ||
- | ''34 - 1'' | + | Va trebui să implementați următoarele operații: |
- | **Pentru cerinta III:** | + | ==== 1. Concat - 10p ==== |
- | ''Exista 4 elemente distincte'' | + | <code c> |
+ | RopeTree* concat(RopeTree* rt1, RopeTree* rt2); | ||
+ | </code> | ||
- | <note warning>**Fiecare** dintre cele **3** cerinte se va implementa intr-un **fisier separat**.</note> | + | Concat(R1, R2) - concatenarea a 2 rope-uri |
- | ===== I. Vector de frecventa - 25p ===== | + | ex: Concat(Rope("ab"), Rope("cde")) -> Rope("abcde") |
- | La intrare se dau numere intre **0** si **2000000**. Gasiti numarul de aparitii ale fiecarui element, utilizand un **vector de frecventa**. | + | {{:sd-ca:teme:tema3_2021:concat.png?800 |}} |
- | Un vector de frecventa este un vector care are pe pozitia **i** //numarul de aparitii// ale elementului **i**. | ||
- | {{:sd-ca:teme:freq.png|}} | + | ==== 2. Index - 10p ==== |
- | //Se garanteaza ca numarul de aparitii ale oricarui element este mai mic decat **256**.// | + | <code c> |
+ | char indexRope(RopeTree* rt, int idx); | ||
+ | </code> | ||
- | ===== II. Hashtable cu open addressing - 25p ===== | ||
- | La intrare se dau siruri de caractere. Gasiti numarul de aparitii ale fiecarui sir folosind un Hashtable cu politica de rezolvare a conflictelor de tip **open addressing** prin **linear probing**. | + | Index(R1, i) - obținerea caracterului de pe poziția i dintr-un rope |
- | Aceasta politica presupune ca, in momentul in care bucketul unde trebuie realizata insertia este deja ocupat, //se va cauta secvential o pozitie libera incepand cu bucketul urmator//. | + | ex: Index(Rope("abcde"), 1) -> 'b' |
- | {{:sd-ca:teme:hash1.png|}} | + | {{:sd-ca:teme:tema3_2021:index.png?300 |}} |
- | Evident, daca si aceasta pozitie este ocupata, vom cauta prima pozitie libera in continuare. | + | ==== 3. Search - 20p ==== |
- | {{:sd-ca:teme:hash2.png|}} | + | <code c> |
+ | char* search(RopeTree* rt, int start, int end); | ||
+ | </code> | ||
- | Daca se va ajunge la finalul listei de bucketuri, se va continua de la inceput. | + | Search(R1, start, end) - obținerea stringului din intervalul [start,end) |
- | {{:sd-ca:teme:hash3.png|}} | + | ex: Search(Rope("abcde"), 1, 3) -> "bc" |
- | //Se garanteaza existenta a cel putin unui bucket liber in momentul fiecarei operatii de insertie.// **Pentru a satisface aceasta conditie, o idee ar fi ca dimensiunea Hashtable-ului sa fie egala cu numarul de siruri existente in fisierul de intrare.** | + | {{:sd-ca:teme:tema3_2021:search.png?300 |}} |
- | Evident, daca in momentul unei operatii de selectie nu gasim cheia in bucketul in care ne-am astepta, vom continua cautarea secvential, aplicand un procedeu similar cu cel din momentul insertiei. | + | ==== 4. Split - 20p ==== |
- | //Se garanteaza ca lungimea maxima a oricarui sir este maxim **100** de caractere.// | + | <code c> |
+ | SplitPair split(RopeTree* rt, int idx); | ||
+ | </code> | ||
- | //Se garanteaza ca numarul de aparitii ale oricarui element este mai mic decat **256**.// | + | Split(R1, i) - împărțirea unui rope în 2 rope-uri separate |
- | ===== III. Estimatori probabilistici - 30p ===== | + | ex: Split(Rope("abcde"), 2) -> (Rope("ab"), Rope("cde")) |
- | In cerintele anterioare am observat ca putem calcula cu exactitate numarul de elemente distincte (si numarul lor de aparitii) retinand, intr-un fel sau altul, fiecare element unic (ca pozitie intr-un vector, respectiv ca cheie intr-un hashtable). Din pacate, in aplicatiile din lumea reala, //aceasta strategie nu este sustenabila//. | + | Exemplu 1: |
- | Sa ne imaginam urmatoarea situatie: **Youtube** afiseaza pentru fiecare videoclip **numarul de vizualizari**. Pentru ca acest sistem sa nu fie abuzat (spre exemplu de un bot care vizioneaza acelasi videoclip incontinuu pentru a-i spori view count-ul), trebuie sa tina cont si de **numarul de utilizatori unici** care au accesat clipul. | + | {{:sd-ca:teme:tema3_2021:split1.png?700 |}} |
- | Daca ar retine un id pentru fiecare utilizator, ar avea nevoie de o structura de date cu milioane de intrari, si asta doar pentru un singur clip. Tinand cont ca exista //peste **31 de milioane** de canale// (iar multe dintre ele au **peste 100 de clipuri**), acest lucru iese din discutie. | + | \\ |
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | Exemplu 2: | ||
- | Cum putem totusi sa ne indeplinim obiectivul? | + | {{:sd-ca:teme:tema3_2021:split2.png?700 |}} |
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
+ | \\ | ||
- | Problema la abordarea anterioara este faptul ca foloseste o cantitate de memorie proportionala cu numarul de utilizatori distincti **O(n)**. In cautarea unei solutii mai bune, va trebui sa obtinem o complexitate a spatiului mai mica (**O(sqrt(n))**, **O(logn)**, **O(1)** etc.) | + | Exemplu 3: |
- | Solutia gasita (ce urmeaza a fi implementata in tema) aduce cu sine un sacrificiu din punct de vedere al preciziei: din moment ce nu stim exact ce utilizatori am avut, numarul total de utilizatori unici **nu va mai fi unul precis, ci doar aproximativ**. | + | {{:sd-ca:teme:tema3_2021:split3.png?900 |}} |
- | In practica, acest lucru nu este un dezavantaj prea mare (intrucat rareori e relevanta diferenta dintre 1m vizualizari si 1.1m vizualizari). | + | ==== 5. Insert - 5p ==== |
- | In ilustrarea functionarii algoritmului **HyperLogLog**, vom incepe de la o serie de principii simple pe care le vom pune cap la cap, ajungand la descrierea algoritmului final. | + | <code c> |
+ | RopeTree* insert(RopeTree* rt, int idx, const char* string); | ||
+ | </code> | ||
- | <note important>Sectiunile 1 si 2 sunt prezentate pentru a intelege de ce functioneaza HyperLogLog. Pentru a rezolva tema, trebuie sa implementati **doar algoritmul final (descris in sectiunea 3)**.</note> | + | Insert(R1, i, str) - inserarea unui string pe o poziție a unui Rope |
- | ==== 1. Probabilistic counting ==== | + | ex: Insert(Rope("abcde"), 1), "123") -> Rope("a123bcde") |
- | Sa presupunem ca generam un numar la intamplare. | ||
- | Probabilitatea ca numarul sa inceapa cu **un** bit **0** este **1/2** (deoarece poate incepe fie cu 0, fie cu 1). | + | <note important>Dacă ați implementat corect operațiile Split și Concat, Insert-ul va fi practic un Split, urmat de două Concat-uri.</note> |
- | Probabilitatea ca numarul sa inceapa cu **2** biti **0** este **1/4** (deoarece poate incepe cu 00, 01, 10 sau 11). | ||
- | Similar, probabilitatea ca numarul sa inceapa cu **3** biti **0** este **1/8**. Astfel, pentru a intalni un numar care sa inceapa cu **3** biti **0**, //va trebui sa generam, in medie, **8** numere//. | ||
- | {{:sd-ca:teme:probcount1.png|}} | + | ==== 6. Delete - 5p ==== |
- | Privind aceasta observatie in sens invers, daca am generat numere aleatoare si secventa cea mai lunga de **0** de la inceputul oricarui numar a fost de lungime **3**, atunci avem doua posibilitati: | + | <code c> |
+ | RopeTree* delete(RopeTree* rt, int start, int len); | ||
+ | </code> | ||
- | - am generat //cel putin **8** numere// | + | Delete(R1, start, len) - ștergerea substringului din intervalul [start, start+len) |
- | - am avut noroc si a trebuit sa generam //mai putin de 8 numere// | + | ex: Delete(Rope("abcde"), 1, 2) -> Rope("ade") |
- | Evident, pentru valori mici precum **2** sau **3** biti consecutivi, exista o sansa semnificativa sa generam numarul mai rapid (chiar din prima incercare), dar cu cat valorile devin mai mari, cu atat scade aceasta sansa. | + | <note important>Dacă ați implementat corect operațiile Split și Concat, Delete-ul va fi practic două Split-uri, urmate de un Concat.</note> |
- | In principiu, daca am primit valori aleatoare la intrare, si numarul maxim de biti **0** consecutivi initiali ai oricarui numar este **x**, putem spune ca am primit //intre **2^x** si **2^(x+1)** valori//. | + | === Precizări === |
+ | <note warning>Aveți grijă ca Rope-urile create de funcțiile voastre să **respecte cele două proprietăți din definiția unui Rope**!</note> | ||
- | In viata reala, valorile primite la intrare //nu vor fi neaparat valori aleatoare//. Mai mult, nu toate valorile de intrare vor fi numere intregi. Din acest motiv, vom trece aceste valori printr-o **functie de hash**. O functie de hash buna ar trebui sa ofere la iesire //valori uniform distribuite//. | + | <note warning>Nu încercați să vă folosiți de "trucuri" pentru a ocoli dificultatea cerințelor. |
- | ==== 2. LogLog ==== | + | Următorele sunt exemple |
+ | de operații care, deși returnează Rope-uri care respectă definiția, **nu vor fi punctate**: | ||
+ | </note> | ||
- | Daca vrem sa imbunatatim performanta algoritmului nostru va trebui sa: | + | {{:sd-ca:teme:tema3_2021:nu2.png?400 |}} |
- | - Atenuam efectul negativ al generarii rapide unui numar cu multi biti de **0** initiali | + | {{:sd-ca:teme:tema3_2021:nu1.png?600 |}} |
- | - Oferim estimari //mai granulare// decat **puterile lui 2** | + | {{:sd-ca:teme:tema3_2021:nu3.png?600 |}} |
- | {{:sd-ca:teme:2n_table.png|}} | + | <note warning>Dacă nu sunteți siguri, puteți întreba pe forum. |
+ | </note> | ||
- | O idee de rezolvare a primei probleme este sa impartim numerele in mai multe **bucketuri**. Cea mai usoara modalitate de a face acest lucru este sa impartim fiecare numar in 2 parti: //prima parte// va fi folosita pentru a determina bucketul, iar //a doua// va fi folosita ca pana acum. | + | === Schelet + Checker === |
- | + | {{:sd-ca:teme:tema3_2021:checker.zip|DOWNLOAD}} | |
- | {{:sd-ca:teme:buckets.png|}} | + | |
- | + | ||
- | Singura diferenta fata de metoda precedenta este ca acum vom face maximul de zerouri consecutive //pentru fiecare bucket// si nu pentru toate numerele. | + | |
- | + | ||
- | Pentru a agrega aceste maxime vom folosi **media geometrica**. | + | |
- | + | ||
- | ==== 3. HyperLogLog ==== | + | |
- | + | ||
- | Recapituland ce am prezentat in sectiunile precedente, in cadrul algoritmului HyperLogLog avem 3 etape: | + | |
- | + | ||
- | **1)** stabilim numarul total de bucketuri **m**, apoi initializam cu 0 un vector **M** de dimensiune **m**. | + | |
- | <note important>Alegerea lui m este diferita de la caz la caz. In contextul problemei curente, puteti sa folositi valoarea m = $2^{11}$, insa exista si alte variante posibile.</note> | + | |
- | + | ||
- | **2)** pentru fiecare numar citit de la intrare: | + | |
- | + | ||
- | - ii calculam hash-ul cu o functie de hash pentru numere intregi | + | |
- | + | ||
- | - pe baza primilor $\log_2(m)$ biti din hash determinam bucketul in care se afla (din cele $m$ bucketuri posibile); notam numarul bucketului cu **j** | + | |
- | + | ||
- | - calculam numarul de biti 0 initiali din restul hash-ului; notam acest numar cu **x** | + | |
- | + | ||
- | - M[j] = max(M[j], x) | + | |
- | + | ||
- | **3)** agregam valorile din toate bucketurile | + | |
- | + | ||
- | + | ||
- | In sectiunea precedenta, am mentionat ca pentru a agrega valorile din fiecare bucket, folosim media geometrica. Pentru a implementa HyperLogLog, vom folosi in locul ei urmatorea medie: | + | |
- | + | ||
- | {{:sd-ca:teme:media.png|}} | + | |
- | + | ||
- | Ca exemplu, pentru bucketul evidentiat cu verde, **j = 6**, iar **M[j] = 2** | + | |
- | + | ||
- | {{:sd-ca:teme:buckets.png|}} | + | |
- | + | ||
- | + | ||
- | Avand aceasta medie **Z**, raspunsul final **E** (numarul de elemente distincte intalnite) va fi dat de urmatorea formula: | + | |
- | + | ||
- | {{:sd-ca:teme:estimare.png|}} | + | |
- | + | ||
- | Explicatie: | + | |
- | + | ||
- | **m**, ca si pana acum, este numarul total de bucketuri folosite | + | |
- | + | ||
- | **$\alpha_m$** reprezinta //factorul de atenuare//, calculat in functie de **m** dupa urmatoarea formula: | + | |
- | + | ||
- | {{:sd-ca:teme:alfa.png|}} | + | |
- | + | ||
- | + | ||
- | //Pentru ultima cerinta, citirea se va face dintr-un fisier al carui nume este primit ca parametru.// | + | |
- | + | ||
- | === Precizări === | + | |
- | <note warning>Rezolvati cerintele I si II utilizand structura de date ceruta. Nerespectarea acestui lucru va aduce la anularea punctajului pentru cerinta respectiva.</note> | + | |
- | + | ||
- | <note important>Având în vedere ca a 3-a parte a temei presupune implementarea unei structuri de date probabilistice, checkerul ofera punctajul daca raspunsul vostru se incadreaza intr-o marja de eroare de 10% fata de raspunsul corect.</note> | + | |
- | + | ||
- | === Checker === | + | |
- | {{:sd-ca:teme:2-distinct-count.zip|CHECKER}} | + | |
Temele vor fi trimise pe [[https://elf.cs.pub.ro/vmchecker/ui/#SD|vmchecker]]. | Temele vor fi trimise pe [[https://elf.cs.pub.ro/vmchecker/ui/#SD|vmchecker]]. | ||
Line 214: | Line 193: | ||
Arhiva trebuie să conțină: | Arhiva trebuie să conțină: | ||
* surse | * surse | ||
- | * fișierul Makefile **din arhiva cu checkerul** | ||
* fișier **README** care să conțină detalii despre implementarea temei | * fișier **README** care să conțină detalii despre implementarea temei | ||
Line 222: | Line 200: | ||
- 80p teste | - 80p teste | ||
- | - **Fiecare** test este verificat cu valgrind. Dacă un test are memory leaks, nu va fi punctat. | + | - Testele pentru operațiile elementare sunt verificate cu valgrind. Dacă un test are memory leaks, nu va fi punctat. |
- 20p README + comentarii/claritate cod (ATENȚIE! Fișierul README trebuie făcut explicit, cât să se înțeleagă ce ați făcut în sursă, dar fără comentarii inutile și detalii inutile). | - 20p README + comentarii/claritate cod (ATENȚIE! Fișierul README trebuie făcut explicit, cât să se înțeleagă ce ați făcut în sursă, dar fără comentarii inutile și detalii inutile). | ||
- Se acordă 20% din punctajul obținut pe teste, ca bonus pentru coding style. De exemplu, pentru o temă care obține maxim pe teste, se pot obține 20p bonus dacă nu aveți erori de coding style. Pentru o temă ce trece 18 teste din 20, se pot obține 18p dacă nu aveți erori de coding style. | - Se acordă 20% din punctajul obținut pe teste, ca bonus pentru coding style. De exemplu, pentru o temă care obține maxim pe teste, se pot obține 20p bonus dacă nu aveți erori de coding style. Pentru o temă ce trece 18 teste din 20, se pot obține 18p dacă nu aveți erori de coding style. | ||
Line 232: | Line 210: | ||
===FAQ=== | ===FAQ=== | ||
- | **Q:** Ce functii de hashing trebuie sa folosesc in tema, la cerintele II si III? \\ \\ | + | **Q:** Pot modifica structurile din schelet? \\ \\ |
- | **A:** Puteti folosi orice functii doriti. Un exemplu ar fi cele din laborator. | + | **A:** Nu. |
- | + | ||
- | **Q:** La cerinta II functia mea de hashing nu imi genereaza deloc coliziuni. E ok? \\ \\ | + | |
- | **A:** E in regula, insa codul care trateaza posibilitatea coliziunilor **trebuie sa existe**. | + | |
- | + | ||
- | **Q:** In enuntul cerintei III sunt mentionate functiile matematice log si pow, insa checkerul nu permite folosirea functiilor matematice. Cum rezolvam problema asta \\ \\ | + | |
- | **A:** Pentru a-l calcula pe **m** care e de forma $2^k$, puteti folosi shiftarea pe biti, adica <code>int m = 1 << k;</code> Din moment ce k se stabileste in prealabil, $\log_2{m} = k$. | + | |
- | ===Link-uri utile=== | + | **Q:** Îmi pot implementa alte funcții helper, pe lângă cele cerute? \\ \\ |
+ | **A:** Desigur, chiar vă încurajăm să o faceți. | ||
- | [[https://en.wikipedia.org/wiki/Count-distinct_problem]] | + | **Q:** Deci pentru Insert si Delete pot să dau copy-paste la codul din funcțiile Split si Concat?\\ \\ |
+ | **A:** Nu dați copy-paste, vă vom scădea din punctaj pentru cod duplicat. Pur și simplu chemați funcțiile deja implementate cu parametri corespunzători | ||
- | [[https://www.omnicoreagency.com/youtube-statistics]] | + | **Q:** Rope-urile trebuie echilibrate de către noi?\\ \\ |
+ | **A:** Nu. | ||
- | [[https://en.wikipedia.org/wiki/HyperLogLog]] | + | **Q:** Nu mă prind cum aș putea folosi funcția ''getTotalWeight'' din schelet. De ce ați inclus-o?\\ \\ |
+ | **A:** Sincer, am folosit-o în implementarea soluției și am uitat să o ștergem. Dacă nu vi se pare evident cum v-ați putea folosi de ea și mai mult vă încurcă, puteți să o ștergeți :) | ||
- | [[https://en.wikipedia.org/wiki/Linear_probing]] | + | **Q:** Am căutat informații despre Rope pe internet și am citit că greutatea unei frunze ar trebui să fie egală cu lungimea stringului pe care îl conține. Respectăm enunțul și o lăsăm 0? |
+ | **A:** Vom considera ambele variante drept corecte. | ||
- | [[https://stackoverflow.com/questions/12327004/how-does-the-hyperloglog-algorithm-work]] | + | **Q:** Aș vrea să folosesc un string auxiliar în care să îmi extrag tot șirul din Rope și să mă folosesc de el mai departe. E în regulă? |
+ | **A:** Nu este permisă extragerea stringului din rope urmată de prelucrarea acestui string. | ||
- | </hidden> |