Table of Contents

Haskell: Graph Zoo

Obiective

Descriere generală și organizare

Tema urmărește familiarizarea cu două modalități de reprezentare a grafurilor orientate, una standard (prin mulțimi de noduri și de arce) și alta constructivă (algebrică), adecvată unei abordări funcționale. Veți avea ocazia să implementați diverse operații de manipulare a grafurilor sub ambele reprezentări, astfel încât să puteți compara punctele lor tari și slabe.

Tema este împărțită în 3 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 02.05, 09.05, 16.05).

Rezolvările tuturor etapelor pot fi trimise până în ziua laboratorului 10 (deadline hard pentru toate etapele). Orice exercițiu trimis după un deadline soft se punctează cu jumătate din punctaj. Cu alte cuvinte, 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ă există un schelet de cod (dar rezolvarea se bazează în mare măsură pe rezolvările anterioare). Enunțul caută să ofere o imagine de ansamblu atât la nivel conceptual, cât și în privința aspectelor care se doresc implementate, în timp ce detaliile se găsesc direct în schelet.

Etapa 1

În această etapă:

Construcțiile și mecanismele de limbaj pe care le veți exploata în rezolvare sunt:

În schelet veți găsi două module:

Găsiți detalii despre funcționalitate și implementare, precum și exemple, direct în codul sursă. Veți avea de completat definițiile care încep cu *** TODO ***.

Pentru reprezentarea mulțimilor, veți folosi tipul predefinit Set a, similar tipului listă [a]. Având în vedere că există funcții pe mulțimi cu același nume ca cele pe liste (de exemplu, map, filter), pentru evitarea conflictului de nume, abordarea standard, adoptată și în temă, este de a importa etichetat modulul necesar (import qualified Data.Set as S), urmând ca toate tipurile și funcțiile din acest modul să fie utilizate cu numele prefixat: S.Set a, S.map, S.filter etc.

ATENȚIE! Toate funcțiile din această etapă, cu excepția search, vor fi implementate FĂRĂ recursivitate explicită. Nerespectarea acestei cerințe va conduce la o depunctare de 10p/100 per funcție.

Este suficient ca arhiva pentru vmchecker să conțină modulele StandardGraph și Algorithms.

Etapa 2

În această etapă:

Funcțiile din modulul Algorithms ar trebui să funcționeze neschimbate în etapa 2, cu toate că accentul nu mai cade pe ele acum.

Construcțiile și mecanismele noi de limbaj pe care le veți exploata în rezolvare, pe lângă cele din etapa 1, sunt:

Ce putem înțelege prin reprezentare algebrică a grafurilor? Ori de cât ori auzim aceste termen, ne putem gândi la construirea unor obiecte mai complexe din altele mai simple, în baza unor operații de îmbinare. De exemplu, plecând de la numerele 0 și 1, și utilizând operațiile de adunare și înmulțire, putem genera toate numerele naturale.

Similar, putem elabora o abordare constructivă a grafurilor orientate, pornind de la următoarele mecanisme (vedeți articolul din secțiunea de Referințe dacă doriți să aprofundați subiectul):

Ideile de mai sus se pot traduce direct într-un tip de date utilizator, unde a este tipul etichetelor nodurilor:

data AlgebraicGraph a
    = Empty
    | Node a
    | Overlay (AlgebraicGraph a) (AlgebraicGraph a)
    | Connect (AlgebraicGraph a) (AlgebraicGraph a)

Mai jos, sunt exemplificate mai multe grafuri care utilizează această reprezentare, pentru o înțelegere mai bună:

Din exemplele de mai sus, transpar două avantaje importante ale acestei reprezentări algebrice, care îi lipsesc reprezentării standard din etapa 1:

În baza celui de-al doilea avantaj de mai sus, se poate pune problema compactării reprezentării unui graf, analizând relațiile pe care nodurile le au cu celelalte noduri. Spre exemplu, pentru unul dintre grafurile exemplificate mai sus, este dată și o reprezentare alternativă (a doua), mai lungă.

Ca fapt divers, echivalența celor două reprezentări de mai sus poate fi înțeleasă ca distributivitate a lui Connect față de Overlay.

Compactarea reprezentării grafului se bazează pe conceptul de descompunere modulară. Pe scurt, un modul este o mulțime de noduri, care toate au aceeași mulțime de out-neighbors și aceeași mulțime de in-neighbors dacă ne uităm doar în afara modulului, cu toate că cele două mulțimi pot fi diferite. Nodurile din interiorul unui modul pot fi conectate oricum. Se poate demonstra că, în cazul a două module disjuncte, dacă există un arc orientat între un nod din primul modul și un nod din al doilea, atunci există arce cu orientarea respectivă între toate nodurile din primul modul și toate nodurile din al doilea (exact ce exprimă Connect).

În graful de mai jos, observăm următoarele:

Orice graf are două descompuneri modulare banale:

dar acestea sunt neinteresante. Pe noi ne interesează descompunerile nebanale, dacă acestea există, care contribuie la compactarea reprezentării grafului. De exemplu, cea mai compactă reprezentare a grafului din imagine este:

Connect (Connect (Connect (Node 1) (Node 2))
                 (Overlay (Node 3) (Node 4)))
        (Node 5)

Veți implementa aspecte legate de descompunerea modulară parțial ca bonus, în cadrul etapelor 2 și 3. Deși există algoritmi eficienți (liniari) pentru determinarea descompunerii modulare, aceștia sunt destul de complicați, astfel că vom utiliza o abordare mai simplă bazată pe forță brută. Cu alte cuvinte, vom genera toate partițiile mulțimii de noduri, și apoi le vom filtra pentru a obține modulele.

În etapa 2, scheletul conține următoarele module:

Etapa 3

În această etapă:

Construcțiile și mecanismele noi de limbaj pe care le veți exploata în rezolvare, pe lângă cele din etapa 2, sunt:

Ca încălzire, amintiți-vă că ați operat până acum cu două reprezentări ale grafurilor, StandardGraph și AlgebraicGraph, și că ați implementat același set de funcții de acces și manipulare pentru amândouă (nodes, edges etc.). Gândiți-vă cum ați putea generaliza această interfață de lucru cu grafuri în Haskell:

Schițați răspunsul în comentarii în vederea prezentării (partea aceasta nu este testată automat).

În etapa 3, scheletul conține următoarele module:

Precizări

Resurse

Referințe

Changelog