Problema satisfiabilității formulelor booleene (SAT) este exemplul canonic de problemă NP-completă. Ea are aplicații importante în mai multe domenii, printre care proiectarea și verificarea circuitelor electronice, verificarea aderenței unui sistem la o specificație dată, criptanaliză, alocare de resurse și chiar genetică. Prin urmare, există multe eforturi de cercetare cu scopul identificării de abordări care să permită rezolvarea în timp rezonabil a instanțelor problemei SAT întâlnite în practică. Astfel, doi algoritmi notabili sunt Davis–Putnam–Logemann–Loveland (DPLL) și conflict-driven clause learning (CDCL). Deși în esență complexitatea algoritmilor moderni rămâne exponențială în cazul cel mai defavorabil, ei utilizează anumite principii care în practică permit reducerea substanțială a spațiului de căutare, înlesnind rezolvarea instanțelor cu sute de mii de variabile și milioane de constrângeri.
Inspirându-se din algoritmii menționați mai sus, tema propune implementarea unor mecanisme de rezolvare mai eficientă a SAT, rezultatul fiind un mic SAT solver. În final, acesta va fi utilizat pentru rezolvarea problemei 3-colorare a unui graf, cunoscută la rândul său ca fiind NP-completă.
Tema este împărțită în 3 etape:
Deadline-ul depinde de semigrupa în care sunteți repartizați. Restanțierii care refac tema și nu refac laboratorul beneficiază de ultimul deadline, și anume în zilele de 02.05, 09.05, respectiv 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ă la jumătate. 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 preced deadline-ul, nota pe ultima submisie constituie nota finală (întrucât n1 = n2).
În fiecare etapă, veți valorifica ce ați învățat în săptămâna anterioară și veți avea la dispoziție un schelet de cod, cu toate că vor exista trimiteri la etapele 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.
În cadrul SAT, operăm cu formule booleene în forma normală conjunctivă (FNC), care denotă conjuncții de disjuncții de literali booleeni. Mai precis, utilizăm următorii termeni:
x1
, x2
etc.x1
, ¬x2
etc.(x1 ∨ ¬x2 ∨ x3)
etc.(x1 ∨ ¬x2 ∨ x3) ∧ (¬x4 ∨ x5)
etc.{x1, ¬x2}
, care înseamnă că variabila x1
este asumată adevărată, iar x2
, falsă.În plus, utilizăm și termenii:
(x1)
etc.¬x1
este pur în raport cu formula (¬x1 ∨ ¬x2) ∧ (¬x1 ∨ x2)
, întrucât nu apare și x1
, dar x2
și ¬x2
nu sunt puri.O instanță a SAT, reprezentată de o formulă în FNC, urmărește determinarea satisfiabilității formulei, adică a existenței unei interpretări în raport cu care întreaga formulă este adevărată. Dacă o astfel de interpretare există, se dorește și identificarea acesteia. În esență, o formulă în FNC este satisfiabilă dacă putem identifica cel puțin un literal adevărat în fiecare clauză (proprietatea P). De exemplu:
(x1 ∨ ¬x2 ∨ x3) ∧ (¬x4 ∨ x5)
este satisfiabilă, existând mai multe interpretări care îndeplinesc proprietatea (P): {x1, ¬x4}
, {x1, ¬x2, x5}
etc. Observați că este posibil să nu fie necesară menționarea tuturor variabilelor în interpretare. Odată ce am asumat literalii x1
și ¬x4
adevărați, formula devine adevărată independent de valorile de adevăr ale celorlalte variabile.(x1 ∨ ¬x1 ∨ x2) ∧ (¬x3 ∨ x3)
este validă/tautologică, adică adevărată în toate interpretările, deci satisfiabilă.(x1) ∧ (¬x1)
este contradictorie, adică falsă în toate interpretările, deci nesatisfiabilă.Distingem și următoarele cazuri particulare:
Prima etapă a temei abordează reprezentările în Haskell ale conceptelor de mai sus, definirea unor funcții de bază care operează cu aceste reprezentări, și implementarea unui mecanism naiv de verificare a satisfiabilității bazat pe enumerarea interpretărilor.
Mai precis, vom utiliza următoarele reprezentări:
x1
devine 1
.x1
devine 1
, iar ¬x1
devine -1
.(x1 ∨ ¬x2 ∨ x3)
devine {1, -2, 3}
.(x1 ∨ ¬x2 ∨ x3) ∧ (¬x4 ∨ x5)
devine {{1, -2, 3}, {-4, 5}}
.{x1, ¬x2}
devine {1, -2}
.
Având în vedere că, în interiorul unei clauze, un literal apare cel mult o dată, iar ordinea literalilor este irelevantă, utilizăm pentru reprezentarea unei clauze mulțimi ordonate de literali în locul listelor. Pentru aceasta, folosim modulul Data.Set
(versiunea 0.6.7, corespunzătoare vmchecker), care definește tipul Set a
(mulțime ordonată cu elemente de tipul a
). Pe baza unui argument similar, formulele sunt reprezentate ca mulțimi ordonate de clauze. Având în vedere că multe dintre funcțiile pe mulțimi au același nume cu cele pe liste (map
, filter
, foldr
etc.), conflictele se evită plasând la începutul scheletului liniile:
import Data.Set (Set) import qualified Data.Set as Set
Semnificația este următoarea:
Set
poate fi utilizat ca atare.Set.
. De exemplu, Set.map
denotă funcționala pe mulțimi, în timp ce map
continuă să refere funcționala pe liste standard.Construcțiile și mecanismele de limbaj pe care le veți exploata în rezolvare sunt:
(x `f`)
sau (`f` y)
let
sau where
).
Modulul de interes din schelet este Formula
, care conține reprezentarea conceptelor de mai sus, precum și operațiile pe care trebuie să le implementați. Găsiți detaliile despre funcționalitate și despre constrângerile de implementare, precum și exemple, direct în fișier. Aveți de completat definițiile care încep cu *** TODO ***
.
Pentru rularea testelor, încărcați în interpretor modulul TestFormula
și evaluați main
.
Este suficient ca arhiva pentru vmchecker să conțină doar modulul Formula
.
Comentariile majorității funcțiilor din schelet conțin CONSTRÂNGERI
, a căror nerespectare atrage depunctări parțiale.
În plus, se impune operarea direct pe reprezentarea de mulțimi a clauzelor, formulelor și interpretărilor. Conversiile intermediare la liste și înapoi la mulțimi atrag depunctarea totală a funcțiilor implementate în acest fel! Funcția predefinită în schelet toLiteralLists
vă este oferită doar pentru a vizualiza mai ușor formulele; nu este gândită pentru a fi folosită efectiv în implementări.
Etapa 1 a presupus implementarea unui mecanism simplu de verificare a satisfiabilității, bazat pe enumerarea interpretărilor. Un dezavantaj al acestei abordări îl constituie generarea completă a unei interpretări, în sensul acoperirii tuturor variabilelor din formulă, cu toate că asumpțiile despre literalii adevărați făcute până la un moment dat pot invalida deja formula, indiferent de asumpțiile realizate ulterior pentru restul variabilelor. De exemplu, pentru formula (x1) ∧ (¬x1 ∨ x2)
, asumpția că literalul ¬x1
este adevărat (numită, pe scurt, asumpția ¬x1
) invalidează formula (mai precis, prima clauză), indiferent de asumpția făcută ulterior pentru variabila x2
.
Prin urmare, ar fi mai avantajos să observăm starea formulei după fiecare nouă asumpție. Astfel, odată ce asumăm un literal adevărat, variabila corespunzătoare poate fi eliminată din formulă, după următoarele principii:
De exemplu, pentru formula (x1 ∨ x2) ∧ (¬x1 ∨ x3)
și asumpția x1
, avem următoarele:
x1
, și anume (x1 ∨ x2)
, sunt eliminate, obținând formula (¬x1 ∨ x3)
.¬x1
, pot fi eliminate din clauzele rămase, obținând formula (x3)
.În urma unei secvențe de eliminări, se pot obține următoarele situații descrise în etapa 1:
De exemplu, în formula de mai sus:
x1
, care a condus la formula (x3)
, realizăm asumpția suplimentară x3
, obținem formula vidă, deci interpretarea {x1, x3}
satisface formula originală, indiferent de asumpția pentru variabila x2
.x3
, realizăm opusul său, ¬x3
, obținem o formulă cu clauza vidă, ()
, care nu mai poate fi satisfăcută.În cel de-al doilea caz, de conflict, trebuie să revenim prin backtracking la o asumpție anterioară și să încercăm opusul ei. Esența unei rezolvări mai eficiente o constituie strategia de alegere a următoarei asumpții, atât în prezent, când este eliminată o nouă variabilă, cât și în trecut, când este necesară revenirea la o variabilă eliminată deja.
Pentru a înțelege mai bine situațiile întâlnite la realizarea unei asumpții în prezent, să luăm formula de la începutul enunțului acestei etape: (x1) ∧ (¬x1 ∨ x2)
. Dacă alegem la întâmplare următoarea asumpție, de exemplu, ¬x1
, riscăm să desfășurăm un proces de calcul inutil, întrucât clauza (x1)
ar deveni imediat falsă. În schimb, la o analiză mai atentă, observăm că există un singur mod de a satisface o clauză unitară, și deci asumpția x1
este impusă. Prin urmare, este o idee bună să detectăm mai întâi prezența clauzelor unitare, și să le satisfacem pe acestea.
De remarcat că satisfacerea unei clauze unitare poate genera noi clauze unitare, care nu aveau inițial această proprietate. De exemplu, în formula anterioară, asumpția x1
conduce prin eliminare la formula (x2)
, în care apare clauza unitară (x2)
, absentă din formula originală. Prin urmare, prelucrarea clauzelor unitare trebuie repetată până la absența acestora din formulă.
O altă situație interesantă vizează literalii puri. Din moment ce complementul unui literal nu apare în formulă, este întotdeauna avantajos să asumăm acel literal, fără teama inducerii vreunui conflict. La fel ca la clauzele unitare, eliminarea unui literal pur poate genera noi literali puri, deci prelucrarea trebuie repetată. De exemplu, în formula (x1 ∨ x2) ∧ (¬x2 ∨ x3) ∧ (¬x2 ∨ ¬x3)
, nu există clauze unitare, dar există un singur literal pur, x1
. Prin eliminarea lui, se obține formula (¬x2 ∨ x3) ∧ (¬x2 ∨ ¬x3)
, în care apare literalul pur ¬x2
, care nu era pur în formula originală. Eliminându-l și pe acesta, se obține formula vidă.
De remarcat că cele două prelucrări de mai sus pot interacționa: satisfacerea unei clauze unitare poate introduce nu numai alte clauze unitare, ci și literali puri. În cazul în care sunt disponibile ambele opțiuni, se preferă satisfacerea clauzelor unitare mai întâi, întrucât ele pot conduce la conflicte, și este de dorit evidențierea cât mai timpurie a acestora, pentru a scuti alt efort de calcul care oricum nu ar împiedica generarea unui conflict. Numai dacă formula nu conține nici clauze unitare, și nici literali puri, are rost să recurgem la asumpții oarecare.
În exemplul final, demonstrăm toate cele trei tipuri de prelucrări pe formula (¬x1 ∨ x4) ∧ (x1 ∨ x2 ∨ ¬x3) ∧ (¬x2 ∨ x3 ∨ ¬x4)
:
¬x4
, care conduce prin eliminare la formula (¬x1) ∧ (x1 ∨ x2 ∨ ¬x3)
.(¬x1)
, și prioritizăm satisfacerea acesteia, cu toate că există și literalii puri x2
și ¬x3
. Eliminarea lui ¬x1
conduce la formula (x2 ∨ ¬x3)
.¬x3
și obținem formula vidă, care corespunde satisfacerii formulei originale sub interpretarea {¬x1, ¬x3, ¬x4}
.Conflictele (clauzele vide) și alegerile mai eficiente în caz de revenire la asumpțiile anterioare vor fi abordate în etapa 3.
Algoritmul final de rezolvare utilizează atât varianta curentă a formulei, care surprinde eliminările realizate până la un moment dat, cât și varianta originală. Prin urmare, simpla reprezentare a unei formule ca o mulțime de clauze, ca în etapa 1, devine insuficientă pentru modelarea întregii informații necesare. Din fericire, o mică extensie acoperă noua nevoie: în locul unei mulțimi, utilizăm un tablou asociativ (Data.Map
, consultați acest tutorial), în care cheile sunt clauzele curente, iar valorile, clauzele originale. Fiecărei clauze curente (chei) îi corespunde clauza originală (valoarea) din care a fost obținută prin eliminări.
De exemplu, formula (x1 ∨ x2) ∧ (¬x1 ∨ x3)
este reprezentată inițial, înainte de orice eliminare, prin tabloul {({1, 2}, {1, 2}), ({-1, 3}, {-1, 3})}
, unde am folosit notația (clauză-curentă, clauză-originală)
pentru a desemna o corespondență cheie-valoare. Evident, înainte de vreo eliminare, cheile și valorile coincid. Eliminând literalul 1
, așa cum a fost demonstrat mai sus, se obține noul tablou, {({3}, {-1, 3})}
, în care clauza originală {1, 2}
a fost înlăturată, și în care varianta curentă a clauzei originale {-1, 3}
este {3}
.
A doua etapă a temei abordează aceste reprezentări extinse ale formulelor, mecanismul de eliminare a literalilor, și prelucrarea clauzelor unitare și a literalilor puri.
Similar cu etapa 1, funcțiile pe tablouri trebuie prefixate cu Map.
. Remarcăm, totuși, că funcționalele cu nume standard pe tablouri (Map.map
, Map.filter
etc.) operează pe valori. Veți avea nevoie mai degrabă de funcționalele care iau în calcul și cheile (Map.mapKeys
, Map.filterWithKey
, Map.foldrWithKey
etc.), întrucât acestea sunt supuse transformărilor, în timp ce valorile rămân neschimbate.
Construcțiile și mecanismele de limbaj pe care le veți exploata în rezolvare, pe lângă cele din etapa 1, sunt:
data
)
Modulul de interes din schelet este ExtendedFormula
, care conține reprezentarea extinsă a formulelor, precum și operațiile pe care trebuie să le implementați. Găsiți detaliile despre funcționalitate și despre constrângerile de implementare, precum și exemple, direct în fișier. Aveți de completat definițiile care încep cu *** TODO ***
.
Pentru rularea testelor, încărcați în interpretor modulul TestExtendedFormula
și evaluați main
.
Este suficient ca arhiva pentru vmchecker să conțină modulele ExtendedFormula
și Formula
din etapa 1.
Comentariile majorității funcțiilor din schelet conțin CONSTRÂNGERI
, a căror nerespectare atrage depunctări parțiale.
În plus, se impune operarea direct pe reprezentările de mulțimi sau tablouri ale clauzelor, formulelor și interpretărilor. Conversiile intermediare la liste și înapoi la mulțimi sau tablouri atrag depunctarea totală a funcțiilor implementate în acest fel!
Etapa 2 a abordat situațiile în care o formulă poate fi satisfăcută mergând numai „înainte”, alegând în fiecare pas unul dintre cele trei tipuri de acțiuni de eliminare, în această ordine, a clauzelor unitare, a literalilor puri și a literalilor oarecare. Rămâne de stabilit cum trebuie procedat când, la un moment dat, eliminarea unui literal produce o clauză vidă (conflict).
De exemplu, plecând de la formula (¬x7 ∨ x1) ∧ (¬x6 ∨ ¬x2 ∨ x1) ∧ (¬x5 ∨ x1) ∧ (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x2 ∨ ¬x1 ∨ x6) ∧ (¬x2 ∨ x5 ∨ x7) ∧ (¬x1 ∨ x2) ∧ (x1 ∨ x2)
, se aplică mai întâi următoarea secvență de acțiuni, conform celor discutate în etapa 2:
¬x7
, care produce formula: (¬x6 ∨ ¬x2 ∨ x1) ∧ (¬x5 ∨ x1) ∧ (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x2 ∨ ¬x1 ∨ x6) ∧ (¬x2 ∨ x5) ∧ (¬x1 ∨ x2) ∧ (x1 ∨ x2)
.¬x6
produce formula: (¬x5 ∨ x1) ∧ (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x2 ∨ ¬x1) ∧ (¬x2 ∨ x5) ∧ (¬x1 ∨ x2) ∧ (x1 ∨ x2)
.¬x5
produce formula: (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x2 ∨ ¬x1) ∧ (¬x2) ∧ (¬x1 ∨ x2) ∧ (x1 ∨ x2)
.(¬x2)
, care conduce prin eliminare la formula: (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x1) ∧ (x1)
.(¬x1)
sau (x1)
ar fi eliminate, s-ar obține o clauză vidă (conflict).
Ce ar trebui făcut în acest caz? Intuitiv, ar trebui revenit prin backtracking la o asumpție anterioară și explorat opusul ei. Dar acest lucru ridică o altă întrebare: la ce punct decizional ar trebui să ne întoarcem? Evident, clauzele unitare impun alegerea, și nu ar avea sens să încercăm opusul; de exemplu, revenirea la asumpția ¬x2
(pasul 4 de mai sus), dictată de clauza unitară (¬x2)
, și explorarea variantei x2
ar fi inutile. Prin urmare, ar trebui să revenim la cel mai recent punct decizional în care există o alternativă viabilă; de exemplu, am putea reveni la asumpția ¬x5
(pasul 3), arbitrară, și explora asumpția x5
. Din păcate, și această cale ar conduce la conflict, și ar trebui revenit un nivel și mai sus, la asumpția ¬x6
(pasul 2) etc.
Se ridică următoarea întrebare interesantă: am putea oare reveni într-un punct mai precis din trecut, astfel încât să evităm încercarea măcar a câtorva alternative care în final ar conduce tot la conflict? În exemplul de mai sus, atât secvența inițială ¬x7 - ¬x6 - ¬x5
, cât și secvența alternativă ¬x7 - ¬x6 - x5
au condus la conflicte. Puteam să o evităm cu totul pe a doua?
La baza răspunsului stă observația că un conflict poate fi indus de fapt timpuriu, printr-o anumită combinație de asumpții incompatibile realizate în trecut, dar manifestat târziu. În exemplul de mai sus, combinația de asumpții incompatibile este {¬x7, ¬x5}
, care conduce la clauza unitară (¬x2)
, provenită din clauza originală (¬x2 ∨ x5 ∨ x7)
. Eliminarea acesteia conduce pe ambele căi de mai sus la conflicte. Prin urmare, ar trebui revenit într-un punct cât mai distant din trecut în care incompatibilitatea să fie înlăturată, astfel încât toate asumpțiile făcute ulterior asupra altor variabile să se armonizeze cu modificarea realizată. Acest lucru nu previne complet apariția altor conflicte pe măsură ce se realizează noi asumpții, dar reduce spațiul de căutare.
Remarcăm că, din moment ce clauzele unitare impun literalii eliminați, ca urmare a asumpțiilor anterioare, ei nu vor apărea în combinația conflictuală („nu aveam de ales decât să-i eliminăm”). Ne interesează să identificăm literalii pentru care asumpția opusă era viabilă.
Evident că rămân întrebările:
Pentru a răspunde la întrebarea (1), introducem mai întâi conceptul de rezoluție a două clauze, care va fi prezentat formal în capitolul de programare logică al cursului. Pentru temă, este suficient să o definim astfel: dacă o clauză C1
conține literalul xi
, iar o clauză C2
, complementul său, ¬xi
(sau viceversa), cele două clauze pot fi rezolvate în raport cu literalii respectivi, obținând o nouă clauză, R
(rezolvent), care conține toți literalii celor două clauze, mai puțin xi
și ¬xi
. În plus, din punct de vedere logic, avem că (C1 ∧ C2) ⟹ R
, adică R
este adevărat ori de câte ori C1
și C2
sunt.
De exemplu:
(x1 ∨ x2)
și (¬x1 ∨ x3 ∨ x4)
pot fi rezolvate în raport cu variabila x1
, producând clauza (x2 ∨ x3 ∨ x4)
.(x1 ∨ x2)
și (x1 ∨ x3 ∨ x4)
nu pot fi rezolvate în raport cu nicio variabilă, întrucât nu există niciun literal din prima clauză al cărui complement să apară în a doua.Utilizând rezoluția, introducem următorul algoritm de determinare a asumpțiilor incompatibile:
Unit
), rezolvăm (dacă este posibil, conform definiției de mai sus a rezoluției) clauza stocată în acțiune cu clauza curentă, în raport cu literalul stocat de asemenea în acțiune, rezultând o nouă clauză curentă.Rezolvând doar în raport cu literalii impuși de eliminarea clauzelor unitare, obținem efectul dorit ca aceștia să nu apară în combinația de asumpții incompatibile. Nevoia de operare pe clauzele originale justifică formulele extinse din etapa 2.
Să aplicăm algoritmul pe exemplul de mai sus, operând de data aceasta în ordine inversă, dinspre pasul 5, care a generat conflictul, către pasul 1:
(¬x1)
, care a condus la vidarea clauzei (x1)
(conflict), se determină clauza originală din care s-a obținut această clauză vidă, și anume (x1 ∨ x2)
, care devine clauza curentă. Apoi, începe consultarea acțiunilor realizate deja, conform algoritmului de mai sus. Mai întâi, se analizează cea mai recentă acțiune, de eliminare a clauzei unitare (¬x1)
, care stochează în reprezentarea sa literalul ¬x1
și clauza originală din care s-a obținut această clauză unitară, și anume (¬x1 ∨ x2)
. Rezolvând-o pe aceasta cu clauza curentă (x1 ∨ x2)
în raport cu literalul ¬x1
, se obține rezolventul (x2)
, reprezentând noua clauză curentă.(¬x2)
, care stochează în reprezentarea sa literalul ¬x2
și clauza originală din care s-a obținut această clauză unitară, și anume (¬x2 ∨ x5 ∨ x7)
. Rezolvând-o pe aceasta cu clauza curentă (x2)
în raport cu literalul ¬x2
, se obține rezolventul (x5 ∨ x7)
, reprezentând noua clauză curentă.(x5 ∨ x7)
.
Pentru a înțelege semnificația acestei clauze, (x5 ∨ x7)
, să ne amintim combinația de asumpții incompatibile depistată mai sus, {¬x7, ¬x5}
, conform căreia ambele variabile, x7
și x5
, au fost asumate false. Pentru a depăși incompatibilitatea, cel puțin una dintre variabile trebuie asumată adevărată, adică exact ce codifică clauza (x5 ∨ x7)
. Proprietatea rezolventului afirmă că acesta este întotdeauna adevărat, dacă clauzele pe baza căruia a fost obținut sunt adevărate. Prin urmare, dacă vrem ca clauza vidată (x1 ∨ x2)
și clauzele devenite unitare și eliminate (¬x1 ∨ x2)
și (¬x2 ∨ x5 ∨ x7)
să fie simultan adevărate (într-o anumită interpretare), atunci și clauza (x5 ∨ x7)
trebuie să fie adevărată (în acea interpretare). Observați cum într-adevăr clauza referă doar variabile asupra cărora s-au realizat asumpții arbitrare, ca x5
și x7
, nu și variabile pentru care asumpțiile au fost impuse, ca x2
.
(x5 ∨ x7)
poartă numele de clauză învățată (learned clause), întrucât nu face parte din formula originală. Dacă clauza învățată însăși este vidă, înseamnă că este imposibil ca clauzele din care a fost obținută să fie adevărate simultan, și deci formula originală este nesatisfiabilă.
Pentru a ne asigura că noua clauză, (x5 ∨ x7)
, nu va mai fi niciodată vidată de asumpțiile realizate, o putem adăuga la formula originală, și ne putem întoarce în trecut la cel mai distant punct în care această clauză devine unitară (funcția backtrackToUnitClause
din etapa 2), astfel încât următoarea acțiune să o satisfacă imediat. Revenirea se poate face cu un număr arbitrar de pași în trecut, motiv pentru care poartă numele de backtracking necronologic. Revenirea la cel mai distant punct, și nu la cel mai recent, scade probabilitatea de eșec al căii curente, datorat altor asumpții realizate pe anterioara cale eșuată. Astfel, răspundem și la întrebarea (2). Remarcăm că, din moment ce rezolventul derivă logic din anumite clauze ale formulei originale, formula rezultată prin adăugarea clauzei învățate la formula originală este echivalentă cu formula originală.
În exemplul de mai sus, se adaugă clauza învățată (x5 ∨ x7)
la formula originală, obținându-se formula (¬x7 ∨ x1) ∧ (¬x6 ∨ ¬x2 ∨ x1) ∧ (¬x5 ∨ x1) ∧ (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x2 ∨ ¬x1 ∨ x6) ∧ (¬x2 ∨ x5 ∨ x7) ∧ (¬x1 ∨ x2) ∧ (x1 ∨ x2) ∧ (x5 ∨ x7)
:
¬x7
, în urma căreia clauza învățată, (x5 ∨ x7)
, devine unitară, (x5)
. Noua formulă este (¬x6 ∨ ¬x2 ∨ x1) ∧ (¬x5 ∨ x1) ∧ (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x2 ∨ ¬x1 ∨ x6) ∧ (¬x2 ∨ x5) ∧ (¬x1 ∨ x2) ∧ (x1 ∨ x2) ∧ (x5)
.(x5)
determină eliminarea acesteia și obținerea formulei (¬x6 ∨ ¬x2 ∨ x1) ∧ (x1) ∧ (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x2 ∨ ¬x1 ∨ x6) ∧ (¬x1 ∨ x2) ∧ (x1 ∨ x2)
. Acesta este punctul în care clauza învățată își produce efectele.(x1)
și se obține formula (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (¬x2 ∨ x6) ∧ (x2)
.(x2)
și se obține formula (¬x4 ∨ x3) ∧ (¬x3 ∨ x4) ∧ (x6)
.(x6)
și se obține formula (¬x4 ∨ x3) ∧ (¬x3 ∨ x4)
.¬x4
și se obține formula (¬x3)
.(¬x3)
și se obține formula vidă. SUCCES!
Observați cum combinația alternativă neproductivă menționată mai sus, ¬x7 - ¬x6 - x5
, a fost evitată, prin realizarea de data aceasta a asumpției x5
înaintea asumpției pentru variabila x6
.
Am ajuns în punctul în care putem integra toate conceptele discutate până acum, în algoritmul complet de satisfacere:
Problema 3-colorare a unui graf neorientat urmărește asocierea unei culori din trei (Red
, Green
, Blue
) fiecărui nod din graf, astfel încât oricare două noduri adiacente să fie colorate diferit. Problema poate fi rezolvată prin reducere la SAT, parcurgând etapele:
Vom întrebuința următoarea schemă de codificare a unui graf într-o formulă CNF:
n
din graf îi corespund trei variabile booleene, aferente celor trei culori posibile: x[n,R]
, x[n,G]
, x[n,B]
.n
are cel puțin o culoare: (x[n,R] ∨ x[n,G] ∨ x[n,B])
.n
are cel mult o culoare: (¬x[n,R] ∨ ¬x[n,G]) ∧ (¬x[n,R] ∨ ¬x[n,B]) ∧ (¬x[n,G] ∨ ¬x[n,B])
.(n, o)
are capetele colorate diferit: (¬x[n,R] ∨ ¬x[o,R]) ∧ (¬x[n,G] ∨ ¬x[o,G]) ∧ (¬x[n,B] ∨ ¬x[o,B])
.
Considerăm că nodurile grafului sunt reprezentate prin numere naturale mai mari sau egale cu 1, iar cele trei variabile booleene aferente unui nod n
sunt reprezentate prin numerele 10n+1
, 10n+2
, 10n+3
. De exemplu, nodului 1
îi corespund variabilele 11
, 12
, 13
, nodului 2
, variabilele 21
, 22
, 23
etc. Variabila 11
corespunde propoziției „Nodul 1 este roșu”, variabila 22
, propoziției „Nodul 2 este verde”, variabila 33
, propoziției „Nodul 3 este albastru” etc.
De exemplu, pentru graful neorientat cu nodurile {1, 2}
și muchia (1, 2)
, se obține formula cu următoarele variabile și clauze (numerele itemilor oglindesc enumerarea de mai sus):
11
, 12
, 13
, 21
, 22
, 23
.{11, 12, 13}
, {21, 22, 23}
.{-11, -12}
, {-11, -13}
, {-12, -13}
, {-21, -22}
, {-21, -23}
, {-22, -23}
.{-11, -21}
, {-12, -22}
, {-13, -23}
.
Pentru întreaga formulă descrisă mai sus, algoritmul de satisfacere ar putea produce interpretarea {-23, -22, -13, -11, 12, 21}
, care corespunde colorării {(1, Green), (2, Red)}
.
A treia etapă a temei abordează rezolvarea clauzelor, învățarea clauzelor, algoritmul de satisfacere și aplicația la problema 3-colorare.
Construcțiile și mecanismele de limbaj pe care le veți exploata în rezolvare, pe lângă cele din etapele 1 și 2, sunt:
Modulul de interes din schelet este Solver
, care conține operațiile pe care trebuie să le implementați. Găsiți detaliile despre funcționalitate și despre constrângerile de implementare, precum și exemple, direct în fișier. Aveți de completat definițiile care încep cu *** TODO ***
.
Pentru rularea testelor, încărcați în interpretor modulul TestSolver
și evaluați main
. Ultimul test (stress
) utilizează grafuri mai mari și execuția poate dura câteva secunde.
Este suficient ca arhiva pentru vmchecker să conțină modulele Solver
, ExtendedFormula
din etapa 2 și Formula
din etapa 1.
Depunctările urmează aceleași principii ca în primele două etape.
Graph
, Color
și ThreeColoring
din Solver.hs
cu deriving (Show, Eq)
în loc de deriving Show
.backtrackToUnitClause