Responsabili:
Data publicării : 26 martie, ora: 21:00
Deadline: 17 aprilie, ora 23:55
În urma realizării acestei teme:
Problema estimarii cardinalitatii (a numararii elementelor distincte) este, in esenta, gasirea numarului de elemente unice dintr-o colectie de elemente care se pot repeta.
Conceptual, ne referim la:
INPUT:
1, 34, 2, 2, 2, 3
OUTPUT:
Pentru cerintele I si II:
1 - 1
2 - 3
3 - 1
34 - 1
Pentru cerinta III:
Exista 4 elemente distincte
La intrare se dau numere intre 0 si 2000000. Gasiti numarul de aparitii ale fiecarui element, utilizand un vector de frecventa.
Un vector de frecventa este un vector care are pe pozitia i numarul de aparitii ale elementului i.
Se garanteaza ca numarul de aparitii ale oricarui element este mai mic decat 256.
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.
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.
Evident, daca si aceasta pozitie este ocupata, vom cauta prima pozitie libera in continuare.
Daca se va ajunge la finalul listei de bucketuri, se va continua de la inceput.
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.
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.
Se garanteaza ca lungimea maxima a oricarui sir este maxim 100 de caractere.
Se garanteaza ca numarul de aparitii ale oricarui element este mai mic decat 256.
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.
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.
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.
Cum putem totusi sa ne indeplinim obiectivul?
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.)
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.
In practica, acest lucru nu este un dezavantaj prea mare (intrucat rareori e relevanta diferenta dintre 1m vizualizari si 1.1m vizualizari).
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.
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).
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.
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:
- am generat cel putin 8 numere
- am avut noroc si a trebuit sa generam mai putin de 8 numere
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.
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.
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.
Daca vrem sa imbunatatim performanta algoritmului nostru va trebui sa:
- Atenuam efectul negativ al generarii rapide unui numar cu multi biti de 0 initiali
- Oferim estimari mai granulare decat puterile lui 2
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.
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.
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.
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:
Ca exemplu, pentru bucketul evidentiat cu verde, j = 6, iar M[j] = 2
Avand aceasta medie Z, raspunsul final E (numarul de elemente distincte intalnite) va fi dat de urmatorea formula:
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:
Pentru ultima cerinta, citirea se va face dintr-un fisier al carui nume este primit ca parametru.
Temele vor fi trimise pe vmchecker. Atenție! Temele trebuie trimise în secțiunea Structuri de Date (CA).
Arhiva trebuie să conțină:
De aceea, vă sfătuim să nu vă lăsați rezolvări ale temelor pe calculatoare partajate (la laborator etc), pe mail/liste de discuții/grupuri etc.
Q: Ce functii de hashing trebuie sa folosesc in tema, la cerintele II si III?
A: Puteti folosi orice functii doriti. Un exemplu ar fi cele din laborator.
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
int m = 1 << k;
Din moment ce k se stabileste in prealabil, $\log_2{m} = k$.