Tema 3 - Biblioteca
Responsabili:
Deadline soft: 28 aprilie, ora 23:55
Depunctare întârziere după depășirea deadline-ului soft: -10p/zi
Deadline hard: 1 mai, ora 23:55
Changelist:
09 aprilie, ora: 09:24 actualizare checker (in conformitate cu constrangerile cerintei)
14 aprilie, ora: 19:45 actualizare checker (adaugare bonus)
Introducere
Fiind aproape de terminarea anului I de facultate, dornici sa ne afirmam si sa ne testam cunostintele acumulate pana acum, acceptam sa participam intr-un proiect de modernizare a unei biblioteci.
In prima faza a proiectului, cea de documentare si intelegere a cerintelor clientului, aflam de la directorul institutiei urmatoarele:
se doreste o metoda de indexare a fiecarei carti intr-un format electronic, astfel incat operatiile de gestiune a volumelor sa fie simple, rapide, moderne, sigure etc. (operatia PUT
)
majoritatea cautarilor in baza de date a bibliotecii nu se realizeaza dupa numele cartilor ci dupa continut astfel incat este nevoie de o metoda rapida de cautare, tinand cont de volumul mare de carti. Practic, date niste cuvinte cheie (ce se afla in continutul cartilor) trebuie sa se gaseasca numele cartilor ce le contin. (operatia GET
)
multi clienti organizeaza competitii unu-la-unu pe baza cartilor citite astfel: primul alege numele cartii, iar oponentul sau scrie o lista de cuvinte continute in acea carte. In cazul in care nu a gresit niciun cuvant (toate cuvintele mentionate se gasesc in carte), acesta acumuleaza un punctaj egal cu numarul de cuvinte scrise, si 0p altfel. Dupa un numar prestabilit de runde, se desemneaza castigatorul: persoana cu cele mai multe puncte acumulate! Deoarece partea de verificare a cuvintelor este foarte costisitoare manual, se doreste implementarea in aplicatia electronica si a acestei functionalitati. Formal, dat un nume de carte si o lista de cuvinte, sa se determine daca toate cuvintele se afla in acea carte. (operatia PLAY
)
Detalii implementare
Dându-se ca input o serie de comenzi, fiecare comanda specifica aplicatiei: PUT
, GET
sau PLAY
, se doreste generarea unui output cu raspunsurile corespunzatoare. Atat fomatele comenzilor cat si a raspunsurilor sunt detaliate in continuare.
Input-ul constă dintr-o insiruire de comenzi, fiecare pe cate o linie. Citirea se realizeaza pana la intalnirea caracterului de sfarsit de fisier EOF
.
comanda1
comanda2
...
Tipurile de comenzi (detaliate ulterior):
Comanda PUT
Sintaxa comenzii PUT este:
PUT titlu_carte cuv1 cuv2 .. cuvN
Rolul acestei comenzi este de a indexa conținutul cărții titlu_carte
si anume: cuv1 cuv2 .. cuvN
, astfel încât la orice căutare ulterioara de cuvinte apartinand cartii, aceasta să fie printre rezultatele găsite.
Această comandă se poate reprezenta în două moduri:
În tema voastră, va trebui să implementați cea de-a doua tehnică de reținere a informațiilor, cea în care într-un hashtable se rețin asocieri de tipul cuvânt - listă de carti.
Astfel dacă se dau la input următoarele 3 comenzi:
PUT titlu_carte1 cand rasare soarele
PUT titlu_carte2 afara este soarele
PUT titlu_carte3 cand mergem afara
Hashtable-ul după aceste 3 comenzi va arăta în felul următor:
afara → {titlu_carte2, titlu_carte3}
cand → {titlu_carte1, titlu_carte3}
este → {titlu_carte2}
mergem → {titlu_carte3}
rasare → {titlu_carte1}
soarele → {titlu_carte1, titlu_carte2}
Observații comandă PUT:
Două comenzi PUT pentru același titlu de carte se consideră adăugări de conținut și nu ștergere sau înlocuire de conținut.
Un cuvânt poate apărea de mai multe ori în cadrul unei cărți, insa nu e nevoie sa retineti de cate ori apare.
Comanda GET
Sintaxa comenzii GET este:
GET cuv1_interogare cuv2_interogare .. cuvN_interogare
Această comandă va întoarce toate cărțile în care apar toți termenii interogării. De exemplu, pentru următoarele interogări:
GET cand
Rezultatul va fi {titlu_carte1, titlu_carte3}
Cuvântul „cand” apare în cartea 1 si in cartea 3.
Cele două cărți sunt afișate în ordine alfabetică.
GET cand rasare
Rezultatul va fi {titlu_carte1, titlu_carte3} SI {titlu_carte1} = {titlu_carte1}
SI - intersectie de mulțimi
GET cenusareasa
Observații comandă GET:
Numele cărților întoarse vor fi afișate în ordine alfabetică
Dacă nu există nicio carte care să conțină toți termenii interogării, atunci se va afișa BOOK_NOT_FOUND
Intrucât va trebui să afisați toate cărțile care conțin toți termenii interogării, va trebui să găsiți o metodă de intersecție rapidă (eficienta).
Comanda PLAY
Sintaxa comenzii PLAY este:
PLAY titlu_carte cuv1_concurent cuv2_concurent .. cuvN_concurent
Această comandă verifică dacă toți termenii din interogarea: cuv1_concurent cuv2_concurent .. cuvN_concurent
există în cartea titlu_carte
. Dacă toți termenii există în carte, atunci se va afișa YOU_WIN, iar dacă nu, se va afișa YOU_LOSE.
Exemplu:
INPUT:
PLAY titlu_carte1 soarele cand
PLAY titlu_carte3 mergem bal
OUTPUT:
YOU_WIN
YOU_LOSE
Delimitatorul între cuvintele din conținutul cărților sau din interogare este doar caracterul spațiu.
Aveti la dispozitie un schelet de cod care să realizeze parsarea unei comenzi intr-un vector de string-uri.
Fiecare comandă se află pe o linie.
Nu există un număr maxim de comenzi și nici nu există un număr maxim de caractere pe o linie.
Se garantează ca numele cartilor sau al comenzilor de interogare nu va contine spatii.
Comenzile se termina cu caracterul terminator de linie(\n
), care se garantează ca nu apare în conținutul cărților sau în interogări.
Output-ul comenzilor GET și PLAY este influențat doar de comenzile PUT de până atunci. Așadar nu va trebui să rețineți comenzile date, ci doar să le procesați imediat.
Output-ul comenzilor GET și PLAY trebuie scris la stdout, în ordinea citirii comenzilor GET și PLAY din input.
Comanda PUT nu are output. Ca urmare, in urma unei comenzi PUT voi trebuie doar sa actualizati corespunzator structura de date, fara sa afisati nimic.
Observații generale timpi
Fiecare test are asociat un timeout, calculat in functie de dimensiunea lui. Este interzisa folosirea oricaror optimizari de compilator (exemplu: -O2
, -O3
, etc.) pentru incadrarea in timpul limita.
Acesti timpi sunt specifici rularii temei pe vmchecker si nu rularii temei local, unde este posibil sa obtineti timpi diferiti (mai buni). Timpul de rulare/test considerat va fi numai cel de pe vmchecker.
Hashtable
Pentru implementarea eventualelor structuri de date de tip hashtable din temă, puteți pleca de la implementarea din Laboratorul 6, bazată pe vector de liste înlănțuite.
Evident, elementele din dictionar sunt de tipul (cheie, valoare). In cazul temei, valoare va fi reprezentat sub forma unui vector std::vector<std::string>
in care veti mentine lista cartilor in care se gaseste cuvantul din cheie. Pentru a optimiza cautarea in acest vector va trebui sa mentineti acest vector sortat lexicografic si sa optimizati cautarea in vector folosind un algoritm specific acestei proprietati (de sortare).
Pentru a putea efectua tema, trebuie să țineți cont de următoarele observații:
Operația de GET dintr-un hashtable trebuie să poată fi efectuată în timp constant și să nu fie influențată de numărul de intrări în hashtable. Din acest motiv, dacă implementați hashtable-ul ca un vector, așa cum este în laboratorul de hash-uri, trebuie să tineți cont de raportul între câte intrări sunt în hashtable și capacitatea lui.
Dacă acest raport, numit factor de încărcare, depășeste o valoare predefinită (se recomandă ca această valoare să fie 0.7), atunci va trebui să redimensionați vectorul și bineînțeles va trebui să introduceți vechile intrări în noul vector.
Pentru vectorul inițial din hashtable, puteți considera o capacitate predefinită de intrări, de exemplu 16, urmând ca la fiecare redimensionare să dublați capacitatea vectorului.
Va trebui să gasiți o metodă prin care două liste de cărți se intersectează foarte rapid.
Întrucât se lucrează doar cu string-uri puteți folosi funcția de hash din laboratorul 6.
In cadrul temei este obligatorie setarea dimensiunii initiale a dictionarului la valoarea 16
si setarea factorului de incarcare la valoarea 0.7
, pentru ca toate implementarile voastre sa faca acelasi numar de redimensionari.
Următorul exemplu vă va ajuta să înțelegeți cum se redimensionează un hashtable:
Înainte:
După redimensionare:
Explicații:
Cele două poze indică același hashtable, odată înainte de redimensionare, având capacitatea de 10 și apoi după redimensionare, având capacitatea de 20.
În cele două poze sunt arătate doar pozițiile din vector care conțin o listă nevidă de elemente.
În poze, numele țărilor indică cheia din hashtable, iar valorile (listele de carti care conțin aceste cuvinte) nu au fost afișate
Între paranteze, pentru fiecare țară este arătat valoarea hash-ului calculat cu funcția din laborator (hash(“romania”)==1443285676)
Poziția din vector în care s-a inserat o cheie este indicele hash(cuvânt)%capacitate
În prima situație, în hashtable sunt deja 7 elemente (70% din capacitatea hashtable-ului este ocupată) și se inserează o nouă cheie (“romania”).
În acest caz, întrucât factorul de încărcare depășeste 70%, trebuie ca hashtable-ul să fie redimensionat, iar cheile remapate la noii indecși.
Astfel cheia “america” a fost mutată în elementul 15 din vector, întrucât hash(“america”)%20==15 samd.
Cheia “romania” a fost inserată pe poziția 16.
Schelet de Cod
Aveti la dispoziție un schelet de cod, care realizează citirea și parsarea comenzilor. Acesta va scuteste de efortul de a implementa partea nefunctionla a temei.
Scheletul de cod se gaseste aici: 3-biblioteca_schelet
Nu este obligatorie folosirea acestui schelet de cod.
Exemple
Input | Output |
PUT titlu_carte1 cand rasare soarele | |
PUT titlu_carte3 cand mergem | |
GET mergem | titlu_carte3 |
PLAY titlu_carte1 cand rasare | YOU_WIN |
PLAY titlu_carte1 ana are | YOU_LOSE |
PLAY titlu_carte3 mergem | YOU_WIN |
PUT titlu_carte3 afara soarele | |
GET cand soarele | titlu_carte1 titlu_carte3 |
PUT titlu_carte2 afara este soarele | |
GET afara | titlu_carte2 titlu_carte3 |
GET afara | titlu_carte2 titlu_carte3 |
GET cand mergem | titlu_carte3 |
GET temaSD | BOOK_NOT_FOUND |
Așa cum observați, pentru claritate răspunsul comenzilor GET/PLAY a fost pus pe rândul corespunzător din tabel, lăsând câte o linie goală pentru fiecare comandă PUT, dar voi NU trebuie să lăsați nicio linie goală. Pentru mai multe detalii, consultați checkerul disponibil.
Așa cum am amintit și în secțiunea corespunzătoare comenzii GET, cărțile sunt afișate în ordine alfabetică.
Bonus
Bucurosi ca am finalizat cu succes cea de-a doua faza a proiectului, anume implementarea corecta a bibliotecii virtuale, dorim sa aflam cum functioneaza aplicatia, “in mediul real”. Contactandu-l pe director, acesta ne spune ca lucrurile stau mult mai bine acum: gestiunea cartilor este mult usurata, clientii s-au obisnuit cu folosirea aplicatiei si a crescut numarul de competitii datorita simplificarii verificarilor. Insa, exista si o mica problema recenta, neprevazuta la inceputul proiectului: din cauza numarului mare de carti, si a numarului mare de interogari: GET/PLAY, aplicatia este destul de lenta. Deoarece momentan nu exista fonduri suficiente pentru achizitionarea unor echipamente hardware dedicate si nici de upgrade-uri la cele existente, acesta ne propune o prima pentru optimizarea (software) a aplicatiei.
Cerinta bonus
Optimizati aplicatia din punct de vedere a timpului de rulare, pastrand implemetarea si algoritmul folosit in tema, astfel incat sa respecte timeout-urile noi (setate in checker, la sectiunea bonus).
Pentru indeplinirea cerintei, este obligatoriu sa pastrati algoritmul si structura descrisa in cerinta temei. Scopul bonusului este scrierea corecta si eficienta a codului. Cateva sfaturi in acest sens ar fi:
std::vector<T> a, b;
//fill a with many elements
b = a; //realizeaza o copiere (duplicare a tuturor elementelor din a)
eliminati flag-ul '-g' din Makefile. Acest flag realizeaza imbogatirea executabilului cu informatii de debugging, foarte utile in acest sens, dar care aduc un impact negativ asupra performantei.
in general, lucrati ingrijit: modularizati codul, considerati mebrii claselor private si folositi metode pentru interactiunea cu clasele, organizati clasele pe diverse nivele logice/de abstractizare. Intr-adevar, asta nu va creste performata, dar va va face sa vedeti mai usor eventuale bug-uri in implementare si actiuni care se puteau realiza mai eficient.
pentru clasa vector: in momentul in care folosim structura vector implementata in C++, este bine sa folositi functia reserve(size_type n)
, ce aloca in prealabil n
elemente, atunci cand capacitatea finala (sau minima) a vectorului este cunoscuta. De obicei se foloseste impreuna cu functia push_back(T& el). Un exemplu de folosire este:
std::vector<int> intVect;
for (int i = 0; i < 10000; ++i)
intVect.push_back(i);
// In acest caz, in vector se aloca un element nou, pentru fiecare apel push_back.
// Apelul de alocare este unul costisitor.
// Chiar mai ineficient este cazul cand, din lipsa posibilitatii alocarii
// unui element nou in continuarea elementelor vectorului, se copiaza toate elementele
// intr-un spatiu continuu suficient de mare. Acest proces poate sa apara, in cel mai
// defavorabil caz, la fiecare alocare.
// Solutie:
std::vector<int> intVect;
intVect.reserve(10000);
for (int i = 0; i < 10000; ++i)
intVect.push_back(i);
// In acest caz se va face o alocare de 10000 elemente o singura data (la inceput)
// dupa care nu va mai fi nevoie de nicio realocare.
Trimitere și punctare
Temele vor trebui trimise pe vmchecker.
Atenție! Temele trebuie trimise în secțiunea Structuri de Date (CA).
Arhiva trebuie să conțină:
Punctare:
90 puncte obținute pe testele principale de pe vmchecker
Test1: Test SIMPLE GET
Test2: Test SIMPLE GET/PLAY
Test3: Test MEDIUM GET/PLAY
Test4: Test MEDIUM GET/PLAY
Test5: Test HARD GET/PLAY
Test6: Test HARD GET/PLAY
Test7: Test HARD GET/PLAY
Test8: Test VERY HARD GET/PLAY
Test9: Test Valgrind, verifică dacă eliberați toată memoria
10 puncte: coding style și README
10 puncte obținute pe testele bonus de pe vmchecker
2p: Test5, cu un alt TIMEOUT
2p: Test6, cu un alt TIMEOUT
2p: Test7, cu un alt TIMEOUT
2p: Test8, cu un alt TIMEOUT
2p: Test9, cu un alt TIMEOUT
TOTAL: 110 puncte
Timpul de rulare/test: Există un timp de rulare specific pentru fiecare test, astfel încât să fie verificat că implementați bine operațiile hashtable si ca respectati optimizarile precizate in cerinta. Acesti timpi sunt specifici rularii temei pe vmchecker si nu rularii temei local, unde este posibil sa obtineti timpi diferiti (mai buni). Timpul de rulare/test considerat va fi numai cel de pe vmchecker.
Pentru alte detalii legate de punctare citiți Regulamentul General de Trimitere a Temelor.
Checker
FAQ
Q: Se poate folosi STL?
R: Puteți folosi clasele std::vector, std::list și std::string pentru a reține cuvintele, titlurile cartilor etc. De asemenea, in general este recomandat sa folositi iteratorii nativi structurii, precum std::vector<T>::iterator
in C++, acestia fiind mai eficienti in parcurgerea structurilor (iterabile). Vedeti exemple de utilizare in scheletul de cod.