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 $
, pentru ca acesta să nu fie numărat drept caracter comun celor 2 texte)text1
(parcurgând căile relevante în ST1); cea mai lungă asemenea potrivire este rezultatul final(longest-common-substring (string->list "babcxabac") (string->list "babxabxaaxxaba"))
⇒ '(#\x #\a #\b #\a)
“babxabxaaxxaba”
, cea mai lungă potrivire a fost “bab”
“abxabxaaxxaba”
, cea mai lungă potrivire a fost “ab”
“bxabxaaxxaba”
, cea mai lungă potrivire a fost “b”
“xabxaaxxaba”
, cea mai lungă potrivire a fost “xab”
“abxaaxxaba”
, cea mai lungă potrivire a fost “ab”
“xaba”
, cea mai lungă potrivire a fost “xaba”
, care este și cea mai lungă potrivire per total(longest-common-substring (string->list "banana") (string->list "bandana"))
⇒ '(#\b #\a #\n)
“ban”
și “ana”
, dar este întoarsă prima, conform algoritmului“bandana”
ar fi inclus marcajul $
, am fi obținut rezultatul greșit '(#\a #\n #\a #\$)
(repeated-substring-of-given-length text len)
len
și caută un subșir de lungime len
care se repetă în textlen
, atunci întoarce primele len
caractere ale acestui prefix (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 alfabeticBaremul depunctărilor posibile în etapa 3 este:
În etapa 3 ați putut observa că anumite aplicații (căutarea unui subșir în text, căutarea unui subșir de o anumită lungime care se repetă în text) nu necesită decât explorarea parțială a arborelui de sufixe asociat textului, prin urmare ar fi mai eficient ca acest arbore să fie construit “leneș”, pe măsură ce diverse porțiuni din el sunt necesare.
În acest scop, în etapa 4, veți modifica implementarea tipului ST astfel încât fiecare arbore (și, la rândul lor, toți subarborii aferenți) să fie nu o listă de ramuri, ci un flux de ramuri. Dacă aceasta ar fi singura modificare, singurele funcții care ar trebui redefinite ar fi constructorii și operatorii tipului ST (presupunând că ați respectat bariera de abstractizare a acestui tip).
Întrucât arborii sunt construiți pe baza tuturor sufixelor unui text, veți modifica de asemenea reprezentarea tuturor sufixelor unui text - ele vor fi reținute într-un flux, nu într-o listă cum s-a întâmplat până acum. Pentru că multe funcții din etapele anterioare primeau ca argumente sau întorceau liste de sufixe, va trebui să redefiniți, de asemenea, toate aceste funcții. Așa cum este explicat în scheletul de cod, această redefinire “în masă” putea fi evitată dacă am fi lucrat de la început cu conceptul de “colecție de sufixe”, în loc să presupunem că acestea vor fi grupate neapărat într-o listă. Una din cerințele etapei este să realizați acum acest re-design (implementând un nou tip de date Collection), și să observați cum el vă ajută să jonglați cu ușurință între reprezentările alternative pentru colecțiile de sufixe.
Este important să distingeți între colecțiile care devin fluxuri și cele care își păstrează vechea reprezentare:
Dintr-un anumit punct de vedere este o etapă ușoară, întrucât nu necesită implementarea unor funcții noi, ci doar ajustarea celor vechi (plus definirea tipului Collection). Dificultatea etapei constă în următoarele aspecte:
Scopul etapei este consolidarea cunoștințelor legate de:
În realitate, implementarea cu fluxuri ne ajută doar atunci când evităm astfel explorarea unei porțiuni semnificative din arbore. Pentru aplicații precum găsirea celui mai lung subșir comun a două texte, care necesită căutarea în întreg arborele, este mai eficientă reprezentarea cu liste, pentru că operațiile pe liste sunt mai rapide decât cele pe fluxuri. În săptămânile următoare, veți vedea că în limbajul Haskell nu avem dificultatea acestei alegeri, întrucât în acest limbaj toate listele sunt, de fapt, fluxuri. Sperăm că tema noastră v-a ajutat să înțelegeți mai bine anumite concepte din programare și v-a trezit interesul pentru ce va urma.
Baremul depunctărilor posibile în etapa 4 este:
În afară de aceste depunctări, nota va fi, în principiu, cea obținută pe vmchecker. Întrucât nu putem anticipa cât de bine va discerne timeout-ul de pe vmchecker între soluțiile implementate conform specificației și celelalte, ne rezervăm dreptul să efectuăm ajustări manuale în ambele sensuri:
suffixes->st
: funcția nu își sortează alfabetul; ramurile arborelui generat vor fi ordonate conform ordinii caracterelor din alfabetul primit ca argument.