This is an old revision of the document!


Racket: Arbori de sufixe

  • Data publicării: 03.03.2024
  • Data ultimei modificări: 03.03.2024 (changelog)
  • Tema (o arhivă .zip cu toate fișierele .rkt folosite în etapa curentă) se va încărca pe vmchecker

Descriere generală și organizare

Tema constă în definirea și utilizarea arborilor de sufixe asociați unui text și este împărțită în 4 etape:

  • una pe care o veți rezolva după laboratorul 2 (cu deadline în ziua laboratorului 3, la ora 23:59)
  • una pe care o veți rezolva după laboratorul 3 (cu deadline în ziua laboratorului 4, la ora 23:59)
  • una pe care o veți rezolva după laboratorul 4 (cu deadline în ziua laboratorului 5, la ora 23:59)
  • una pe care o veți rezolva după laboratorul 5 (cu deadline în ziua laboratorului 6, la ora 23:59)

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.

Etapa 1

Î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:

  • nodul rădăcină are un număr de fii egal cu dimensiunea alfabetului textului T (de exemplu, pentru un text care folosește toate literele mici ale alfabetul englez, nodul rădăcină va avea 26 de fii)
  • muchia de la un nod tată la un nod fiu este etichetată cu prefixul comun al tuturor sufixelor stocate pe această cale
  • procedeul se repetă: fiecare nod va avea un număr de fii egal cu numărul de simboluri distincte cu care încep sufixele stocate în nod
  • în arborele final, fiecărui sufix al textului T îi corespunde o cale de la rădăcină la o frunză (iar sufixul se recompune prin concatenarea tuturor etichetelor de pe această cale)

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ă.

  • Când muchiile sunt etichetate cu cel mai lung prefix comun al sufixelor din subarborele respectiv, arborele de sufixe se numește compact (ca în figura de mai sus).
  • Când muchiile sunt etichetate cu câte un singur caracter (care este tot un prefix comun al tuturor sufixelor “de mai jos”, însă nu neapărat cel mai lung), arborele de sufixe se numește atomic (găsiți un exemplu în scheletul de cod).

Reprezentare în Racket

  • Vom reprezenta un arbore de sufixe ca pe o listă de ramuri, unde fiecare ramură corespunde unui fiu al rădăcinii. Pe exemplul de mai sus, textul “BANANA” generează o listă de 4 ramuri (una cu eticheta “$”, una cu eticheta “A”, una cu eticheta “BANANA$” și una cu eticheta “NA”).
  • Prin ramură, vom înțelege o pereche între o etichetă și subarborele cu rădăcina în nodul de sub etichetă. Pe exemplul din figură, cea de-a doua ramură (încadrată cu verde) are eticheta “A” și subarborele cu rădăcina portocalie și restul nodurilor galbene.
  • Pentru a folosi funcțiile de lucru pe liste, fiecare etichetă va fi reprezentată nu ca string, ci ca listă de caractere. Astfel, eticheta “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:

  • liste și operatorii acestora (datorită modului de reprezentare a tipului ST și a etichetelor, precum și a funcțiilor care vă solicită să întoarceți ca rezultat liste cu un format specific)
  • perechi și operatorii acestora (pentru că fiecare ramură este o pereche între o etichetă și un subarbore)
  • funcții recursive pe stivă, respectiv pe coadă (observați tipul de recursivitate al fiecărei funcții implementate, și atenție la cazurile în care vi se solicită un anumit tip de implementare - chiar dacă obțineți punctaj pe checker, punctajul va fi anulat în cazul în care funcțiile nu sunt implementate conform cerințelor)
  • operatori condiționali, operatori logici și valori boolene

Î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 primește un arbore de sufixe (ST) și întoarce prima ramură a acestuia (o pereche etichetă-subarbore)
  • other-branches primește un ST și întoarce ST-ul fără prima sa ramură (o listă de ramuri, așa cum era și ST-ul original)
  • ex: (first-branch st-banana)'((#\$))
    • rezultatul este o pereche între eticheta “$” (reprezentată ca '(#\$) - listă de caracterul $) și subarborele vid (reprezentat ca o listă vidă)
    • când construim o pereche între un element E și o listă L, ceea ce se întâmplă este că obținem o listă cu E urmat de toate elementele din L, de aceea rezultatul final este o listă care conține doar eticheta '(#\$) (o listă care conține o altă listă)
  • ex: (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)
  • get-branch-label primește o ramură a unui ST și întoarce eticheta acesteia
  • get-branch-subtree primește o ramură a unui ST și întoarce subarborele de sub eticheta acesteia
  • ținând cont că o ramură este o pereche între o etichetă și un subarbore, cele două funcții nu fac decât să extragă cele două componente
  • ex: pentru branch definit ca '((#\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 primește un ST și un caracter ch și întoarce acea ramură a ST-ului a cărei etichetă începe cu caracterul ch, respectiv false în cazul în care nu există o asemenea ramură
  • ex: (get-ch-branch st-banana #\N)'((#\N #\A) ((#\$)) ((#\N #\A #\$)))
  • ex: (get-ch-branch st-banana #\Z)#f
(longest-common-prefix w1 w2)
  • longest-common-prefix primește două cuvinte (liste de caractere) w1 și w2 și întoarce o listă formată din trei elemente: cel mai lung prefix comun al lui w1 și w2, restul lui w1 după eliminarea acestui prefix, restul lui w2 după eliminarea acestui prefix
  • în cazul în care cele două cuvinte nu au un prefix comun, cel mai lung prefix comun este lista vidă
  • ex: (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 primește o listă nevidă de cuvinte care încep cu același caracter și întoarce cel mai lung prefix comun al tuturor cuvintelor din listă
  • având în vedere că toate cuvintele încep cu același caracter, rezultatul va fi mereu o listă nevidă de caractere
  • ex: (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 se folosește pentru a căuta un șablon (un subșir) într-un text, folosind ST-ul asociat textului
  • funcția primește un ST și un șablon (listă nevidă de caractere) și procedează astfel:
    • caută ramura din ST care s-ar putea potrivi cu șablonul (ramura care începe cu același caracter cu care începe șablonul)
    • dacă găsește o asemenea ramură, există 3 posibilități (iar formatul rezultatului diferă în funcție de cazul în care ne aflăm):
      1. dacă șablonul este conținut integral în etichetă, înseamnă că el este conținut în text, și atunci întoarcem true
      2. dacă eticheta este conținută integral în șablon, înseamnă că este în continuare posibil să găsim șablonul în text, și pentru asta va trebui să cercetăm subarborele de sub etichetă; în acest caz, întoarcem lista (etichetă, șablon nou, subarbore) care ne oferă informațiile despre ce s-a potrivit până acum (eticheta) și ce subșir (noul șablon) ne-a rămas de căutat în subarbore pentru a putea determina dacă șablonul inițial apărea în text (întoarcem și subarborele pentru a ști unde să continuăm căutarea)
      3. dacă șablonul și eticheta au un prefix comun dar nu se potrivesc până la final, putem conchide că șablonul nu apare în text, și atunci întoarcem lista (false, cel mai lung prefix comun între etichetă și șablon); prefixul comun nu ne folosește la căutare, dar ne folosește în rezolvarea altor aplicații din etapele următoare ale temei
    • dacă nu găsește o asemenea ramură, atunci șablonul nu apare în text și întoarcem un rezultat de tipul celui de la cazul 3 anterior: lista (false, lista vidă) - pentru că practic cel mai lung prefix comun al șablonului cu orice etichetă este lista vidă
  • ex: (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? primește un ST și un șablon și întoarce true dacă șablonul apare în ST, respectiv false dacă nu apare
  • ex: (st-has-pattern? st-banana (string->list "ANAN"))#t, pentru că șirul “ANAN” apare în textul “BANANA”
  • acest lucru se determină astfel:
    • eticheta “A” este conținută în șablonul “ANAN”, așadar se va căuta noul șablon “NAN” în subarborele de sub eticheta “A”
    • eticheta “NA” este conținută în șablonul “NAN”, așadar se va căuta noul șablon “N” în subarborele de sub eticheta “NA”
    • șablonul “N” este conținut în eticheta “NA$”, așadar șablonul apare în text (și întoarcem true)

Depunctări generate de nerespectarea cerințelor din enunț

În enunțul anumitor exerciții apar restricții. Nerespectarea acestora duce la depunctări conform următorului barem:

  • -10p: get-ch-branch nu manipulează arborele doar prin intermediul operatorilor tipului ST
  • -20p: longest-common-prefix folosește recursivitate pe stivă
  • -0p: longest-common-prefix-of-list nu oprește parcurgerea când se cunoaște deja prefixul final (nu se depunctează, dar este de dorit să eficientizați programul în acest sens)

Etapa 2

În această etapă veți implementa cei mai importanți constructori pentru tipul ST:

  • funcția text->ast care primește un text și calculează arborele de sufixe atomic asociat textului
  • funcția text->cst care primește un text și calculează arborele de sufixe compact asociat textului

Algoritmul de construcție, descris și în scheletul de cod, este următorul:

  1. se determină și se sortează alfabetul folosit de text
  2. se determină toate sufixele textului
  3. pentru fiecare simbol din alfabetul sortat:
    • determină lista S a tuturor sufixelor care încep cu acest simbol
    • determină eticheta ramurii care va începe cu acest simbol:
      • pentru un AST, eticheta este chiar simbolul (ca listă de caractere, pentru uniformitate)
      • pentru un CST, eticheta este cel mai lung prefix comun al sufixelor din S
    • calculează fiecare ramură din arbore, ca pereche între:
      • eticheta determinată anterior
      • subarborele construit recursiv (pe baza sufixelor rămase în S după ce am eliminat prefixul comun reprezentat de etichetă)

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:

  • funcționale (anumite sarcini impun lucrul cu funcționale în locul utilizării recursivității explicite)
  • funcții anonime (deși aveți libertate cu privire la utilizarea lor, vă recomandăm să dați funcții anonime ca parametri pentru funcționale atunci când funcțiile respective nu mai sunt necesare altundeva)
  • funcții curry și uncurry (veți folosi mecanismul de curry-ing pentru a deriva “cu ușurință” cei doi constructori dintr-o funcție mai generală)

Î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)
  • get-suffixes primește un text (o listă de caractere la finalul căreia s-a adăugat caracterul special $) și întoarce toate sufixele textului (în ordine descrescătoare a lungimii)
  • fiecare sufix din rezultat va conține marcajul de final (caracterul special $)
  • ex: (get-suffixes '(#\w #\h #\y #\$))'((#\w #\h #\y #\$) (#\h #\y #\$) (#\y #\$) (#\$))
(get-ch-words words ch)
  • get-ch-words primește o listă de cuvinte words și un caracter ch și întoarce acele cuvinte din words care încep cu caracterul ch
  • ex: (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)
  • ast-func și cst-func reprezintă funcții de etichetare a unui ST
  • o funcție de etichetare primește o listă de sufixe care încep cu același caracter - și prin urmare se vor găsi pe aceeași ramură - și calculează o pereche între eticheta pe care o va avea ramura, respectiv noile sufixe din care se va construi subarborele de sub etichetă
  • noile sufixe se obțin din vechile sufixe prin înlăturarea prefixului reprezentat de etichetă
  • pentru ast-func, eticheta va fi o listă care conține un singur caracter (cel cu care încep toate sufixele)
  • pentru cst-func, eticheta va fi cel mai lung prefix comun al sufixelor din lista suffixes
  • ex: (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”
  • ex: (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”
  • observație: rezultatul apare ca o listă care conține laolaltă eticheta și noile sufixe, întrucât noile sufixe sunt o listă, iar la crearea perechii între etichetă și noile sufixe, constructorul de perechi 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:
    • AST-ul asociat textului, dacă apelăm suffixes->st cu funcția de etichetare ast-func
    • CST-ul asociat textului, dacă apelăm suffixes->st cu funcția de etichetare cst-func
  • practic, funcția descrie pasul 3 al algoritmului enunțat la începutul etapei
  • ex: (suffixes->st cst-func (get-suffixes (string->list "banana$")) (string->list "$abn"))
    • primul argument este 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$”
    • pentru caracterul “$”, 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
    • pentru caracterul “a”, algoritmul va selecta sufixele “anana$”, “ana$”, “a$”
    • acestea vor fi grupate într-o ramură de etichetă “a” (cel mai lung prefix comun al lor), iar noile sufixe din care va trebui să construim subarborele sunt “nana$”, “na$”, “$”
    • la rândul lor, noile sufixe încep ori cu caracterul “$”, ori cu caracterul “n”
    • rezultă că subarborele de sub eticheta “a” este compus din două ramuri:
      • una (terminală) de etichetă “$”
      • una de etichetă “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.
    • rezultatul final este: '(((#\$)) ((#\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 textului
  • text->cst primește un text și calculează arborele de sufixe compact asociat textului
  • cele două funcții trebuie obținute prin aplicația parțială a funcției mai generale text->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
  • ex: (text->cst "banana") va genera același rezultat cu exemplul anterior pentru funcția suffixes->st

Depunctări generate de nerespectarea cerințelor din enunț

Baremul depunctărilor posibile în etapa 2 este:

  • -10p: get-suffixes nu este recursivă pe stivă
  • -10p*n: unde n = numărul de funcții dintre get-ch-words, ast-func, cst-func rezolvate fără a folosi funcționale în locul recursivității explicite
  • -10p*n: unde n = numărul de pași din funcția suffixes→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ționale
  • -10p: text→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)

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

Changelog

  • 08.03 (ora 16:30) - Am publicat etapa 2.
  • 03.03 (ora 20:45) - Am publicat etapa 1.

Referinţe

pp/24/teme/racket-st.1709909018.txt.gz · Last modified: 2024/03/08 16:43 by mihaela.balint
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0