This is an old revision of the document!
Tema constă în definirea și utilizarea arborilor de sufixe asociați unui text și este împărțită în 4 etape:
Așa cum se poate observa, ziua deadline-ului variază în funcție de semigrupa în care sunteți repartizați. Restanțierii care refac tema și nu refac laboratorul beneficiază de ultimul deadline (deci vor avea deadline-uri în zilele de 20.03, 27.03, 03.04, 10.04).
Rezolvările tuturor etapelor pot fi trimise până în ziua laboratorului 6, dar orice exercițiu trimis după deadline (și până în ziua laboratorului 6) se punctează cu jumătate din punctaj. Orice exercițiu trimis după ziua laboratorului 6 nu se mai punctează deloc. Nota finală pe etapă se calculează conform formulei: n = (n1 + n2) / 2 (n1 = nota obținută înainte de deadline; n2 = nota obținută după deadline). Când toate submisiile sunt înainte de deadline, nota pe ultima submisie este și nota finală (întrucât n1 = n2).
În fiecare etapă, veți folosi ce ați învățat în săptămâna anterioară pentru a dezvolta aplicația.
Pentru fiecare etapă veți primi un schelet de cod (dar rezolvarea se bazează în mare măsură pe rezolvările anterioare). Enunțul din această pagină este menit să descrie arborii de sufixe (pe care îi vom numi ST, conform prescurtării în limba engleză) și să vină cu exemple de rulare a funcțiilor din schelet. Dacă preferați, puteți rezolva tema utilizând doar indicațiile din schelet.
În prima etapă vă veți familiariza cu structura și reprezentarea arborilor de sufixe (ST) în Racket, veți implementa o mini-bibliotecă pentru tipul ST (în fișierul suffix-tree.rkt), apoi veți implementa câteva funcții utile construcției și manipulării arborilor de sufixe (în fișierul etapa1.rkt).
Un arbore de sufixe este un arbore care stochează toate sufixele unui text T, astfel:
Convenim să terminăm fiecare sufix prin caracterul special $
, asigurându-ne astfel că fiecărui sufix îi va corespunde o frunză în arbore. Pentru textul “BANANA”, arborele de sufixe arată astfel:
Observăm că, fără convenția de a utiliza terminația $
, sufixului “ANA”
i-ar fi corespuns un nod intern în arborele de sufixe, nu o frunză.
“BANANA”
generează o listă de 4 ramuri (una cu eticheta “$”
, una cu eticheta “A”
, una cu eticheta “BANANA$”
și una cu eticheta “NA”
).“A”
și subarborele cu rădăcina portocalie și restul nodurilor galbene.“BANANA$”
va fi de fapt reținută sub forma listei '(#\B #\A #\N #\A #\N #\A #\$)
(acesta este modul de reprezentare a caracterelor în Racket).Rezultă că reprezentarea completă în Racket a arborelui din figură este:
'( ((#\$)) ; prima ramură, de etichetă "$", corespunzătoare sufixului vid ((#\A) ; a doua ramură, de etichetă "A" ((#\$)) ; ramura 2.1, terminală, corespunzătoare sufixului "A" ((#\N #\A) ; ramura 2.2, de etichetă "NA" ((#\$)) ; ramura corespunzătoare sufixului "ANA" ((#\N #\A #\$)))) ; ramura corespunzătoare sufixului "ANANA" ((#\B #\A #\N #\A #\N #\A #\$)) ; a treia ramură, de etichetă "BANANA$" ((#\N #\A) ; a patra ramură, de etichetă "NA" ((#\$)) ; ramura corespunzătoare sufixului "NA" ((#\N #\A #\$))) ) ; ramura corespunzătoare sufixului "NANA"
Acești arbori vor fi definiți în checker, însă este necesar să le înțelegeți structura pentru a putea implementa funcțiile din cerință.
În etapa 1, veți exersa lucrul cu:
În următoarele exemple, considerăm că reprezentarea arborelui de sufixe pentru textul “BANANA”
este reținută în variabila st-banana
.
Funcțiile principale pe care le veți implementa sunt:
(first-branch st) (other-branches st)
(first-branch st-banana)
⇒ '((#\$))
“$”
(reprezentată ca '(#\$)
- listă de caracterul $
) și subarborele vid (reprezentat ca o listă vidă)'(#\$)
(o listă care conține o altă listă)(other-branches st-banana)
⇒ '(((#\A) ((#\$)) ((#\N #\A) ((#\$)) ((#\N #\A #\$)))) ((#\B #\A #\N #\A #\N #\A #\$)) ((#\N #\A) ((#\$)) ((#\N #\A #\$))))
, adică o listă cu cele 3 ramuri în afară de prima (unde fiecare ramură este o pereche între un element (eticheta) și o listă (subarborele))(get-branch-label branch) (get-branch-subtree branch)
'((#\A) ((#\$)) ((#\N #\A) ((#\$)) ((#\N #\A #\$))))
:(get-branch-label branch)
⇒ '(#\A)
(get-branch-subtree branch)
⇒ '(((#\$)) ((#\N #\A) ((#\$)) ((#\N #\A #\$))))
(get-ch-branch st ch)
(get-ch-branch st-banana #\N)
⇒ '((#\N #\A) ((#\$)) ((#\N #\A #\$)))
(get-ch-branch st-banana #\Z)
⇒ #f
(longest-common-prefix w1 w2)
(longest-common-prefix '(#\w #\h #\y) '(#\w #\h #\e #\n))
⇒ '((#\w #\h) (#\y) (#\e #\n))
(longest-common-prefix-of-list words)
(longest-common-prefix-of-list (list (string->list "when") (string->list "where") (string->list "why") (string->list "who")))
⇒ '(#\w #\h)
(match-pattern-with-label st pattern)
(match-pattern-with-label st-banana (string->list "BABA"))
⇒ '(#f (#\B #\A))
, pentru că șablonul “BABA”
s-a potrivit doar parțial cu eticheta “BANANA$”
, deci ne încadrăm în cazul 3, iar cel mai lung prefix comun dintre șablon și etichetă este “BA”
(st-has-pattern? st pattern)
(st-has-pattern? st-banana (string->list "ANAN"))
⇒ #t
, pentru că șirul “ANAN”
apare în textul “BANANA”
“A”
este conținută în șablonul “ANAN”
, așadar se va căuta noul șablon “NAN”
în subarborele de sub eticheta “A”
“NA”
este conținută în șablonul “NAN”
, așadar se va căuta noul șablon “N”
în subarborele de sub eticheta “NA”
“N”
este conținut în eticheta “NA$”
, așadar șablonul apare în text (și întoarcem true)În enunțul anumitor exerciții apar restricții. Nerespectarea acestora duce la depunctări conform următorului barem:
În această etapă veți implementa cei mai importanți constructori pentru tipul ST:
text->ast
care primește un text și calculează arborele de sufixe atomic asociat textuluitext->cst
care primește un text și calculează arborele de sufixe compact asociat textuluiAlgoritmul de construcție, descris și în scheletul de cod, este următorul:
Exercițiile valorifică faptul că, în programarea funcțională, funcțiile sunt valori de ordinul întâi. Scopul etapei este consolidarea cunoștințelor legate de:
În toate funcțiile de mai jos, când folosim noțiuni precum text, cuvânt sau sufix, ne referim la entități reprezentate ca liste de caractere. Funcțiile pe care va trebui să le implementați sunt:
(get-suffixes text)
$
) și întoarce toate sufixele textului (în ordine descrescătoare a lungimii)$
)(get-suffixes '(#\w #\h #\y #\$))
⇒ '((#\w #\h #\y #\$) (#\h #\y #\$) (#\y #\$) (#\$))
(get-ch-words words ch)
words
și un caracter ch
și întoarce acele cuvinte din words
care încep cu caracterul ch
(get-ch-words '((#\M #\a #\r #\y) (#\h #\a #\s) (#\a) (#\l #\i #\t #\t #\l #\e) (#\l #\a #\m #\b)) #\l)
⇒ '((#\l #\i #\t #\t #\l #\e) (#\l #\a #\m #\b))
(ast-func suffixes) (cst-func suffixes)
suffixes
(ast-func '((#\w #\h #\e #\n) (#\w #\h #\e #\r #\e) (#\w #\h #\y) (#\w #\h #\o)))
⇒ '((#\w) (#\h #\e #\n) (#\h #\e #\r #\e) (#\h #\y) (#\h #\o))
, adică eticheta este doar “w”
, iar noile sufixe sunt “hen”
, “here”
, “hy”
, “ho”
(cst-func '((#\w #\h #\e #\n) (#\w #\h #\e #\r #\e) (#\w #\h #\y) (#\w #\h #\o)))
⇒ '((#\w #\h) (#\e #\n) (#\e #\r #\e) (#\y) (#\o))
, adică eticheta este “wh”
(cel mai lung prefix comun), iar noile sufixe sunt “en”
, “ere”
, “y”
, “o”
cons
întâlnește un element și o listă și acționează precum constructorul cons
pentru liste(suffixes->st labeling-func suffixes alphabet)
suffixes->st
primește o funcție de etichetare (precum ast-func
sau cst-func
), o listă de sufixe (toate sufixele unui text) și un alfabet (alfabetul folosit de text) și întoarce:suffixes->st
cu funcția de etichetare ast-func
suffixes->st
cu funcția de etichetare cst-func
(suffixes->st cst-func (get-suffixes (string->list "banana$")) (string->list "$abn"))
cst-func
, al doilea argument sunt toate sufixele textului “banana$”
, iar al treilea argument este un alfabet care include caracterele “a”
, “b”
, “n”
și “$”
folosite de textul “banana$”
text->st
, acest alfabet va fi întotdeauna sortat; altfel, dacă suffixes->st
primește un alfabet nesortat, se va construi pur și simplu un arbore de sufixe cu altă ordonare a ramurilor decât cea alfabetică)“$”
, algoritmul va selecta sufixul “$”
, care generează o ramură de etichetă “$”
sub care se găsește o frunză în arbore, întrucât după eliminarea prefixului “$”
reprezentat de etichetă rămânem doar cu un nou sufix vid“a”
, algoritmul va selecta sufixele “anana$”
, “ana$”
, “a$”
“a”
(cel mai lung prefix comun al lor), iar noile sufixe din care va trebui să construim subarborele sunt “nana$”
, “na$”
, “$”
“$”
, ori cu caracterul “n”
“a”
este compus din două ramuri:“$”
“na”
(prefixul comun al sufixelor “nana$”
și “na$”
) - al cărei subarbore se alcătuiește din noile sufixe “na$”
și “$”
, rămase după ce am eliminat eticheta “na”
din “nana$”
, respectiv “na$”
… etc.
'(((#\$))
((#\a) ((#\$))
((#\n #\a) ((#\$))
((#\n #\a #\$))))
((#\b #\a #\n #\a #\n #\a #\$))
((#\n #\a) ((#\$))
((#\n #\a #\$))))
(text->ast text) (text->cst text)
text->ast
primește un text și calculează arborele de sufixe atomic asociat textuluitext->cst
primește un text și calculează arborele de sufixe compact asociat textuluitext->st
; aceasta din urmă trebuie să fie o funcție curry proiectată de către voi (de aceea nu i-am specificat signatura), care efectuează pașii 1 (determinare alfabet sortat) și 2 (determinare sufixe la care se adaugă marcajul “$”
) ai algoritmului, apoi predă controlul funcției suffixes->st
care efectuează pasul 3(text->cst "banana")
va genera același rezultat cu exemplul anterior pentru funcția suffixes->st
Baremul depunctărilor posibile în etapa 2 este:
get-ch-words
, ast-func
, cst-func
rezolvate fără a folosi funcționale în locul recursivității explicitesuffixes->st
rezolvați fără a folosi funcționale în locul recursivității explicite (reamintim că funcția însăși poate fi recursivă explicit, pentru a realiza pasul de construcție a subarborilor, însă pentru alte prelucrări (ca gruparea pe ramuri, determinarea etichetelor, etc.) trebuie folosite funcționaletext->ast
și text->cst
nu sunt obținute cu “efort minim” din text->st
(-5p dacă text->st
nu e curry, -5p dacă derivarea nu se face scriind minimul posibil)În această etapă veți implementa câteva aplicații ale arborilor de sufixe:
Scopul principal al etapei este lucrul cu expresii de legare statică a variabilelor:
Etapa testează de asemenea faptul că implementările voastre respectă bunele practici în ceea ce privește abstractizarea - funcția repeated-substring-of-given-length
cere să manipulați arborii de sufixe doar prin interfața definită în fișierul suffix-tree.rkt. În etapa 4 veți modifica implementarea arborilor de sufixe și efectul respectării (sau nerespectării) barierei de abstractizare se va reflecta în necesitatea de a modifica sau nu implementarea funcției repeated-substring-of-given-length
.
Vă reamintim că programarea funcțională este o programare de tip wishful thinking: veți descrie rezolvarea în termeni conceptuali, și veți implementa ulterior conceptele (subproblemele) care nu există deja în program (sub forma funcțiilor ajutătoare care rezolvă aceste subprobleme).
Funcțiile pe care va trebui să le implementați sunt:
(substring? text pattern)
(substring? (string->list "banana") (string->list "bananas"))
⇒ #f
, pentru că șablonul “bananas”
nu apare în textul “banana”
(longest-common-substring text1 text2)
text1
și text2
și calculează cel mai lung subșir comun al acestora, astfel:text1
text2
(fără marcajul de final , am fi obținut rezultatul greșit
'(#\a #\n #\a #\$)
<file>
(repeated-substring-of-given-length text len)
</file>
* repeated-substring-of-given-length primește un text și un număr natural
len și caută un subșir de lungime
len care se repetă în text
* funcția întoarce ori primul asemenea subșir găsit, ori false dacă nu există soluție
* algoritmul de căutare este următorul:
* pe fiecare ramură a arborelui de sufixe compact (atenție, acest algoritm nu funcționează pentru arborele de sufixe atomic!):
* caută un nod intern (un nod care are fii, ceea ce înseamnă că drumul până la acel nod este un prefix comun pentru două sau mai multe sufixe distincte, adică un subșir care se repetă în text)
* dacă nodul intern găsit reprezintă un prefix de lungime mai mare sau egală cu
len, atunci întoarce primele
len caractere ale acestui prefix
* altfel, continuă căutarea
* funcția presupune că ramurile arborelui sunt sortate alfabetic (conform metodei de construcție din etapa anterioară), așadar rezultatul va fi mereu prima soluție din punct de vedere alfabetic
* ex:
(repeated-substring-of-given-length (string->list "xabxabxaaxbbxabxabxaaxbb") 10) ⇒
'((#\a #\b #\x #\a #\b #\x #\a #\a #\x #\b)
, pentru că, deși mai există și soluțiile
“xabxabxaax” și
“bxabxaaxbb”,
“abxabxaaxb” este prima din punct de vedere alfabetic
==== Depunctări generate de nerespectarea cerințelor din enunț ====
Baremul depunctărilor posibile în etapa 3 este:
* -20p: longest-common-substring nu folosește named let pentru parcurgerea sufixelor
* -10p: repeated-substring-of-given-length nu manipulează arborele doar prin intermediul operatorilor tipului ST
* -20p: aplicări repetate ale acelorași funcții pe aceleași argumente (în loc de a evalua expresiile o singură dată și a prelua rezultatele în variabile cu ajutorul let-urilor) - nu se va aplica nicio depunctare pentru o scăpare izolată, dar se va aplica depunctarea în întregime pentru “scăpări” multiple
===== Precizări =====
* Scheletul fiecărei etape va conține unul sau mai multe fișiere .rkt în care trebuie să lucrați, plus fișierul checker.rkt pe care îl veți folosi doar pentru testare (rulând codul fără să îl modificați).
* Fiecare etapă (o arhivă .zip cu fișierele în care ați lucrat, plus eventualele fișiere care sunt solicitate de acestea cu “require”) se va încărca pe vmchecker. Testele de vmchecker sunt aceleași cu cele din checker.rkt.
* În fiecare etapă veți avea de implementat o serie de funcții, în sprijinul cărora vă puteți defini oricând funcții ajutătoare (dacă nu se interzice asta în mod explicit). Atunci când există restricții asupra implementării funcției din cerință, aceleași restricții trebuie respectate și de eventualele funcții ajutătoare definite de voi.
* Dacă doriți să rezolvați exerciții din etapa curentă care depind de exerciții din etapele anterioare pe care nu le-ați rezolvat, puteți semnala acest lucru titularului de curs, care vă va pune la dispoziție o rezolvare pentru acele exerciții astfel încât să puteți continua tema. Odată ce alegeți această variantă, renunțați la dreptul de a mai trimite cu întârziere etapa pentru care ați solicitat parțial sau integral rezolvarea. Puteți solicita rezolvări doar pentru exercițiile din etapele anterioare, nu și pentru cele din etapa curentă. Dacă implementați un exercițiu din etapa curentă pe baza unui alt exercițiu din etapa curentă pe care nu l-ați rezolvat, se va lua în calcul punctajul dat de checker, chiar dacă implementarea ar funcționa în caz că exercițiul nerezolvat ar funcționa.
* Tema este o temă de programare funcțională - pentru care folosim Racket. Racket este un limbaj multiparadigmă, care conține și elemente “ne-funcționale” (de exemplu proceduri cu efecte laterale), pe care nu este permis să le folosiți în rezolvare.
* Pentru fiecare etapă, checker-ul vă oferă un punctaj între 0 și 120 de puncte. Pentru a obține cele 1.33p din nota finală cu care este creditată tema de Racket, este suficient să acumulați 400 de puncte de-a lungul celor 4 etape. Un punctaj între 400 și 480 se transformă într-un bonus proporțional.
* Veți prezenta tema asistentului, care poate modifica punctajul dat de checker dacă observă nereguli precum răspunsuri hardcodate, proceduri cu efecte laterale, implementări neconforme cu restricțiile din enunț.
===== Resurse =====
* etapa 1
* etapa 2
* etapa 3
===== Changelog =====
* 14.03 (ora 21:15) - Am publicat etapa 3.
* 14.03 (ora 15:05) - Am adăugat o observație în etapa 2, la descrierea funcției
suffixes->st'': funcția nu își sortează alfabetul; ramurile arborelui generat vor fi ordonate conform ordinii caracterelor din alfabetul primit ca argument.