Racket: Arbori de sufixe

  • Data publicării: 03.03.2024
  • Data ultimei modificări: 21.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$”
    • observație: alfabetul primit va fi parcurs în ordine (când funcția va fi invocată din funcția 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ă)
    • 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)

Etapa 3

În această etapă veți implementa câteva aplicații ale arborilor de sufixe:

  • căutarea unui subșir într-un text
  • identificarea celui mai lung subșir comun a două texte
  • căutarea unui subșir de lungime dată care se repetă în text

Scopul principal al etapei este lucrul cu expresii de legare statică a variabilelor:

  • let sau let* - pentru evitarea calculelor duplicate
  • named let sau letrec - pentru implementarea ad-hoc a proceselor recursive, fără a apela la funcții ajutătoare

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? primește un text și un șablon și întoarce true dacă șablonul apare în text, respectiv false dacă nu apare
  • funcția este corespondenta funcției st-has-pattern?, diferența fiind că st-has-pattern? lucrează pe arbori de sufixe, iar substring? lucrează pe texte
  • ex: (substring? (string->list "banana") (string->list "bananas"))#f, pentru că șablonul “bananas” nu apare în textul “banana”
(longest-common-substring text1 text2)
  • longest-common-substring primește două texte text1 și text2 și calculează cel mai lung subșir comun al acestora, astfel:
    • construiește arborele de sufixe ST1 pentru text1
    • determină toate sufixele S ale textului text2 (fără marcajul de final $, pentru ca acesta să nu fie numărat drept caracter comun celor 2 texte)
    • pentru fiecare sufix din S (de la cel mai lung la cel mai scurt), caută cea mai lungă potrivire a acestuia cu textul text1 (parcurgând căile relevante în ST1); cea mai lungă asemenea potrivire este rezultatul final
    • dacă există mai multe cele mai lungi subșiruri comune, funcția îl întoarce pe cel care a fost găsit primul urmând pașii de mai sus
  • ex: (longest-common-substring (string->list "babcxabac") (string->list "babxabxaaxxaba"))'(#\x #\a #\b #\a)
    • pentru sufixul “babxabxaaxxaba”, cea mai lungă potrivire a fost “bab”
    • pentru sufixul “abxabxaaxxaba”, cea mai lungă potrivire a fost “ab”
    • pentru sufixul “bxabxaaxxaba”, cea mai lungă potrivire a fost “b”
    • pentru sufixul “xabxaaxxaba”, cea mai lungă potrivire a fost “xab”
    • pentru sufixul “abxaaxxaba”, cea mai lungă potrivire a fost “ab”
    • pentru sufixul “xaba”, cea mai lungă potrivire a fost “xaba”, care este și cea mai lungă potrivire per total
  • ex: (longest-common-substring (string->list "banana") (string->list "bandana"))'(#\b #\a #\n)
    • există 2 potriviri de lungime 3: “ban” și “ana”, dar este întoarsă prima, conform algoritmului
    • dacă sufixele pentru textul “bandana” ar fi inclus marcajul $, am fi obținut rezultatul greșit '(#\a #\n #\a #\$)
(repeated-substring-of-given-length text len)
  • 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

Etapa 4

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

  • arborii devin fluxuri de ramuri
  • ramurile rămân perechi etichetă-subarbore
  • listele de sufixe/cuvinte devin fluxuri de sufixe/cuvinte
  • sufixele/cuvintele/etichetele/textele rămân liste de caractere

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:

  • aveți mai multă independență și responsabilitate în ceea ce privește designul programului
    • vă veți defini singuri constructorii și operatorii utili pentru tipul Collection (fără să se spună în schelet fiecare funcție de care aveți nevoie)
    • nu vom descrie din nou fiecare funcție care trebuie “ajustată”, întrucât comportamentul este același cu cel anterior, diferă eventual doar modul de reprezentare a datelor; voi înșivă trebuie să acționați conform specificației de mai sus și să înțelegeți când parametrii/rezultatul unei funcții au devenit, din liste, fluxuri
    • sunteți responsabili pentru eficiența implementărilor, astfel încât, la rularea testelor, să vă încadrați în limita de timp impusă pe vmchecker; jumătate din teste folosesc un text lung, pentru care o implementare corectă cu fluxuri ar trebui să fie mult mai rapidă decât implementarea anterioară cu liste
  • fără o înțelegere foarte bună a datelor cu care lucrați, nu veți ști când trebuie utilizate liste și funcții pe liste și când trebuie utilizate fluxuri și funcții pe fluxuri

Scopul etapei este consolidarea cunoștințelor legate de:

  • fluxuri
  • abstractizare (pe de o parte veți observa efectul respectării sau nerespectării barierei de abstractizare în etapele anterioare, pe de altă parte veți face un re-design care să abstractizeze și mai mult soluția, care va deveni astfel mai flexibilă)

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

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

Baremul depunctărilor posibile în etapa 4 este:

  • -10p: anumite funcții din fișierul etapa4.rkt sunt scrise să ruleze pe fluxuri, nu pe colecții (adică folosesc operatori de tip stream-*, nu pe cei de tip collection-* implementați de voi, cu care s-ar putea jongla ușor între reprezentarea cu liste și cea cu fluxuri)
  • -6p*n: unde n = numărul de funcții dintre cele testate la exercițiul 1 (din checker) care, în loc să lucreze pe fluxuri, fac conversii între cele două tipuri de date pentru a lucra pe liste
  • -30p: suffixes->st, în loc să lucreze pe fluxuri, face conversii între cele două tipuri de date pentru a lucra pe liste

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

  • vom depuncta (total) soluțiile implementate cu liste care reușesc să ia punctaj (probabil parțial) pe vmchecker
  • vom oferi punctaj (parțial sau total) soluțiilor care sunt implementate corect cu fluxuri, dar care nu se încadrează în timp din cauza unor probleme de eficiență la alte niveluri
    • vom acorda punctajul total când problemele de eficiență sunt minore, soluția fiind totuși destul de rapidă (nu necesită mai mult decât dublul timpului cerut pe vmchecker)
    • vom acorda 50% din punctaj când problemele de eficiență sunt majore

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

  • 21.03 (ora 11:30) - Am adăugat o mică depunctare (10p) în etapa 4, pentru cazul în care funcțiile redefinite sunt redefinite exclusiv pentru fluxuri (folosind operatori de tip stream-*), nu pentru colecții (folosind operatori de tip collection-*), nerealizând astfel abstractizarea cerută în enunț (care ne permite să lucrăm alternativ cu liste sau cu fluxuri, efectuând modificări doar în interfața tipului Collection, nu și în restul funcțiilor).
  • 18.03 (ora 11:45) - Am publicat etapa 4.
  • 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.
  • 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.txt · Last modified: 2024/03/21 11:34 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