You are on page 1of 145

A. M. Florea B. Dorohonceanu C.

Frâncu

Programare în Prolog
pentru
Inteligenţă Artificială

UNIVERSITATEA “POLITEHNICA” BUCUREŞTI


1997
Partea I

Limbajul Prolog
Începutul programării logice poate fi atribuit lui R. Kowalski şi A. Colmerauer şi se situează
la începutul anilor '70. Kowalski a plecat de la o formulă logică de tipul:
S1 ∧ S2 ∧ ... Sn → S
care are, în logica cu predicate de ordinul întâi semnificaţia declarativă conform căreia
S1 ∧ S2 ∧ ... Sn implică S, adică dacă S1 şi S2 ... şi Sn sunt fiecare adevărate atunci şi S este
adevărat, şi a propus o interpretare procedurală asociată. Conform acestei interpretări,
formula de mai sus poate fi scrisă sub forma:
S dacă S1 şi S2 ... şi Sn

şi poate fi executată ca o procedură a unui limbaj de programare recursiv, unde S este


antetul procedurii şi S1, S2, ... Sn corpul acesteia. Deci, pe lângă interpretarea declarativă,
logică, a unei astfel de formule, formula poate fi interpretată procedural astfel: pentru a
executa S se execută S1 şi S2 ... şi Sn.

În aceeaşi perioadă, A. Colmerauer şi colectivul lui de cercetare de la Universitatea din


Marsilia au dezvoltat un limbaj de implementare a acestei abordări, pe care l-au denumit
Prolog, abreviere de la "Programmation et Logique". De atunci şi până în prezent, limbajul
Prolog s-a impus ca cel mai important limbaj de programare logică şi s-au dezvoltat
numeroase implementări, atât ale unor interpretoare cât şi ale unor compilatoare ale
limbajului.
Limbajul Prolog este un limbaj declarativ susţinut de o componentă procedurală. Spre
deosebire de limbajele procedurale, cum ar fi C sau Pascal, în care rezolvarea problemei
este specificată printr-o serie de paşi de execuţie sau acţiuni, într-un limbaj declarativ
problema este specificată prin descrierea universului problemei şi a relaţiilor sau funcţiilor
existente între obiecte din acest univers. Exemple de astfel de limbaje sunt cele funcţionale,
de exemplu Lisp, Scheme, ML, şi cele logice, de exemplu Prolog.
Deşi iniţial a fost gândit pentru un set restrâns de probleme, Prolog a devenit cu timpul
un limbaj de uz general, fiind o unealtă importantă în aplicaţiile de inteligenţă artificială
[CM84, Bra88, SS86]. Pentru multe probleme, un program Prolog are cam de 10 ori mai
puţine linii decât echivalentul lui în Pascal.
În 1983, cercetătorii din Japonia au publicat un plan ambiţios de creare a unor
calculatoare de generaţia a 5-a pentru care Prolog era limbajul de asamblare. Planul nu a

2
reuşit, dar acest proiect a marcat o dezvoltare deosebită a interpretoarelor şi compilatoarelor
de Prolog, precum şi o creştere mare a numărului de programatori în acest limbaj.
Multe clase de probleme poate fi rezolvate în Prolog, existând anumite categorii care
sunt rezolvabile mult mai uşor în Prolog decât în orice alt limbaj procedural. Astfel de
probleme sunt în principal cele dedicate prelucrării simbolice sau care necesită un proces de
căutare a soluţiei într-un spaţiu posibil de transformări ale problemei.
Prezentarea limbajului Prolog ce urmează este în principal orientată pe descrierea
limbajului Prolog standard. Exemplele de programe din această parte cât şi din partea a
doua sunt rulate utilizând interpretorul ARITY Prolog pe microcalculatoare de tip IBM/PC,
mediul de programare ARITY Prolog şi particularităţile lui sintactice fiind prezentate în
partea a doua.

1 Entităţile limbajului Prolog


Limbajul Prolog este un limbaj logic, descriptiv, care permite specificarea problemei de
rezolvat în termenii unor fapte cunoscute despre obiectele universului problemei şi a
relaţiilor existente între aceste obiecte. Execuţia unui program Prolog constă în deducerea
implicaţiilor acestor fapte şi relaţii, programul definind astfel o mulţime de consecinţe ce
reprezintă înţelesul sau semnificaţia declarativă a programului.
Un program Prolog conţine următoarele entităţi:
• fapte despre obiecte şi relaţiile existente între aceste obiecte;

• reguli despre obiecte şi relaţiile dintre ele, care permit deducerea (inferarea) de noi
fapte pe baza celor cunoscute;
• întrebări, numite şi scopuri, despre obiecte şi relaţiile dintre ele, la care programul
răspunde pe baza faptelor şi regulilor existente.
1.1 Fapte
Faptele sunt predicate de ordinul întâi de aritate n considerate adevărate. Ele stabilesc relaţii
între obiectele universului problemei. Numărul de argumente ale faptelor este dat de aritatea
(numărul de argumente) corespunzătoare a predicatelor.
Exemple:

3
Fapt: Aritate:
papagal(coco). 1
iubeşte(mihai, maria). 2
iubeşte(mihai, ana). 2
frumoasă(ana). 1
bun(gelu). 1
deplasează(cub, camera1, camera2). 3

Interpretarea particulară a predicatului şi a argumentelor acestuia depinde de


programator. Ordinea argumentelor, odată fixată, este importantă şi trebuie păstrată la orice
altă utilizare a faptului, cu aceeaşi semnificaţie. Mulţimea faptelor unui program Prolog
formează baza de cunoştinţe Prolog. Se va vedea mai târziu că în baza de cunoştinte a unui
program Prolog sunt incluse şi regulile Prolog.
1.2 Scopuri
Obţinerea consecinţelor sau a rezultatului unui program Prolog se face prin fixarea unor
scopuri care pot fi adevărate sau false, în funcţie de conţinutul bazei de cunoştinţe Prolog.
Scopurile sunt predicate pentru care se doreşte aflarea valorii de adevăr în contextul faptelor
existente în baza de cunoştinţe. Cum scopurile pot fi văzute ca întrebări, rezultatul unui
program Prolog este răspunsul la o întrebare (sau la o conjuncţie de întrebări). Acest răspuns
poate fi afirmativ, yes, sau negativ, no. Se va vedea mai târziu că programul Prolog, în cazul
unui răspuns afirmativ la o întrebare, poate furniza şi alte informaţii din baza de cunoştinţe.
Exemplu
Considerând baza de cunoştinţe specificată anterior, se pot pune diverse întrebări, cum
ar fi:
?- iubeste(mihai, maria).
yes deoarece acest fapt există în baza de cunoştinţe
?- papagal(coco).
yes
?- papagal(mihai).
no deoarece acest fapt nu există în baza de cunoştinţe
?- inalt(gelu).
no

1.3 Variabile
În exemplele prezentate până acum, argumentele faptelor şi întrebărilor au fost obiecte
particulare, numite şi constante sau atomi simbolici. Predicatele Prolog, ca orice predicate în
logica cu predicate de ordinul I, admit ca argumente şi obiecte generice numite variabile. În
Prolog, prin convenţie, numele argumentelor variabile începe cu literă iar numele
constantelor simbolice începe cu literă mică. O variabilă poate fi instanţiată (legată) dacă
4
există un obiect asociat acestei variabile, sau neinstanţiată (liberă) dacă nu se ştie încă ce
obiect va desemna variabila.
La fixarea unui scop Prolog care conţine variabile, acestea sunt neinstanţiate iar
sistemul încearcă satisfacerea acestui scop căutând printre faptele din baza de cunoştinţe un
fapt care poate identifica cu scopul, printr-o instanţiere adecvată a variabilelor din scopul
dat. Este vorba de fapt de un proces de unificare a predicatului scop cu unul din predicatele
fapte existente în baza de cunoştinţe.
La încercarea de satisfacere a scopului, căutarea se face întotdeauna pornind de la
începutul bazei de cunoştinţe. Dacă se întâlneşte un fapt cu un simbol predicativ identic cu
cel al scopului, variabilele din scop se instanţiază conform algoritmului de unificare şi
valorile variabilelor astfel obţinute sunt afişate ca răspuns la satisfacerea acestui scop.
Exemple:
?- papagal(CineEste).
CineEste = coco
?- deplaseaza(Ce, DeUnde, Unde).
Ce = cub, DeUnde = camera1, Unde = camera2
?- deplaseaza(Ce, Aici, Aici).
no

Cum se comportă sistemul Prolog în cazul în care există mai multe fapte în baza de
cunoştinţe care unifică cu întrebarea pusă? În acest caz există mai multe răspunsuri la
întrebare, corespunzând mai multor soluţii ale scopului fixat. Prima soluţie este dată de
prima unificare şi există atâtea soluţii câte unificări diferite există. La realizarea primei
unificări se marchează faptul care a unificat şi care reprezintă prima soluţie. La obţinerea
următoarei soluţii, căutarea este reluată de la marcaj în jos în baza de cunoştinţe. Obţinerea
primei soluţii este de obicei numită satisfacerea scopului iar obţinerea altor soluţii,
resatisfacerea scopului. La satisfacera unui scop căutarea se face întotdeauna de la
începutul bazei de cunoştinţe. La resatisfacerea unui scop, căutarea se face începând de la
marcajul stabilit de satisfacerea anterioară a acelui scop.
Sistemul Prolog, fiind un sistem interactiv, permite utilizatorului obţinerea fie a
primului răspuns, fie a tuturor răspunsurilor. În cazul în care, după afişarea tuturor
răspunsurilor, un scop nu mai poate fi resatisfăcut, sistemul răspunde no.
Exemple:
?- iubeste(mihai, X).
X = maria; tastând caracterul “;” şi Enter, cerem o nouă soluţie
X = ana;
no
?- iubeste(Cine, PeCine).
5
Cine = mihai, PeCine = maria;
Cine = mihai, PeCine = ana;
no

Există deci două soluţii pentru scopul iubeste(mihai, X) şi tot două soluţii pentru
scopul iubeste(Cine, PeCine), considerând tot baza de cunoştinţe prezentată în secţiunea
1.1.
1.4 Reguli
O regulă Prolog exprimă un fapt care depinde de alte fapte şi este de forma:
S :- S1, S2, …Sn.
cu semnificaţia prezentată la începutul acestui capitol. Fiecare Si, i = 1,n şi S au forma
faptelor Prolog, deci sunt predicate, cu argumente constante, variabile sau structuri. Faptul S
care defineşte regula, se numeşte antet de regulă, iar S1, S2, …Sn formează corpul regulii şi
reprezintă conjuncţia de scopuri care trebuie satisfăcute pentru ca antetul regulii să fie
satisfăcut.
Fie următoarea bază de cunoştinţe Prolog:
frumoasa(ana). %1
bun(vlad). %2
cunoaste(vlad, maria). %3
cunoaste(vlad, ana). %4
iubeste(mihai, maria). %5
iubeste(X, Y) :- %6
bun(X),
cunoaste(X, Y),
frumoasa(Y).

Se observă că enunţul (6) defineşte o regulă Prolog; relaţia iubeste(Cine, PeCine),


fiind definită atât printr-un fapt (5) cât şi printr-o regulă (6).
În condiţiile existenţei regulilor în baza de cunoştinţe Prolog, satisfacerea unui scop se
face printr-un procedeu similar cu cel prezentat în Secţiunea 1.2, dar unificarea scopului se
încearcă atât cu fapte din baza de cunoştinţe, cât şi cu antetul regulilor din bază. La
unificarea unui scop cu antetul unei reguli, pentru a putea satisface acest scop trebuie
satisfăcută regula. Aceasta revine la a satisface toate faptele din corpul regulii, deci
conjuncţia de scopuri. Scopurile din corpul regulii devin subscopuri a căror satisfacere se va
încerca printr-un mecanism similar cu cel al satisfacerii scopului iniţial.
Pentru baza de cunoştinţe descrisă mai sus, satisfacerea scopului
?- iubeste(vlad, ana).

6
se va face în următorul mod. Scopul unifică cu antetul regulii (6) şi duce la instanţierea
variabilelor din regula (6): X = vlad şi Y = ana. Pentru ca acest scop să fie îndeplinit,
trebuie îndeplinită regula, deci fiecare subscop din corpul acesteia. Aceasta revine la
îndeplinirea scopurilor bun(vlad), care reuşeşte prin unificare cu faptul (2),
cunoaste(vlad, ana), care reuşeşte prin unificare cu faptul (4), şi a scopului
frumoasa(ana), care reuşeşte prin unificare cu faptul (1). În consecinţă, regula a fost
îndeplinită, deci şi întrebarea iniţială este adevarată, iar sistemul răspunde yes.
Ce se întîmplă dacă se pune întrebarea:
?- iubeste(X, Y).

Prima soluţie a acestui scop este dată de unificarea cu faptul (5), iar răspunsul este:
X = mihai, Y = maria

Sistemul Prolog va pune un marcaj în dreptul faptului (5) care a satisfăcut scopul.
Următoarea soluţie a scopului iubeste(X, Y) se obţine începând căutarea de la acest marcaj
în continuare în baza de cunoştinţe. Scopul unifică cu antetul regulii (6) şi se vor fixa trei
noi subscopuri de îndeplinit, bun(X), cunoaste(X, Y) şi frumoasa(Y). Scopul bun(X) este
satisfăcut de faptul (2) şi variabila X este instanţiată cu valoarea vlad, X = vlad . Se încearcă
acum satisfacerea scopului cunoaste(vlad, Y), care este satisfăcut de faptul (3) şi determină
instanţierea Y = maria. Se introduce în baza de cunoştinţe un marcaj asociat scopului
cunoaste(vlad, Y), care a fost satisfăcut de faptul (3). Se încearcă apoi satisfacerea scopului
frumoasa(maria). Acesta eşuează. În acest moment sistemul intră într-un proces de
backtracking în care se încearcă resatisfacerea scopului anterior satisfăcut,
cunoaste(vlad, Y), în speranţa că o noua soluţie a acestui scop va putea satisface şi scopul
curent care a eşuat, frumoasa(Y). Resatisfacerea scopului cunoaste(vlad, Y) se face
pornind căutarea de la marcajul asociat scopului în jos, deci de la faptul (3) în jos. O nouă
soluţie (resatisfacere) a scopului cunoaste(vlad, Y) este dată de faptul (4) care determină
instanţierea Y = ana. În acest moment se încearcă satisfacerea scopului frumoasa(ana).
Cum este vorba de un nou scop, căutarea se face de la începutul bazei de cunoştinţe şi
scopul frumoasa(ana) este satisfăcut de faptul (1). În consecinţă a doua soluţie a scopului
iubeste(X, Y) este obţinută şi sistemul răspunde:
X = vlad, Y = ana

urmând un mecanism de backtracking, descris intuitiv în figura 1 prin prezentarea arborilor


de deducţie construiţi de sistemul Prolog.
La încercarea de resatisfacere a scopului iubeste(X, Y), printr-un mecanism similar, se
observă că nu mai există alte solutii. În concluzie, fiind dată baza de fapte şi reguli Prolog
anterioară, comportarea sistemului Prolog este:
?- iubeste(X, Y).
7
X = mihai, Y = maria;
X = vlad, Y = ana;
no

iubeste(X,Y)

%5 iubeste(mihai,maria)

SUCCES

solutie 1: X=mihai, Y=maria

iubeste(X,Y)

bun(X) cunoaste(X,Y) %6 frumoasa(Y)

%2 bun(vlad) cunoaste(vlad,Y) frumoasa(maria) %1 frumoasa(ana)

SUCCES %3 cunoaste(vlad,maria) %4 cunoaste(vlad,ana) INSUCCES SUCCES

SUCCES SUCCES

solutie 2: X=vlad, Y=ana


Figura 1. Mecanismul de satisfacere a scopurilor în Prolog
Observaţii:
• La satisfacerea unei conjuncţii de scopuri în Prolog, se încearcă satisfacerea fiecărui
scop pe rând, de la stânga la dreapta. Prima satisfacere a unui scop determină
plasarea unui marcaj în baza de cunoştinţe în dreptul faptului sau regulii care a
determinat satisfacerea scopului.
• Dacă un scop nu poate fi satisfăcut (eşuează), sistemul Prolog se întoarce şi încearcă
resatisfacerea scopului din stânga, pornind căutarea în baza de cunoştinţe de la
marcaj în jos. Înainte de resatisfacerea unui scop se elimină toate instanţierile de
variabile determinate de ultima satisfacere a acestuia. Dacă cel mai din stânga scop
din conjuncţia de scopuri nu poate fi satisfăcut, întreaga conjuncţie de scopuri
eşuează.

8
• Această comportare a sistemului Prolog în care se încearcă în mod repetat
satisfacerea şi resatisfacerea scopurilor din conjuncţiile de scopuri se numeşte
backtracking.
• În secţiunea 4 se va discuta pe larg structura de control a sistemului Prolog,
mecanismul fundamental de backtracking şi modurile în care se poate modifica
parţial acest mecanism.
1.5 Un program Prolog simplu
Simplitatea şi expresivitatea limbajului Prolog poate fi pusă în evidentă de următorul
exemplu de descriere a unui circuit logic "poartă ŞI". Se consideră poarta ŞI ca fiind
construită prin conectarea unei "porţi ŞI-NU" cu un inversor. Întregul circuit este definit de
relaţia:
poarta_si(Intrare1, Intrare2, Iesire)

pe baza relaţiilor
poarta_si_nu(Intrare1, Intrare2, Iesire)
inversor(Intrare, Iesire).

În figura 2 este prezentată schema porţii ŞI în care se observă că inversorul este


construit dintr-un tranzistor cu sursa conectată la masă şi un rezistor cu un capăt conectat la
alimentare. Poarta tranzistorului este intrarea inversorului, în timp ce celălalt capăt al
rezistorului trebuie conectat la colectorul tranzistorului care formează ieşirea inversorului.

n1

n2 n3

n4
n5

Figura 2. Un circuit logic poartă ŞI


Variabilele comune între predicate sunt utilizate pentru a specifica legăturile comune.
rezistor(alimentare, n1).
rezistor(alimentare, n2).

9
tranzistor(n2, masa, n1).
tranzistor(n2, masa, n1).
tranzistor(n3, n4, n2).
tranzistor(n5, masa, n4).
inversor(Intrare, Iesire) :-
tranzistor(Intrare, masa, Iesire),
rezistor(alimentare, Iesire).
poarta_si_nu(Intrare1, Intrare2, Iesire) :-
tranzistor(Intrare1, X, Iesire),
tranzistor(Intrare2, masa, X),
rezistor(alimentare, Iesire).
poarta_si(Intrare1, Intrare2, Iesire) :-
poarta_si_nu(Intrare1, Intrare2, X),
inversor(X, Iesire).

Pentru întrebarea
?- poarta_si(In1, In2, Iesire).
In1 = n3, In2= n5, Iesire = n1

răspunsul sistemului Prolog confirmă faptul că circuitul descris este o poartă ŞI, identificând
intrările şi ieşirile corespunzătoare.

2 Sintaxa limbajului Prolog


Aşa cum s-a arătat în secţiunea anterioară, un program Prolog este format din fapte, reguli şi
întrebări, acestea fiind construite pe baza predicatelor definite de utilizator sau predefinite.
În orice sistem Prolog există o mulţime de predicate predefinite, unele dintre acestea fiind
predicate standard, iar altele depinzând de implementare. Argumentele predicatelor Prolog,
prin analogie cu logica predicatelor de ordinul I, se numesc termeni, şi pot fi constante,
variabile sau structuri.
2.1 Constante
Constantele definesc obiecte specifice, particulare, sau relaţii particulare. Există două tipuri
de constante: atomi şi numere. Atomii sunt constante simbolice care încep, de obicei, cu o
literă şi pot conţine litere, cifre şi caracterul “_”. Există şi alte caractere ce pot forma atomi
speciali, care au o semnificaţie aparte în limbaj. Atomii pot desemna:
• obiecte constante care sunt argumentele predicatelor, de exemplu atomii mihai şi
maria în faptul iubeste(mihai, maria);
• predicate Prolog, fie cele definite de utilizator, fie cele predefinite în sistem; de
exemplu atomul iubeste în faptul iubeste(mihai, maria);
10
• atomi speciali, de exemplu atomii “:-” şi “?”- ;

• diverse alte reguli de construcţie sintactică a atomilor depind de implementare.


Numerele pot fi întregi sau reale; sintaxa particulară acceptată cât şi domeniile de
definiţie depinzând de implementare. Detalii despre formatul numerelor şi a altor tipuri de
constante din mediul ARITY Prolog sunt descrise în partea a doua.
2.2 Variabile
Variabilele sunt, din punct de vedere sintactic, tot atomi, dar ele au o semnificaţie specială,
aşa cum s-a arătat în Secţiunea 1.3. Spre deosebire de regulile generale admise pentru
construcţia atomilor, numele unei variabile poate începe şi cu simbolul “_”, ceea ce indică o
variabilă anonimă. Utilizarea unei variabile anonime semnifică faptul că nu interesează
valoarea la care se va instanţia acea variabilă.
De exemplu, interogarea ?- iubeste( _, maria). semnifică faptul că se întreabă dacă
există cineva care o iubeşte pe Maria, dar nu interesează cine anume. Limbajul Prolog face
distincţia între litere mari şi litere mici (este case sensitive). Se reaminteşte că, din punctul
de vedere al convenţiei Prolog, numele oricărei variabile trebuie să înceapă fie cu literă
mare, fie cu “_”.
2.3 Structuri
O structură Prolog este un obiect ce desemneaza o colecţie de obiecte corelate logic, care
formează componentele structurii. Un exemplu este structura asociată obiectului carte, care
este formată din componentele: titlu carte, autor, şi an apariţie. Un fapt ce referă relaţia de
posedare a unei cărţi de Prolog de către Mihai poate fi exprimat astfel:
poseda(mihai, carte(prolog, clocksin, 1981)).

unde carte(prolog, clocksin, 1981) este o structură cu numele carte şi cu trei componente:
prolog, clocksin şi 1981. Se admit şi structuri imbricate, de exemplu:
poseda(mihai, carte(prolog, autori(clocksin, mellish), 1981)).

unde predicatul poseda are două argumente: primul argument este constanta mihai, iar cel
de-al doilea este structura carte(prolog ....), cu două componente, a doua componentă fiind
structura autori(clocksin, mellish).
În Prolog, o structură se defineşte prin specificarea:
(1) numelui structurii ( functorul structurii);
(2) elementelor structurii (componentele structurii).
Structurile Prolog pot fi utilizate pentru reprezentarea structurilor de date, de exemplu
liste sau arbori. În Secţiunea 2.6 se vor prezenta structuri Prolog predefinite care permit

11
lucrul cu liste. Un arbore binar poate fi reprezentat în Prolog tot printr-o structură, de
exemplu:
arb(barbu, arb(ada, vid, vid), vid).

reprezintă un arbore binar cu cheia din rădăcina barbu, cu subarborele drept vid şi cu
subarborele stâng format dintr-un singur nod cu cheia ada.
Observaţii:
• Sintaxa structurilor este aceeaşi cu cea a faptelor Prolog. Un predicat Prolog poate fi
văzut ca o structură a cărui functor este numele predicatului, iar argumentele
acestuia reprezintă componentele structurii.
• Considerarea predicatelor Prolog ca structuri prezintă un interes deosebit; atât datele
cât şi programele în Prolog au aceeaşi formă, ceea ce facilitează tratarea uniformă şi
sinteza dinamică de programe. În Secţiunea 4.3 se vor prezenta predicate predefinite
în Prolog care permit sinteza şi execuţia dinamică a programelor Prolog.
2.4 Operatori
Uneori este convenabil să se scrie anumiţi functori (nume de structuri sau predicate) în
formă infixată. Aceasta este o formă sintactică ce măreşte claritatea programului, cum ar fi
cazul operatorilor aritmetici sau al operatorilor relaţionali.
Limbajul Prolog oferă o mulţime de operatori, unii care se regăsesc în aproape orice
implementare, iar alţii care sunt specifici unei versiuni particulare de implementare a
limbajului. În continuare se vor prezenta o parte dintre operatorii din prima categorie.
(1) Operatori aritmetici
Operatorii aritmetici binari, cum ar fi +, -, *, /, pot fi scrişi în Prolog în notaţie infixată; de
exemplu:
1 + 2*(X * Y) / Z

Aceasta sintaxa este de fapt o rescriere infixată a formei prefixate a structurilor echivalente:
+(1, 2) / (*(X, Y), Z)

Este important de reţinut că operatorii aritmetici sunt o rescriere infixată a unor


structuri deoarce valoarea expresiei astfel definită nu este calculată. Evaluarea expresiei se
face la cerere în cazul în care se foloseste operatorul predefinit infixat is, de exemplu:
X is 1 + 2.

va avea ca efect instanţierea variabilei X la valoarea 3.


(2) Operatori relaţionali

12
Operatorii relaţionali sunt predicate predefinite infixate. Un astfel de operator este
operatorul de egalitate =. Predicatul (operatorul) de egalitate funcţionează ca şi cum ar fi
definit prin următorul fapt:
X = X.

iar încercarea de a satisface un scop de tipul X = Y se face prin încercarea de a unifica X cu


Y. Din aceasta cauză, dându-se un scop de tipul X = Y, regulile de decizie care indică dacă
scopul se îndeplineşte sau nu sunt următoarele:
• Dacă X este variabilă neinstanţiată, iar Y este instanţiată la orice obiect Prolog,
atunci scopul reuşeşte. Ca efect lateral, X se va instanţia la aceeaşi valoare cu cea a
lui Y. De exemplu:
?- carte(barbu, poezii) = X.
este un scop care reuşeşte şi X se instanţiază la carte(barbu, poezii).
• Dacă atât X cât şi Y sunt variabile neinstanţiate, scopul X = Y reuşeşte, variabila X
este legată la Y şi reciproc. Aceasta înseamnă că ori de câte ori una dintre cele două
variabile se instanţiază la o anumită valoare, cealaltă variabila se va instanţia la
aceeaşi valoare.
• Atomii şi numerele sunt întotdeauna egali cu ei înşişi. De exemplu, următoarele
scopuri au rezultatul marcat drept comentariu:
mihai = mihai % este satisfăcut
mare = mic % eşuează
102 = 102 % reuşeşte
1010 = 1011 % eşuează

• Două structuri sunt egale dacă au acelaşi functor, acelaşi număr de componente şi
fiecare componentă dintr-o structură este egală cu componenta corespunzătoare din
cealaltă structură. De exemplu, scopul:
autor(barbilian, ciclu(uvedenrode, poezie(riga_crypto))) =
autor(X, ciclu(uvedenrode, poezie(Y)))

este satisfăcut, iar ca efect lateral se fac instanţierile:


X = barbilian şi Y = riga_crypto.

Operatorul de inegalitate \= se defineşte ca un predicat opus celui de egalitate. Scopul


X \= Y reuşeşte dacă scopul X = Y nu este satisfăcut şi eşuează dacă X = Y reuşeşte. În
plus, există predicatele relaţionale de inegalitate definite prin operatorii infixaţi >, <, =<,
>=, cu semnificaţii evidente.

13
Un operator interesant este =:=. El face numai evaluare aritmetică şi nici o instanţiere.
Exemplu:
?- 1 + 2 =:= 2 + 1.
yes
?- 1 + 2 = 2 + 1.
no

Predicatul = = testează echivalenţa a două variabile. El consideră cele două variabile


egale doar dacă ele sunt deja partajate. X = = Y reuşeşte ori de câte ori X = Y reuşeşte, dar
reciproca este falsă:
?- X = = X.
X=_23 %variabilă neinstanţiată
?- X= =Y.
no
?- X=Y, X= =Y.
X=_23, Y=_23 % X şi Z variabile neinstanţiate partajate

În cele mai multe implementări Prolog există predefinit operatorul de obţinere a


modulului unui număr, mod; scopul
X is 5 mod 3.

reuşeşte şi X este instanţiat la 2.


Comentariile dintr-un program Prolog sunt precedate de caracterul “%”.
Exemple:
1. Presupunând că nu există operatorul predefinit mod, se poate scrie un predicat
Prolog cu efect similar. O definiţie posibilă a predicatului modulo(X, Y, Z), cu
semnificaţia argumentelor Z = X mod Y , presupunând X, Y > 0, este:
% modulo(X, Y, Z)
modulo(X, Y, X) :- X < Y.
modulo(X, Y, Z) :- X >= Y, X1 is X - Y, modulo(X1, Y, Z).

2. Plecând de la predicatul modulo definit anterior, se poate defini predicatul de calcul


al celui mai mare divizor comun al două numere, conform algoritmului lui Euclid,
presupunând X > 0 , Y > 0 , astfel:
% cmmdc(X, Y, C)
cmmdc(X, 0, X).
cmmdc(X, Y, C) :- modulo(X, Y, Z), cmmdc(Y, Z, C).

La întrebarea

14
?- cmmdc(15, 25, C).
C=5

răspunsul sistemului este corect. În cazul în care se încearcă obţinerea unor noi
soluţii (pentru semnificaţia cmmdc acest lucru este irelevant, dar interesează din
punctul de vedere al funcţionării sistemului Prolog) se observă ca sistemul intră
într-o buclă infinită datorita imposibilităţii resatisfacerii scopului modulo(X, Y, Z)
pentru Y = 0 . Dacă la definiţia predicatului modulo se adaugă faptul:
modulo(X, 0, X).

atunci predicatul modulo(X, Y, Z) va genera la fiecare resatisfacere aceeaşi soluţie,


respectiv solutia corectă, la infinit. Cititorul este sfătuit să traseze execuţia
predicatului cmmdc în ambele variante de implementare a predicatului modulo.
3. Calculul ridicării unui număr natural la o putere naturală se poate face definind
următorul predicat:
% expo(N, X, XlaN)
expo( _ , 0, 0).
expo(0, _ , 1).
expo(N, X, Exp) :- N > 0, N1 is N - 1, expo(N1, X, Z), Exp is Z * X.

2.5 Operatori definiţi de utilizator


Limbajul Prolog permite definirea de noi operatori de către programator prin introducerea în
program a unor clauze de formă specială, numite directive. Directivele acţionează ca o
definire de noi operatori ai limbajului în care se specifică numele, precedenţa şi tipul
(infixat, prefixat sau postfixat) operatorului. Se reaminteşte faptul că orice operator Prolog
este de fapt o structură care are ca functor numele operatorului şi ca argumente argumentele
operatorului, dar este scris într-o sintaxă convenabilă. La definirea de noi operatori în
Prolog, se creează de fapt noi structuri cărora le este permisă o sintaxă specială, conform
definiţiei corespunzătoare a operatorului. Din acest motiv, nu se asociază nici o operaţie
operatorilor definiţi de programator. Operatorii noi definiţi sunt utilizaţi ca functori numai
pentru a combina obiecte în structuri şi nu pentru a executa acţiuni asupra datelor, deşi
denumirea de operator ar putea sugera o astfel de acţiune.
De exemplu, în loc de a utiliza structura:
are(coco, pene)

se poate defini un nou operator are


:- op(600, xfx, are).

caz în care este legală expresia

15
coco are pene.

Definirea de noi operatori se face cu ajutorul directivei:


:- op(precedenţă-operator, tip-operator, nume-operator).

Operatorii sunt atomi iar precedenta lor trebuie să fie o valoare întreagă într-un anumit
interval şi corelată cu precedenţa operatorilor predefiniţi în limbaj. Tipul operatorilor
fixează caracterul infixat, prefixat sau postfixat al operatorului şi precedenţa operanzilor săi.
Tipul operatorului se defineşte utilizând una din următoarele forme standard:
(1) operatori infixaţi: xfx xfy yfx
(2) operatori prefixaţi: fx fy
(3) operatori postfixaţi: xf yf
unde f reprezintă operatorul, iar x şi y operanzii săi. Utilizarea simbolului x sau a simbolului
y depinde de precedenţa operandului faţă de operator. Precedenţa operanzilor se defineşte
astfel:
• un argument între paranteze sau un argument nestructurat are precedenţa 0;

• un argument de tip structură are precedenţa egală cu cea a functorului operator.


Observaţie: În ARITY Prolog, cu cât valoare-precedenţă-operator este mai mare, cu atât
operatorul are o precedenţă mai mică şi invers.
Semnificaţiile lui x şi y în stabilirea tipului operatorului sunt următoarele:
• x reprezintă un argument (operand) cu precedenţă strict mai mică decât cea a
functorului (operatorului) f
precedenţa( x ) < precedenţa( f )
• y reprezintă un argument (operand) cu precedenţă mai mică sau egală cu cea a
functorului (operandului) f
precedenţa( y ) ≤ precedenţa( f )
Aceste reguli ajută la eliminarea ambiguităţii operatorilor multipli cu aceeaşi
precedenţă. De exemplu, operatorul predefinit în Prolog - (minus) este definit din punct de
vedere al tipului ca yfx, ceea ce înseamnă că structura a - b - c este interpretată ca (a - b) - c
şi nu ca a - (b - c) . Dacă acest operator ar fi fost definit ca xfy, atunci interpretarea structurii
a - b - c ar fi fost a - (b - c) .
Numele operatorului poate fi orice atom Prolog care nu este deja definit în Prolog. Se
poate folosi şi o listă de atomi, dacă se definesc mai mulţi operatori cu aceeaşi precedenţă şi
acelaşi tip.
Exemple:

16
:- op(100, xfx, [este, are]).
:- op(100, xf, zboara).
coco are pene.
coco zboara.
coco este papagal.
bozo este pinguin.
?- Cine are pene.
Cine = coco
?- Cine zboara.
Cine = coco
?- Cine este Ce.
Cine = coco, Ce = papagal;
Cine = bozo, Ce = pinguin;
no

În condiţiile în care se adaugă la baza de cunoştinţe anterioară şi definiţia operatorilor


daca, atunci şi si
:- op(870, fx, daca).
:- op(880, xfx, atunci).
:- op(880, xfy, si).

următoarea structură este validă în Prolog:


daca Animalul are pene
şi Animalul zboara
atunci Animalul este pasare.

2.6 Liste
O listă este o structură de date ce reprezintă o secvenţă ordonată de zero sau mai multe
elemente. O listă poate fi definită recursiv astfel:
(1) lista vidă (lista cu 0 elemente) este o listă
(2) o listă este o structură cu două componente: primul element din listă (capul
listei) şi restul listei (lista formată din urmatoarele elemente din lista).
Sfârşitul unei liste este de obicei reprezentat ca lista vidă.
În Prolog structura de listă este reprezentată printr-o structură standard, predefinită, al
cărei functor este caracterul “.” şi are două componente: primul element al listei şi restul
listei. Lista vidă este reprezentată prin atomul special [ ]. De exemplu, o listă cu un singur
element a se reprezintă în Prolog, prin notaţie prefixată astfel:
.(a, [ ]) având sintaxa in ARITY Prolog '.'(a, [ ])

17
iar o listă cu trei elemene, a, b, c, se reprezintă ca:
.(a, . (b, . (c, [ ]))) cu sintaxa în ARITY Prolog '.'(a, '. '(b, '. '(c, [ ]))).

Deoarece structura de listă este foarte des utilizată în Prolog, limbajul oferă o sintaxă
alternativă pentru descrierea listelor, formată din elementele listei separate de virgulă şi
încadrate de paranteze drepte. De exemplu, cele două liste anterioare pot fi exprimate astfel:
[a]
[a, b, c]

Această sintaxă a listelor este generală şi valabilă în orice implementare Prolog. O


operaţie frecventă asupra listelor este obţinerea primului element dintr-o listă şi a restului
listei, deci a celor două componente ale structurii de listă. Aceasta operaţie este realizată în
Prolog de operatorul de scindare a listelor “|” scris sub următoarea formă:
[Prim | Rest]

Variabila Prim stă pe postul primului element din listă, iar variabila Rest pe postul
listei care conţine toate elementele din listă cu excepţia primului. Acest operator poate fi
aplicat pe orice listă care conţine cel puţin un element. Dacă lista conţine exact un element,
Rest va reprezenta lista vidă. Încercarea de identificare a structurii [Prim | Rest] cu o listă
vidă duce la eşec. Mergând mai departe, se pot obţine chiar primele elemente ale listei şi
restul listei. Iată câteva echivalenţe:
[a, b, c] = [a | [b, c] ] = [a, b | [c] ] = [a, b, c | [ ] ] = [a | [b | [c] ] ] = [a | [b | [c | [ ] ] ] ].
În Prolog elementele unei liste pot fi atomi, numere, liste şi în general orice structuri.
În consecinţă se pot construi liste de liste.
Exemple:
1. Se poate defini următoarea structură de listă:
[carte(barbu, poezii), carte(clocksin, prolog)]

2. Considerând următoarele fapte existente în baza de cunoştinţe Prolog


pred([1, 2, 3, 4]).
pred([coco, sta, pe, [masa, alba]]).

se pot pune următoarele întrebări obţinând răspunsurile specificate:


?- pred([Prim | Rest]).
Prim = 1, Rest = [2, 3, 4];
Prim = coco, Rest = [sta, pe, [masa, alba]];
no
?- pred([ _, _ , _ , [ _ | Rest]])

18
Rest = [alba]

3. Un predicat util în multe aplicaţii este cel care testează apartenenţa unui element la
o listă şi care se defineşte astfel:
% membru(Element, Lista)
membru(Element, [Element | _ ]).
membru(Element, [ _ | RestulListei]) :- membru(Element, RestListei).

Funcţionarea acestui scurt program Prolog poate fi urmărită cerând răspunsul


sistemului la următoarele scopuri:
?- membru(b, [a, b, c]). %1
yes
?- membru(X, [a, b, c]). %2
X = a;
X = b;
X = c;
no
?- membru(b, [a, X, b]). %3
X = b;
X = _0038;
no

Deci pentru cazul în care primul argument este o variabilă (%2) există trei soluţii
posibile ale scopului membru(Element, Lista). Dacă lista conţine o variabilă (%3)
există două soluţii pentru forma listei ([a, b, b] sau [a, _ , b]).
4. Un alt predicat util este cel de concatenare a două liste L1 şi L2, rezultatul
concatenării obţinându-se în lista L3, iniţial neinstanţiată.
% conc(L1, L2, L3)
conc([], L2, L2). % lista vidă concatenată cu L2 este L2.
conc([Prim1|Rest1], Lista2, [Prim1|Rest3]) :-
conc(Rest1, Lista2, Rest3). % lista rezultată este primul element
% al sublistei curente din L1 concatenat
% cu lista întoarsă de apelul recursiv.
?- conc([a, b], [c, d, e], L3).
L3 = [a, b, c, d, e];
no
?- conc([a, b], [c, d, e], L3).
L3 = [a, b, c, d, e]. % “.” şi Enter când nu căutăm alte soluţii
yes
?- conc(L1, [c, d, e], [a, b, c, d, e]).
L1 = [a, b];
19
no

?- conc([a, b], L2, [a, b, c, d, e]).


L2 = [c, d, e];
no
?- conc(L1, L2, [a, b]).
L1 = [ ], L2 = [a, b];
L1 = [a], L2 = [b];
L1 = [a, b], L2 = [ ];
no

Se observă că pentru cazul în care predicatul de concatenare are un singur argument


neinstanţiat există o singură soluţie, iar pentru cazul în care primele două argumente
sunt neinstanţiate (variabile) se obţin mai multe soluţii, corespunzătoare tuturor
variantelor de liste care prin concatenare generează cea de a treia listă.
5. Următorul exemplu defineşte predicatul de testare a existenţei unei sortări în ordine
crescătoare a elementelor unei liste de întregi. Predicatul reuşeşte dacă elementele
listei sunt sortate crescător şi eşuează în caz contrar.
% sortcresc(Lista)
sortcresc([ ]). % lista vidă se consideră sortată
sortcresc([ _ ]). % lista cu un singur element este sortată
sortcresc([X, Y | Rest]) :- X = < Y, sortcresc([Y | Rest]).
?- sortcresc([1, 2, 3, 4]).
yes
?- sortcresc([1, 3, 5, 4]).
no
?- sortcresc([ ]).
yes

Dacă se consideră că lista vidă nu este o listă sortată crescător atunci se poate
elimina faptul:
sortcresc([ ]).

din definiţia predicatului sortcresc, comportarea lui pentru liste diferite de lista vidă
rămânând aceeaşi.
Observaţii:
• Exemplul 3 pune în evidenţă o facilitate deosebit de interesantă a limbajului Prolog,
respectiv puterea generativă a limbajului. Predicatul membru poate fi utilizat atât
pentru testarea apartenenţei unui element la o listă, cât şi pentru generarea, pe rând,

20
a elementelor unei liste prin resatisfacere succesivă. În anumite contexte de utilizare
această facilitate poate fi folositoare, iar în altele ea poate genera efecte nedorite
atunci când predicatul membru este utilizat în definirea altor predicate, aşa cum se
va arăta în partea a doua a lucrării.
• Aceeaşi capacitate generativă a limbajului Prolog poate fi observată şi în Exemplul
4 unde în funcţie de combinaţiile de argumente instanţiate şi neinstanţiate,
predicatul conc poate produce rezultate relativ diferite.
• La definirea unui predicat p care va fi utilizat în definirea altor predicate trebuie
întotdeauna să se analizeze numărul de soluţii şi soluţiile posibile ale predicatului p.
Acest lucru este necesar deoarece dacă p apare într-o conjuncţie de scopuri
p1,…, pi-1, p ,pi+1,…, pn şi unul dintre scopurile pi+1,…, pn eşuează, mecanismul de
backtracking din Prolog va încerca resatisfacerea scopului p. Numărul de soluţii şi
soluţiile scopului p influenţează astfel, în mod evident, numărul de soluţii şi
soluţiile conjuncţiei de scopuri p1,…, pi-1, p ,pi+1,…, pn. Exemple în acest sens şi
modalităţi de reducere a numărului total de soluţii ale unui corp vor fi prezentate în
Secţiunea 4.2.

3 Limbajul Prolog şi logica cu predicate de ordinul I


Limbajul Prolog este un limbaj de programare logică. Deşi conceput iniţial pentru
dezvoltarea unui interpretor de limbaj natural, limbajul s-a impus ca o soluţie practică de
construire a unui demonstrator automat de teoreme folosind rezoluţia. Demonstrarea
teoremelor prin metoda rezoluţiei necesită ca axiomele şi teorema să fie exprimate în forma
clauzală, adică o disjuncţie de literali, unde un literal este un predicat sau un predicat negat.
Pentru detalii despre noţiunea de clauză şi modul de transformare a unei formule din logica
cu predicate de ordinul I în formă clauzală se poate consulta [Flo93]. Sintaxa şi semantica
limbajului Prolog permit utilizarea numai a unei anumite forme clauzale a formulelor bine
formate: clauze Horn distincte.
Deoarece faptele şi regulile Prolog sunt în formă clauzală, forma particulară a
clauzelor fiind clauze Horn distincte, ele se mai numesc şi clauze Prolog.
Definiţie. Se numeşte clauză Horn o clauză care conţine cel mult un literal pozitiv. O
clauză Horn poate avea una din următoarele patru forme:
(1) o clauză unitară pozitivă formată dintr-un singur literal pozitiv (literal
nenegat);
(2) o clauză negativă formată numai din literali negaţi;
(3) o clauză formată dintr-un literal pozitiv şi cel puţin un literal negativ, numită şi
clauză Horn mixtă;

21
(4) clauză vidă.
Definiţie. Se numeşte clauză Horn distinctă o clauză care are exact un literal pozitiv, ea
fiind fie o clauză unitară pozitivă, fie o clauză Horn mixtă.
Clauzele Horn unitare pozitive se reprezintă în Prolog prin fapte, iar clauzele Horn
mixte prin reguli. O clauză Horn mixtă de forma:
∼S1 ∨ ∼S2 ∨…∨ ∼Sn ∨ S
se exprimă în Prolog prin regula:
S :- S1, S2, …Sn.
Semnificaţia intuitivă a unei reguli Prolog are un corespondent clar în logica cu
predicate de ordinul I dacă se ţine cont de faptul că o clauză Horn mixtă poate proveni din
următoarea formulă bine formată:
S1 ∧ S2 ∧…∧ Sn → S
Variabilele din clauzele distincte se transformă în variabile Prolog, constantele din
aceste formule în constante Prolog, iar funcţiile pot fi asimilate cu structuri Prolog. Deci
argumentele unui predicat Prolog au forma termenilor din calculul cu predicate de ordinul I.
Exemple:
1. Fie următoarele enunţuri: Orice sportiv este puternic. Oricine este inteligent şi
puternic va reuşi în viaţă. Oricine este puternic va reuşi în viaţă sau va ajunge
bătăuş. Există un sportiv inteligent. Gelu este sportiv. Exprimând enunţurile în
logica cu predicate de ordinul I se obţin următoarele formule bine formate:
A1. (∀x) (sportiv(x) → puternic(x))
A2. (∀x) (inteligent(x) ∧ puternic(x) → reuşeşte(x))
A3. (∀x) (puternic(x) → (reuşeşte(x) ∨ bătăuş(x)))
A4. (∃x) (sportiv(x) ∧ inteligent(x))
A5. Sportiv(gelu)
Axiomele se transformă în forma clauzală şi se obţin următoarele clauze:
C1. ∼ sportiv(x) ∨ puternic(x)
C2. ∼ inteligent(x) ∨ ~ puternic(x) ∨ reuseste(x)
C3. ~ puternic(x) ∨ reuseste(x) ∨ bataus(x)
C4. sportiv(a)
C4'. inteligent(a)
C5. sportiv(gelu)

22
Clauzele C1, C2, C4, C4' şi C5 pot fi transformate în Prolog deoarece sunt clauze
Horn distincte, dar clauza C3 nu poate fi transformată în Prolog. Programul Prolog
care se obţine prin transformarea acestor clauze este următorul:
puternic(X) :- sportiv(X).
reuseste(X) :- inteligent(X), puternic(X).
sportiv(a).
inteligent(a).
sportiv(gelu).

2. Fie următoarele enunţuri:


(a) Orice număr raţional este un număr real.
(b) Există un număr prim.
(c) Pentru fiecare număr x există un număr y astfel încât x < y .
Dacă se notează cu prim(x) “x este număr prim”, cu rational(x) ”x este număr
raţional”, cu real(x) “x este număr real” şi cu mai_mic(x, y) “x este mai mic decât
y“, reprezentarea sub formă de formule bine formate în calculul cu predicate de
ordinul I este următoarea:
A1. (∀x) (raţional(x) → real(x))
A2. (∃x) prim(x)
A3. (∀x) (∃y) mai_mic(x, y)
Reprezentarea în formă clauzală este:
C1. ~ rational(x) ∨ real(x)
C2. prim(a)
C3. mai_mic(x, mai_mare(x))
unde mai_mare(x) este funcţia Skolem care înlocuieşte variabila y cuantificată
existenţial. Forma Prolog echivalentă a acestor clauze este:
real(X) :- rational(X).
prim(a).
mai_mic(X, mai_mare(X)).

unde mai_mare(x) este o structură Prolog.


Este evident că nu orice axiomă poate fi transformată în Prolog şi că, dintr-un anumit
punct de vedere, puterea expresivă a limbajului este inferioară celei a logicii cu predicate de
ordinul I. Pe de altă parte, limbajul Prolog oferă o mulţime de predicate de ordinul II, adică
predicate care acceptă ca argumente alte predicate Prolog, care nu sunt permise în logica cu
predicate de ordinul I. Acest lucru oferă limbajului Prolog o putere de calcul superioară
23
celei din logica clasică. Uneori, aceste predicate de ordinul II existente în Prolog pot fi
folosite pentru a modela versiuni de programe Prolog echivalente cu o mulţime de axiome
care nu au o reprezentare în clauze Horn distincte.
Se propune cititorului, după parcurgerea secţiunii următoare, să revină la Exemplul 1
şi să încerce găsirea unei forme Prolog relativ echivalente (cu un efect similar) cu clauza
C3.
Limbajul Prolog demonstrează scopuri (teoreme) prin metoda respingerii rezolutive
[Flo93]. Strategia rezolutivă utilizată este strategia de intrare liniară, strategie care nu este în
general completă dar este completă pentru clauze Horn. Această strategie este deosebit de
eficientă din punct de vedere al implementării şi jutifică astfel forma restricţionată a
clauzelor Prolog.

4 Structura de control a limbajului Prolog


În această secţiune se prezintă în detaliu structura de control specifică sistemului Prolog.
Spre deosebire de limbajele de programare clasice, în care programul defineşte integral
structura de control şi fluxul de prelucrări de date, în Prolog există un mecanism de control
predefinit.
4.1 Semnificaţia declarativă şi procedurală a programelor Prolog
Semnificaţia declarativă a unui program Prolog se referă la interpretarea strict logică a
clauzelor acelui program, rezultatul programului fiind reprezentat de toate consecinţele
logice ale acestuia. Semnificaţia declarativă determină dacă un scop este adevărat (poate fi
satisfăcut) şi, în acest caz, pentru ce instanţe de variabile este adevărat scopul. Se
reaminteşte că o instanţă a unei clauze este clauza de bază (clauza fără variabile) obţinută
prin instanţierea variabilelor din clauza iniţială. În aceste condiţii semnificaţia declarativă a
unui program Prolog se poate defini precum urmează:
Definiţie. Un scop S este adevărat într-un program Prolog, adică poate fi satisfăcut sau
derivă logic din program, dacă şi numai dacă:
1. există o clauză C a programului;
2. există o instanţă I a clauzei C astfel încât:
2.1. antetul lui I să fie identic cu cel al lui S;
2.2. toate scopurile din corpul lui I sunt adevărate, deci pot fi satisfăcute.
Observaţii:
• În definiţia de mai sus clauzele referă atât fapte cât şi reguli Prolog. Antetul unei
clauze este antetul regulii dacă clauza este o regulă Prolog (clauză Horn mixtă) şi

24
este chiar faptul dacă clauza este un fapt Prolog (clauză unitară pozitivă). Corpul
unui fapt este considerat vid şi un fapt este un scop care se îndeplineşte întotdeauna.
• În cazul în care întrebarea pusă sistemului Prolog este o conjuncţie de scopuri,
definiţia anterioară se aplică fiecărui scop din conjuncţie.
Semnificaţia procedurală a unui program Prolog se referă la modul în care sistemul
încearcă satisfacerea scopurilor, deci la strategia de control utilizată. Diferenţa dintre
semnificaţia declarativă şi semnificaţia procedurală este aceea că cea de a doua defineşte, pe
lânga relaţiile logice specificate de program, şi ordinea de satisfacere a scopurilor şi
subscopurilor. În prima secţiune a acestui capitol s-a făcut o prezentare informală a
modalităţii procedurale de satisfacere a scopurilor în Prolog. În continuare se rafinează
această comportare. Din punct de vedere procedural, un program Prolog poate fi descris de
schema bloc prezentată în figura 3.
program Prolog

conjuncþii Execuþie indicator SUCCES/INSUCCES


de scopuri sistem Prolog instanþele variabilelor din scopuri

Figura 3. Comportarea procedurală a sistemului Prolog


Semnificaţia procedurală a unui program Prolog poate fi descrisă de următorul
algoritm în care L = {S1, S2, …, Sn} este lista de scopuri de satisfăcut, iar B este lista de
instanţieri (unificări) ale variabilelor din scopuri, iniţial vidă. Această listă se va actualiza la
fiecare apel.
Algoritm. Strategia de control Prolog
SATISFACE(L,B)
1. dacă L = {} % lista vidă
atunci afişează B şi întoarce SUCCES.
2. Fie S1 ← primul scop din L şi p predicatul referit de S1. Parcurge clauzele programului,
de la prima clauză sau de la ultimul marcaj fixat, asociat lui p, până ce se găseşte o
clauză C al cărei antet unifică cu S1.
3. dacă nu există o astfel de clauză
atunci întoarce INSUCCES.
4. Fie C de forma H :- D1,…,Dm, m ≥ 0. Plasează un marcaj în dreptul clauzei C, asociat
lui p. (H conţine predicatul p).
5. Redenumeşte variabilele din C şi obtine C' astfel încât să nu existe nici o variabilă
comună între C' şi L; C' este de tot forma H :- D1,…,Dm. cu redenumirile făcute.
6. L’ ← { D1,…,Dm, S2,…,Sn } % dacă C este fapt, atunci L se va reduce
25
7. Fie B1 instanţierea variabilelor care rezultă din unificarea lui S1 cu H.

8. Substituie variabilele din L’ cu valorile date de B1 şi obţine:


L” ← { D1’,…,Dm’, S2’,…,Sn’ }.
9. B ← B ∪ B1.
10. dacă SATISFACE(L”, B)=SUCCES
atunci afişează B şi întoarce SUCCES.
11. repetă de la 1.
sfârşit
Observaţii:
• Algoritmul de mai sus reprezintă de fapt implementarea strategiei rezolutive de
intrare liniară utilizată de limbajul Prolog, pentru care se impune o ordine
prestabilită de considerare a clauzelor.
• Algoritmul arată funcţionarea sistemului pentru găsirea primei soluţii.

• Indicatorul SUCCES/INSUCCES corespunde răspunsurilor de yes, respectiv no,


date de sistemul Prolog la încercarea de satisfacere a unei liste de scopuri.
Următoarele două exemple pun în evidenţă diferenţa dintre semnificaţia declarativă şi
semnificaţia procedurală a programelor Prolog. Primul exemplu este un scurt program care
defineşte relaţiile de părinte şi strămoş existente între membrii unei familii. Se dau patru
definiţii posibile ale relaţiei de strămoş, str1, str2, str3 şi str4, toate fiind perfect corecte
din punct de vedere logic, deci din punct de vedere a semnificaţiei declarative a limbajului.
% parinte(IndividX, IndividY)
% stramos(IndividX, IndividZ)
parinte(vali, gelu).
parinte(ada, gelu).
parinte(ada, mia).
parinte(gelu, lina).
parinte(gelu, misu).
parinte(misu, roco).

str1(X, Z) :- parinte(X, Z).


str1(X, Z) :- parinte(X, Y), str1(Y, Z).

% Se schimbă ordinea regulilor:


str2(X, Z) :- parinte(X, Y), str2(Y, Z).
str2(X, Z) :- parinte(X, Z).

26
27
% Se schimbă ordinea scopurilor în prima variantă:
str3(X, Z) :- parinte(X, Z).
str3(X, Z) :- str3(X, Y), parinte(Y, Z).

% Se schimbă atât ordinea regulilor, cât şi ordinea scopurilor:


str4(X, Z) :- str4(X, Y), parinte(Y, Z).
str4(X, Z) :- parinte(X,Z).

vali ada

gelu mia

lina mişu

roco

Figura 4. Arborele genealogic definit de programul Prolog


Figura 4 prezintă arborele genealogic definit de faptele Prolog anterioare. În raport cu
semantica declarativă a limbajului se pot schimba, fără a modifica înţelesul logic, atât
ordinea clauzelor care definesc relaţia de strămoş, cât şi ordinea scopurilor în corpul regulii
de aflare a strămoşilor. Schimbând ordinea clauzelor se obţine din predicatul str1 definiţia
alternativă a relaţiei de strămoş str2; schimbând ordinea scopurilor din corpul regulii în
varianta iniţială se obţine definiţia predicatului str3; şi, schimbând atât ordinea clauzelor,
cât şi ordinea scopurilor în regulă, se obţine predicatul str4.
Comportarea programului folosind cele patru definiţii alternative, dacă se doreşte
aflarea adevărului relaţiei de strămoş pentru perechile (ada, mişu) şi (mia, roco), este cea
prezentată în continuare.
?- str1(ada, misu).
yes

Pentru acest scop, arborele de deducţie este prezentat în figura 5.

28
str1(ada, misu)

parinte(ada, misu) parinte(ada, Y), str1(Y, misu)

INSUCCES Y=gelu str1(gelu, misu)


parinte(ada, gelu)
parinte(gelu, misu)
SUCCES
SUCCES
Figura 5. Arborele de deducţie a scopului str1(ada, misu)
?- str2(ada, misu).
yes
?- str3(ada, misu).
yes

Pentru scopul str3(ada, misu), arborele de deducţie este cel prezentat în figura 6.
str3(ada, misu)

parinte(ada, misu) str3(ada, Y), parinte(Y, misu)

INSUCCES Y=Y' parinte(gelu, misu)


parinte(ada, Y')
SUCCES
Y'=gelu
parinte(ada, gelu)

SUCCES

Figura 6. Arborele de deducţie a scopului str3(ada, misu)


?- str4(ada, misu).
% buclă infinita; eventual mesaj de depăşire a stivei

Pentru acest scop, arborele de deducţie este cel prezentat în figura 7.

29
str4(ada, misu)

str4(ada, Y), parinte(Y, misu)

str4(ada, Y'), parinte(Y', Y)

str4(ada, Y"), parinte(Y", Y)

...
arbore infinit
Figura 7. Arborele de deducţie (infinit) a scopului str4(ada, misu)
Din punctul de vedere al semnificaţiei procedurale, cele patru definiţii nu sunt
echivalente. Primele două, str1 şi str2, pot da răspuns pozitiv sau negativ la orice întrebare,
dar str2 este mai ineficientă decât str1. Cititorul poate încerca trasarea arborelui de deducţie
al satisfacerii scopului str2(ada, misu) şi compararea acestuia cu cel corespunzător
satisfacerii scopului str1(ada, misu). Definiţia str4 produce o buclă infinită datorită intrării
infinite în recursivitate. Este evident o definiţie greşită din punct de vedere procedural.
Definiţia relaţiei de strămoş str3 este o definiţie "capcană". După cum se poate observă din
arborele de deducţie prezentat, răspunsul sistemului este afirmativ în cazul în care există o
relaţie de strămoş între cele două persoane argumente ale predicatului. În cazul în care o
astfel de relaţie nu există, sistemul intră într-o buclă infinită, cum ar fi, de exemplu, pentru
persoanele mia şi roco.
?- str1(mia, roco).
no
?- str2(mia, roco).
no
?- str3(mia, roco).
% buclă infinită; eventual mesaj de depăşire a stivei

În acest caz, arborele de deducţie al scopului str3(mia, roco) este prezentat în figura 8.
Datorită semnificaţiei procedurale a limbajului Prolog trebuie urmărit cu atenţie modul de
definire a unui predicat atât din punctul de vedere al ordinii clauzelor, cât şi din punctul de
vedere al ordinii scopurilor în corpul regulilor.

30
str3(mia, roco)

parinte(mia, roco) str3(mia, Y), parinte(Y, roco)

INSUCCES parinte(mia, Y) str3(mia, Y'), parinte(Y', Y)

INSUCCES parinte(mia, Y') str3(mia, Y"), parinte(Y", Y')

INSUCCES parinte(mia, Y") ...


arbore infinit
INSUCCES
Figura 8. Arbore de deducţie infinit pentru un scop fals
Al doilea exemplu se referă la rezolvarea în Prolog a următoarei probleme. O maimuţă
se găseşte la uşa unei camere şi o banană se află agăţată de plafon în centrul camerei. Lângă
fereastra camerei se află o cutie pe care maimuţa o poate folosi pentru a se urca pe ea şi a
ajunge la banană. Maimuţa ştie să facă următoarele mişcări: să meargă pe sol, să se urce pe
cutie, să deplaseze cutia dacă este lângă cutie, să apuce banana dacă este pe cutie şi cutia
este în centrul camerei. Se cere să se scrie un program Prolog care să descrie această
problema şi care să poată răspunde dacă, dintr-o configuraţie iniţială specificată prin poziţia
maimuţei şi a cubului, maimuţa poate apuca banana după execuţia unei secvenţe de mişcări.
Reprezentarea universului problemei în Prolog este specificată după cum urmează.
Starea iniţială este:
(1) Maimuţa este la uşă.
(2) Maimuţa este pe sol.
(3) Cutia este la fereastră.
(4) Maimuţa nu are banana.
şi poate fi descrisă prin structura Prolog:
stare(la_usa, pe_sol, la_fereastra, nu_are_banana)

Starea finală este aceea în care maimuţa are banana


stare( _ , _ , _ , are_banana)

Mişcările cunoscute de maimuţă, deci operatorii de tranziţie dintr-o stare în alta, sunt:
(m1) Apucă banana. apucă
(m2) Urcă pe cutie. urcă
(m3) Mută cutia. mută(Poziţia1, Poziţia2)

31
(m4) Merge (maimuţa se deplasează pe sol). merge(Poziţia1, Poziţia2)
şi sunt reprezentate prin structurile Prolog indicate la dreapta mişcărilor.
Dintr-o anumită stare numai anumite mişcări sunt permise. De exemplu, maimuţa nu
poate apuca banana decât dacă este urcată pe cutie şi cutia este în centrul camerei, adică sub
banană. Mişcările permise pot fi reprezentate în Prolog prin predicatul de deplasare depl cu
trei argumente:
depl(Stare1, Mişcare, Stare2)
Mişcare
Stare1 Stare2
care transformă problema din starea Stare1 în starea Stare2 prin efectuarea mişcării legale
Mişcare în starea Stare1. Se observă că reprezentarea aleasă este o reprezentare a
problemei folosind spaţiul stărilor [Flo93].
Soluţia problemei este completată prin adăugarea predicatului poatelua(Stare) care va
reuşi dacă maimuţa poate ajunge din starea iniţială Stare într-o stare finală în care poate lua
banana, stare finală descrisă de:
poate_lua(stare( _ , _ , _ , are_banana)).

Programul Prolog complet este prezentat în continuare.


% Structura stare:
% stare(poz_o_maimuta, poz_v_maimuta, poz_cub, are_nu_are_banana)
% Mişcări admise: apucă, urca, muta(Pozitia1, Pozitia2), merge(Pozitia1, Pozitia2),
% reprezentate tot prin structuri.
% Predicate:
% depl(Stare1, Miscare, Stare2)
% poate_lua(Stare)
depl(stare(la_centru, pe_cutie, la_centru, nu_are_banana),
apuca, stare(la_centru, pe_cutie, la_centru, are_banana)).
depl(stare(P, pe_sol, P, H), urca, stare(P, pe_cutie, P, H)).
depl(stare(P1, pe_sol, P1, H), muta(P1, P2), stare(P2, pe_sol, P2, H)).
depl(stare(P1, pe_sol, B, H), merge(P1, P2), stare(P2, pe_sol, B, H)).
poate_lua(stare( _ , _ , _ , are_banana)).
poate_lua(Stare1) :- depl(Stare1, Miscare, Stare2), poate_lua(Stare2).

La întrebarea
?- poate_lua(stare(la_usa, pe_sol, la_fereastra, nu_are_banana)).
yes

sistemul răspunde afirmativ: maimuţa este fericită şi mănâncă banana.

32
O analiză atentă a programului conduce la concluzia că programul dă soluţii numai
pentru anumite situaţii. În primul rând, strategia de control a mişcărilor maimuţei este
impusă de ordinea clauzelor care definesc predicatul depl. Astfel, maimuţa preferă întâi să
apuce, apoi să urce, apoi să mute cubul şi numai la urmă să mearga prin cameră. Dacă
clauza corespunzătoare mişcării merge(Pozitia1, Pozitia2) ar fi fost pusă ca prima clauză în
definiţia predicatului de deplasare, maimuţa ar fi mers la infinit prin cameră fără să mai
ajungă sa mute cubul sau să apuce banana. Chiar pentru ordinea dată a clauzelor, dacă se
pune întrebarea
?- poate_lua(stare(X, pe_sol, la_fereastra, nu_are_banana)).
deci dacă interesează din ce poziţii maimuţa poate lua banana, rezolvarea dată nu este total
satisfăcătoare deoarece programul are o infinitate de soluţii. La cererea repetată a unei
soluţii, se va afişa întotdeauna valoarea:
X = la_fereastra
Considerând din nou modelul spaţiului stărilor, se observă că în acest caz este vorba de
un spaţiu de căutare de tip graf în care o aceeaşi stare poate fi descoperită şi redescoperită la
infinit prin parcurgerea unui ciclu de tranziţii de stări în acest graf. Aşa cum este arătat în
[Flo93], astfel de cazuri trebuie tratate prin introducerea unor liste de stări parcurse (listele
FRONTIERA şi TERITORIU) care să împiedice parcurgerea repetată a unor stări deja
parcurse.
Pericolul de apariţie a unor cicluri infinite datorită parcurgerii unui spaţiu de căutare
graf nu este specific limbajului Prolog şi poate să apară în orice implementare, în aceste
cazuri. Ceea ce este neobişnuit în raport cu alte limbaje este faptul că semnificaţia
declarativă a programului este corectă, indiferent de ordonarea clauzelor, în timp ce
programul este procedural incorect, având comportări diferite în funcţie de această
ordonare. Rezolvarea problemei ar mai trebui completată cu afişarea stărilor şi a mişcărilor
executate de maimuţă pentru a ajunge în starea finală în care ea poate apuca banana.
Modalităţi de eliminare a ciclurilor infinite de tipul celor ce apar în această problemă vor fi
discutate in partea a doua a lucrării.
4.2 Controlul procesului de backtracking: cut şi fail
Sistemul Prolog intră automat într-un proces de backtraking dacă acest lucru este necesar
pentru satisfacerea unui scop. Acest comportament poate fi în unele situaţii deosebit de util,
dar poate deveni foarte ineficient în alte situaţii. Se consideră următorul exemplu de
program în care se definesc valorile unei funcţii:
f(X, 0) :- X < 3. %1
f(X, 2) :- 3 =< X, X < 6. %2
f(X, 4) :- 6 =< X. %3

La întrebarea:
33
?- f(1, Y).
Y=0

sistemul răspunde indicând că valoarea funcţiei pentru X=1 este Y=0. Dacă se pune
întrebarea formată din conjuncţia de scopuri:
?- f(1, Y), 2 < Y.
no

sistemul semnalează eşec. Arborii de deducţie corespunzători acestor răspunsuri sunt


prezentaţi în figura 9.
f(1,Y) f(1,Y), 2<Y

X=1 X=1 X=1 X=1


Y=0 Y=0 Y=2 Y=4
f(1,0) f(1,0) f(1,2) f(1,4)

1<3 1=<3 2<0 3=<1 6=<1

SUCCES INSUCCES INSUCCES INSUCCES


Figura 9. Arborii de deducţie a scopurilor f(1,Y) şi f(1,Y),2 < Y
Se observă că se încearcă resatisfacerea primului scop cu regulile 2 şi 3, deşi acest
lucru este evident inutil datorită semanticii acestor reguli. Cel care a scris aceste reguli poate
cu uşurinţă constata că, dacă o valoare mai mică decât 3 pentru X duce la eşecul scopului S
din conjuncţia de scopuri f(X, Y), S, este inutil să se încerce resatisfacerea scopului f,
deoarece aceasta nu este posibilă [Bra88]. Se poate împiedica încercarea de resatisfacere a
scopului f(X, Y) prin introducerea predicatului cut.
Predicatul cut, notat cu atomul special !, este un predicat standard, fără argumente,
care se îndeplineşte (este adevărat) întotdeauna şi nu poate fi resatisfăcut.
Predicatul cut are următoarele efecte laterale:
• La întâlnirea predicatului cut toate selecţiile făcute între scopul antet de regulă şi
cut sunt "îngheţate", deci marcajele de satisfacere a scopurilor sunt eliminate, ceea
ce duce la eliminarea oricăror altor soluţii alternative pe aceasta porţiune. O
încercare de resatisfacere a unui scop între scopul antet de regula şi scopul curent va
eşua.
• Dacă clauza în care s-a introdus predicatul cut reuşeşte, toate clauzele cu acelasi
antet cu aceasta, care urmează clauzei în care a apărut cut vor fi ignorate. Ele nu se
mai folosesc în încercarea de resatisfacere a scopului din antetul clauzei care
conţine cut.

34
• Pe scurt, comportarea predicatului cut este următoarea:
(C1) H :- D1, D2, …, Dm, !, Dm+1, …, Dn.
(C2) H :- A1, …, Ap.
(C3) H.
Dacă D1, D2, …, Dm sunt satisfăcute, ele nu mai pot fi resatisfăcute datorită lui cut.
Dacă D1, …, Dm sunt satisfăcute, C2 şi C3 nu vor mai fi utilizate pentru
resatisfacerea lui H. Resatisfacerea lui H se poate face numai prin resatisfacerea
unuia din scopurile Dm+1, …, Dn, dacă acestea au mai multe soluţii.
Utilizând predicatul cut, definiţia funcţiei f(X, Y) poate fi rescrisă mult mai eficient
astfel:
f(X, 0) :- X < 3, !.
f(X, 2) :- 3 =< X, X < 6, !.
f(X, 4) :- 6 =< X.

Predicatul cut poate fi util în cazul în care se doreşte eliminarea unor paşi din deducţie
care nu conţin soluţii sau eliminarea unor căi de căutare care nu conţin soluţii. El permite
exprimarea în Prolog a unor structuri de control de tipul:
dacă condiţie atunci acţiune1
altfel acţiune2

astfel:
daca_atunci_altfel(Cond, Act1, Act2) :- Cond, !, Act1.
daca_atunci_altfel(Cond, Act1, Act2) :- Act2.

Se observă însă că există două contexte diferite în care se poate utiliza predicatul cut:
într-un context predicatul cut se introduce numai pentru creşterea eficienţei programului,
caz în care el se numeşte cut verde; în alt context utilizarea lui cut modifică semnificaţia
procedurală a programului, caz în care el se numeşte cut roşu.
Exemplul de definire a funcţiei f(X, Y) cu ajutorul lui cut este un exemplu de cut
verde. Adăugarea lui cut nu face decât să crească eficienţa programului, dar semnificaţia
procedurală este aceeaşi, indiferent de ordinea în care se scriu cele trei clauze.
Utilizarea predicatului cut în definirea predicatului asociat structurii de control
daca_atunci_altfel introduce un cut roşu deoarece efectul programului este total diferit
dacă se schimbă ordinea clauzelor. Introducerea unui cut roşu modifică corespondenţa
dintre semnificaţia declarativă şi semnificaţia procedurală a programelor Prolog.
Se consideră exemplul de definire a predicatului de aflare a minimului dintre două
numere, în următoarele două variante:

35
min1(X, Y, X) :- X =< Y, !. % cut verde
min1(X, Y, Y) :- X > Y.
min2(X, Y, X) :- X =< Y, !. % cut roşu
min2(X, Y, Y).

În definiţia predicatului min1 se utilizează un cut verde; el este pus pentru creşterea
eficienţei programului, dar ordinea clauzelor de definire a lui min1 poate fi schimbată fără
nici un efect asupra rezultatului programului. În cazul predicatului min2 se utilizează un cut
roşu, asemănător structurii daca_atunci_altfel. Dacă se schimbă ordinea clauzelor de
definire a predicatului min2:
min2(X, Y, Y).
min2(X, Y, X) :- X =< Y, !.

rezultatul programului va fi evident incorect pentru valori X < Y.


În anumite cazuri efectul introducerii predicatului cut poate fi mai subtil. Se consideră
din nou definiţia predicatului membru:
membru(X, [X | _]).
membru(X, [ _ |Y]) :- membru(X, Y).

şi o definiţie alternativă
membru1(X, [X| _]) :- !.
membru1(X, [ _ |Y]) :- membru1(X, Y).

Introducerea predicatului cut în definiţia primei clauze a predicatului membru1 este


justificată datorită creşterii eficienţei programului. Odată ce s-a descoperit că un element
este membru al unei liste, este inutilă încercarea de resatisfacere a scopului. Cu toate acestea
predicatul cut de mai sus nu este un cut verde, deoarece el schimbă comportarea
programului în cazul în care se pun întrebări în care X este variabila neinstanţiată în
predicatele membru şi membru1.
?- membru(X, [a, b, c]).
X = a;
X = b;
X = c;
no

trei soluţii pentru membru

?- membru1(X, [a, b, c]).


X = a;
no
36
o soluţie pentru membru1.
Efectul introducerii predicatului cut asupra semnificaţiei declarative a programelor
Prolog poate fi rezumat astfel:
p :- a, b. % Semnificaţia declarativă este:
p :- c. % (a ∧ b) ∨ c
% indiferent de ordinea clauzelor (în general).
p :- a, !, b. % Semnificaţia declarativă este:
p :- c. % (a ∧ b) ∨ (~ a ∧ c).
p :- c. % Semnificaţia declarativă este:
p :- a, !, b. % c ∨ (a ∧ b).
Oportunitatea utilizării unui cut roşu sau a unui cut verde este, dintr-un anumit punct
de vedere, similară cu cea a utilizării sau nu a salturilor în limbajele de programare clasică.
Dacă s-ar rescrie predicatul daca_atunci_altfel folosind un cut verde, definiţia lui ar fi:
daca_atunci_altfel(Cond, Act1, Act2) :- Cond, !, Act1.
daca_atunci_altfel(Cond, Act1, Act2) :- not (Cond), !, Act2.

unde predicatul not(Cond) este satisfăcut dacă scopul Cond nu este satisfăcut. O astfel de
definiţie implică evaluarea lui Cond de două ori şi, dacă Cond se defineşte ca o conjuncţie
de scopuri, posibil sofisticate, atunci ineficienţa utilizării unui cut verde în acest caz este
evidentă. De la caz la caz, programatorul în Prolog trebuie să aleagă între claritate, deci
păstrarea corespondenţei semnificaţiei declarative cu cea procedurală, şi eficienţă.
În multe situaţii interesează atât condiţiile de satisfacere a scopurilor, cât şi condiţiile
de nesatisfacere a acestora, deci de eşec. Fie următoarele fapte Prolog:
bun(gelu).
bun(vlad).
bun(mihai).

Pe baza acestor fapte se poate obţine un răspuns afirmativ despre bunătatea lui Gelu,
Vlad şi Mihai şi un răspuns negativ dacă se întreabă despre bunătatea oricărei alte persoane.
Din acest exemplu se poate vedea că limbajul Prolog foloseşte un model de raţionament
minimal numit ipoteza lumii închise. Conform acestui model, tot ceea ce nu este ştiut de
program, deci afirmat explicit ca adevărat în program, este considerat fals.
Limbajul Prolog permite exprimarea directă a eşecului unui scop cu ajutorul
predicatului fail. Predicatul fail este un predicat standard, fără argumente, care eşueaza
întotdeauna. Datorită acestui lucru introducerea unui predicat fail într-o conjuncţie de
scopuri, de obicei la sfârşit, căci după fail tot nu se mai poate satisface nici un scop,
determină intrarea în procesul de backtracking.

37
Dacă fail se întâlneste după predicatul cut, nu se mai face backtracking. Enunţul "Un
individ este rău dacă nu este bun." se poate exprima astfel:
bun(gelu).
bun(vlad).
bun(mihai).
rau(X) :- bun(X), !, fail.
rau(X).
?- rau(gelu).
no
?- rau(mihai).
no
?- rau(petru).
yes

Dacă predicatul fail este folosit pentru a determina eşecul, cum este cazul exemplului
de mai sus, este de obicei precedat de predicatul cut, deoarece procesul de backtracking pe
scopurile care îl preced este inutil, scopul eşuând oricum datorită lui fail.
Există cazuri în care predicatul fail este introdus intenţionat pentru a genera procesul
de backtracking pe scopurile care îl preced, proces interesant nu din punctul de vedere al
posibilităţii de resatisfacere a scopului ce conţine fail, ci din punctul de vedere al efectului
lateral al acestuia.
rosu(mar).
rosu(cub).
rosu(soare).
afisare(X) :- rosu(X), write(X), fail.
afisare( _ ).

Scopul afisare(X) va afişa toate obiectele roşii cunoscute de programul Prolog datorită
procesului de backtracking generat de fail; în acest fel s-a relizat prin fail o iteraţie peste
faptele rosu( ). Clauza afisare( _ ) este adăugată pentru ca răspunsul final la satisfacerea
scopului să fie afirmativ. În acest caz, introducerea unui cut înainte de fail ar fi determinat
numai afişarea primului obiect roşu din program.
Revenind la combinaţia !,fail, se consideră în continuare implementarea în Prolog a
afirmaţiei: "Mihai iubeşte toate sporturile cu excepţia boxului". Această afirmaţie poate fi
exprimată în pseudocod sub forma:

dacă X este sport şi X este box


atunci Mihai iubeşte X este fals
altfeldacă X este sport
38
atunci Mihai iubeşte X este adevărat

şi tradusă în Prolog astfel:


iubeste(mihai, X) :- sport(X), box(X), !, fail.
iubeste(mihai, X) :- sport(X).

Predicatul cut utilizat aici este un cut roşu. Combinaţia !, fail este deseori utilizată în
Prolog şi are rolul de negaţie. Se mai spune că limbajul Prolog modelează negaţia ca eşec al
satisfacerii unui scop (negaţia ca insucces), aceasta fiind de fapt o particularizare a ipotezei
lumii închise. Combinaţia !, fail este echivalentă cu un predicat standard existent în Prolog,
predicatul not. Predicatul not admite ca argument un predicat Prolog şi reuşeşte dacă
predicatul argument eşuează. Utilizând acest predicat, ultimul exemplu dat se poate exprima
în Prolog astfel:
iubeste(mihai, X) :- sport(X), not(box(X)).
iubeste(mihai, X) :- sport(X).

Un alt predicat standard este predicatul call, care admite ca argument un predicat
Prolog şi are ca efect încercarea de satisfacere a predicatului argument. Predicatul call
reuşeşte dacă predicatul argument reuşeşte şi eşuează în caz contrar. Utilizând acest
predicat, se poate explicita efectul general al predicatului standard not în următorul mod:
not(P) :- call(P), !, fail.
not(P).

Atât predicatul not cât şi predicatul call sunt predicate de ordinul II în Prolog deoarece
admit ca argumente alte predicate. Secţiunea următoare va prezenta mai multe astfel de
predicate.
4.3 Predicate predefinite ale limbajului Prolog
Predicatele prezentate până acum, cu excepţia predicatului call, sunt predicate bazate pe
logica cu predicate de ordinul I, modelul Prolog cu semnificaţia declarativă a programelor
urmărind îndeaproape modelul logic. În continuare se prezintă o serie de predicate Prolog
care nu mai pot fi asimilate cu logica cu predicate de ordinul I, respectiv: predicate ce decid
asupra caracterului unor argumente, predicate de control a fluxului de execuţie a
programului şi predicate de ordinul II, numite uneori şi metapredicate. Metapredicatele
acceptă ca argumente alte predicate Prolog, fapte sau reguli, şi sunt interesante în special
prin efectul lateral pe care îl produc. Toate tipurile de predicate menţionate reprezintă
extinderea modelului programării logice cu tehnici de programare care cresc puterea de
calcul a limbajului. Majoritatea predicatelor prezentate în continuare sunt standard şi se
găsesc predefinite în orice implementare, o parte sunt specifice mediului ARITY Prolog,
acestea fiind indicate ca atare. O implementare particulară poate conţine şi alte predicate
predefinite suplimentare celor prezentate aici.
39
(a) Predicate de clasificare a termenilor
• var(X)
Predicatul var(X) reuşeşte dacă X este o variabilă neinstanţiată şi eşuează în cazul în
care X este o variabila instanţiată sau o constantă.
Exemple:
?- var(5).
no
?- var(mihai).
no
?- var(X).
yes

• novar(X)
Predicatul novar(X) este adevărat dacă X nu este o variabilă neinstanţiată. Acest
predicat este opusul prdicatului var(X) şi s-ar putea defini astfel:
novar(X) :- var(X), !, fail.
novar( _ ).

• atom(X)
Predicatul atom(X) reuşeşte dacă X este un atom Prolog şi eşuează în caz contrar.
Exemple:
?- atom(coco).
yes
?- atom(100).
no
?- atom(carte(prolog, clocksin)).
no

• integer(X)
Predicatul integer(X) reuşeşte dacă X este un număr întreg.
Se consideră urmatorul exemplu de program Prolog care transformă o expresie
reprezentată printr-o sumă de variabile şi constante într-o expresie care conţine suma
variabilelor plus o constantă care reprezintă suma tuturor constantelor din expresie [CM84].
Predicatul simplif are două argumente: primul argument este forma iniţială a expresiei,
argument instanţiat, iar cel de al doilea reprezintă expresia simplificată, sintetizată de
program.
% simplif(Expresie_iniţială, Expresie_simplificată)
40
% Predicat ajutator simpl
% simpl(Ex_iniţială, Suma_acumulată, Ex_simplificată).
simplif(Expr, Expr1) :- simpl(Expr, 0, Expr1).
simpl(Expr + A, N, Expr1) :-
integer(A), !,
N1 is N + A,
simpl(Expr, N1, Expr1).
simpl(Expr + A, N, Expr1 + A) :-
atom(A), !,
simpl(Expr, N, Expr1).
simpl(Rest, N, N1) :-
integer(Rest), !,
N1 is Rest + N.
simpl(Rest, 0, Rest) :- !.
simpl(Rest, N, N + Rest).

La întrebarea
?- simplif(x + 40 + y + 1 + 55 + x + 2, E).

programul răspunde
E = 98 + x + y + x
În programul de mai sus definirea predicatului simplif s-a făcut pe baza unui predicat
ajutător simpl care conţine un argument suplimentar. Acest argument, instanţiat la 0 în
cazul primului apel, este folosit ca o variabilă de acumulare pentru calculul sumei
constantelor întregi din expresie.
• atomic(X)
Pe baza predicatelor atom(X) şi integer(X) se poate defini predicatul atomic(X) care
este adevărat dacă X este fie întreg, fie atom Prolog:
atomic(X) :- atom(X).
atomic(X) :- integer(X).

De multe ori predicatul atomic(X) este predefinit în sistem.

41
(b) Predicate de control
Predicatul true reuşeşte întotdeauna, iar fail eşuează întotdeauna.
Predicatul repeat simulează structurile de control repetitive din limbajele clasice de
programare. El are o infinitate de soluţii, putându-se resatisface de oricâte ori este nevoie,
fără a umple stiva. Definiţia lui ar putea fi:
repeat.
repeat :- repeat.
Acest predicat este predefinit in mediul ARITY. În ARITY Prolog există şi alte
predicate predefinite care simulează structurile de control din programarea procedurală:
ifthen(+Conditie, +Scop), pentru dacă-atunci; ifthenelse(+Conditie, +Scop-then, +Scop-
else), pentru dacă-atunci-altfel; şi case([Conditie1 -> Scop1, … Conditien -> Scopn | Scop-
default), sau case([Conditie1 -> Scop1, … Conditien -> Scopn), pentru case.
O predicat echivalent ciclului for, care nu este prefedinit în ARITY, se poate
implementa astfel:
% for(Variabla, ValoareInitiala, ValoareFinala, Pas)
for(I, I, I, 0).
for( _, _ , _ , 0) :- !, fail.
for(I, I, F, S) :-
S > 0, (I > F, !, fail ; true)
; S < 0, (I < F, !, fail ; true).
for(V, I, F, S) :- I1 is I + S, for(V, I1, F, S).
Exemple de apeluri:
a :- for(X, 10, -10, -3.5), write(X), tab(2), fail ; true.
b :- for(X, -10, 10, 3.5), write(X), tab(2), fail ; true.
c :- for(X, 10, 10, 0), write(X), tab(2), fail ; true.
?- a.
10 6.5 3.0 -0.5 -4.0 -7.5
yes
?- b.
-10 -6.5 -3.0 0.5 4.0 7.5
yes
?- c.
10
yes
Predicatul for eşuează pentru apeluri incorecte (cicluri infinite) de genul:
for(X, -10, 10, 0).
for(X, 10, -10, 2).
for(X, -10, 10, -2).

42
(c) Predicate de tratare a clauzelor drept termeni
Predicatele din această categorie permit: construirea unei structuri care reprezintă o
clauză în baza de cunoştinte, identificarea clauzelor din program, adăugarea sau eliminarea
unor clauze, reprezentate printr-o structură, în baza de cunoştinţe a sistemului.
• clause(Antet, Corp)
Satisfacerea scopului clause(Antet, Corp) necesită unificarea argumentelor Antet şi
Corp cu antetul şi corpul unei clause existente în baza de cunoştinţe Prolog. La apelul
acestui predicat, Antet trebuie să fie instanţiat astfel încât predicatul principal al clauzei să
fie cunoscut. Dacă nu există clauze pentru acest predicat, scopul eşuează. Dacă există mai
multe clauze care definesc predicatul Antet, scopul clause(Antet, Corp) va avea mai multe
soluţii, fiecare soluţie corespunzând unei clauze de definire a predicatului Antet. Dacă o
clauză este fapt, Corp se instanţiază la constanta true.
Exemplu. Considerând definiţia predicatului de concatenare a două liste ca fiind deja
introdusă în baza de cunoştinţe Prolog:
% conc(Lista1, Lista2, ListaRez)
conc([], L, L).
conc([Prim|Rest1], L2, [Prim|Rest3]) :- conc(Rest1, L2, Rest3).

se obţin următoarele rezultate:


?- clause(conc(A, B, C), Corp).
A = [ ], B = _004C, C = _004C, Corp = true;
A = [_00F0|_00F4], B = _004C, C = [_00F0|_00FC],
Corp = conc (_00F4, _004C, _00FC);
no

Utilizând predicatul clause se poate defini un predicat listc de afişare a clauzelor care
definesc un predicat:
% listc(Antet) - afişează toată clauzele din baza de cunoştinţe
% care definesc scopul Antet
listc(Antet) :-
clause(Antet, Corp),
afis(Antet, Corp),
write('.'), nl, fail.
listc( _ ).

afis(Antet, true) :- !, write(Antet). %1


afis(Antet, Corp) :- write(Antet), write(':'), write('-'), write(Corp).

Efectul acestui program, coinsiderând definiţia anterioară a scopului conc, este următorul:
43
?- listc(conc(_, _, _)).
conc([], _0034, _0034).
conc([_0108|_010C], _0034, [_0108|_0114]:-conc(_010C, _0034, _0114).

În definirea primei clauze a predicatului afis utilizarea predicatului cut este esenţială
deoarece afişarea clauzelor este bazată pe procesul de backtracking generat intenţionat de
introducerea predicatului fail în prima clauză a predicatului listc. Regula %1 din predicatul
afis este necesară deoarece faptele sunt reguli având corpul format din scopul true. Această
reprezentare ne arată că faptele şi regulile au aceeaşi reprezentare în Prolog (toate reprezintă
clauze).
Exemplu. Pentru următorul predicat:
p(1).
p(X) :- X > 2, X < 5.
p(X).
predicatul clause dă următoarele răspunsuri:
?- clause(p(X), Corp).
X = 1, Corp = true ;
X = _0038, Corp = _0038 > 2 , _0038 < 5 ;
X = _0038, Corp = true.
• asserta(Clauza), assertz(Clauza), assert(Clauza),
Predicatul asserta(Clauza) reuşeşte întotdeauna o singură dată şi are ca efect lateral
adăugarea clauzei Clauza la începutul bazei de cunoştinţe Prolog. Predicatele
assertz(Clauza) şi assert(Clauza) au o comportare similară, cu excepţia faptului că
argumentul Clauza este adăugat la sfârşitul bazei de cunoştinţe. Argumentul Clauza trebuie
să fie instanţiat la o valoare ce reprezintă o clauză Prolog. Acţiunea de adăugare a unei
clauze prin asserta, assert sau assertz nu este eliminată de procesul de backtracking.
Clauza adăugată va fi eliminată numai în urma unei cereri explicite prin intermediul
predicatului retract.
• retract(Clauza)
Predicatul retract(Clauza) are ca efect eliminarea din baza de cunoştinţe a unei clauze
care unifică cu argumentul Clauza. Predicatul reuşeşte dacă o astfel de clauza exista; la
fiecare resatisfacere se elimină din baza de cunoştinţe o nouă clauză care unifică cu
argumentul. Clauza trebuie sa fie argument instanţiat la apel. Odată ce o clauză este
eliminată din baza de cunoştinţe prin retract, clauza nu va mai fi readaugată dacă se face
backtracking pe retract.

Exemplu.
44
adaug :- asserta(prezent(nero)), asserta(prezent(cezar)).
scot :-retract(prezent(nero)).
actiune1 :- adaug, listc(prezent(X)).
actiune2 :- scot, listc(prezent(X)).
?- actiune1.
prezent(nero).
prezent(cezar).
?- actiune2.
prezent(cezar).

Utilizând predicatul retract, se poate defini un predicat care are rolul de a elimina din
baza de cunoştinţe toate clauzele care definesc un predicat. Acest predicat, numit
retractall(Antet), este predefinit în anumite implementări Prolog. Definiţia lui pe baza
predicatului retract este:
% retractall(Antet) - elimină toate clauzele care definesc scopul Antet
retractall(Antet) :- retract(Antet), fail.
retractall(Antet) :- retract( (Antet :- Corp) ), fail.
retractall( _ ).

Observaţie: În ARITY Prolog, atunci când se adaugă/elimină o regulă în/din baza de date
în mod dinamic cu assert/retract regula trebuie pusă între paranteze rotunde sau dată ca o
structură:

assert( (p(X) :- X=1 ; X=2) ).


retract( ':-'(p(X), ';'(X=1, X=2))).
Predicatele asserta, assertz şi retract pot fi utile în diverse situaţii datorită efectelor
lor laterale. Ele pot simula, de exemplu, un fel de mecanism de variabile globale în Prolog.
Se consideră exemplul de implementare a unui generator de numere aleatoare în Prolog. Se
defineşte predicatul aleator(R, N) care generează un număr aleator N în intervalul [1..R]
pornind de la o sămânţă fixată şi folosind metoda congruenţei liniare. De fiecare dată când
trebuie generat un număr aleator, valoarea acestuia este determinată folosind sămânţa
existentă şi o nouă sămânţă este calculată şi memorată ca fapt Prolog până la noul apel.
Vechea sămânţă este eliminată din baza de cunoştinţe .
% aleator(R, N) - instanţiază N la un număr aleator între 1 şi R
samanta(11).
aleator(R, N) :-
samanta(S),
modul(S, R, N1),
N is N1 + 1,
retract(samanta(S)),
45
N2 is (125 * S +1),
modul(N2, 4096, SamantaNoua),
asserta(samanta(SamantaNoua)).
modul(X, Y, X) :- X < Y.
modul(X,Y,Z) :- % Predicatul modulo este predefinit
X >= Y, % în anumite implementări
X1 is X - Y,
modul(X1, Y, Z).

La apelul predicatului se obţin următoarele rezultate:


?- aleator(100, N).
N = 14
?- aleator(100, N).
N = 27
?- aleator(100, N).
N = 48

Dacă se doreşte afişarea continuă a numerelor aleatoare, o primă variantă este aceea de
a crea un ciclu infinit de afişare a acestor numere, efect realizat de predicatul nr_aleat(R)
care afişează numere aleatoare în intervalul [1..R].
% nr_aleat(R) - afişează la infinit numere aleatoare între 1 şi R
nr_aleat(R) :- repeat, aleator(R, X), write(X), nl, fail.

Predicatul fail introdus în definiţia predicatului nr_aleat(R) determină intrarea în


procesul de backtracking şi datorită predicatului repeat, cele două scopuri aleator(R, X) şi
write(X) se vor satisface la infinit. Trebuie observat faptul că numai scopul repeat se
resatisface; scopurile aleator şi write sunt la prima satisfacere de fiecare dată.
În cazul în care se doreşte construirea unui ciclu care se va termina dacă o anumită
condiţie este îndeplinită, se poate reformula predicatul de generare a numerelor aleatoare
astfel:
nr_aleat1(R) :-
repeat, aleator(R, X), write(X), nl,
write('Continuati? [d/n]'), read('n').

Apelând predicatul se va obţine următoarea secvenţă de numere aleatoare:


?- nr_aleat1(10).
4
Continuati? d.
7
Continuati? d.
46
8
Continuati? n.
yes

• functor(Term, Funct, N)
Predicatul reuşeşte dacă cele trei argumente unifică astfel: Term este o structură cu
functor Funct şi aritate N. Predicatul poate fi folosit în două moduri:
(1) Term este argument instanţiat şi predicatul reuşeşte dacă Term este atom sau
structură şi eşuează în caz contrar. În caz de succes, Funct şi N sunt instanţiate
la functorul, respectiv aritatea lui Term. Un atom este considerat ca fiind o
structură de aritate 0.
(2) Term este argument neinstanţiat iar Funct şi N sunt instanţiate, specificând
functorul şi numărul de argumente. În acest caz scopul reuşeşte întotdeauna şi
Term va fi instanţiat la o structură cu functorul şi numărul de argumente
specificate. Argumentele structurii construite în acest mod sunt neinstanţiate.
Exemple:
?- functor(auto(honda, culoare(roşie)), Funct, N).
Funct = auto, N = 2
?- functor(a + b, F, N).
F = +, N = 2
?- functor(albastru, F, N).
F = albastru, N = 0
?- functor [a, b, c], !, 3).
no
?- functor(Term, pasi_plan, 3).
Term = pasi_plan(_0, _0, _0)

• arg (N, Term, Arg)


Predicatul arg (N, Term, Arg) are ca efect obţinerea celui de al N-lea argument al
unei structuri Term, acest argument devenind instanţierea lui Arg. N şi Term trebuie să fie
argumente instanţiate. Arg poate fi instanţiat, caz în care predicatul reuşeşte dacă Arg este
argumentul N din structura Term, sau Arg poate fi neinstanţiat, caz în care se va instanţia la
cel de al N-lea argument al lui Term.
?- arg(2, poseda(mihai, frumoasa(portocala)), Arg).
Arg = frumoasa(portocala).
?- arg (2,[a, b, c], X).
X = [b, c]
?- arg(1, a + (b + c), b).

47
no

• Scop = .. [Functor | ListArg]


Predicatul =.., numit univ, este un predicat folosit ca operator infixat. El poate fi
folosit în trei moduri:
(1) Scop este instanţiat. În acest caz predicatul reuşeşte, dacă Scop este o
structură, iar Functor şi ListArg se instanţiază la functorul, respectiv lista
argumentelor acelei structuri.
(2) Scop este neinstanţiat, iar Functor şi ListArg sunt instanţiate la un functor,
respectiv la o listă de argumente a unei structuri. Dacă valorile lui Functor şi
ListArg sunt corecte, predicatul reuşeşte şi Scop va fi instanţiat la structura
creată cu functorul şi lista de argumente date.
(3) Toţi trei parametrii sunt instanţiati şi predicatul reuşeşte dacă Functor şi
ListArg sunt functorul, respectiv lista argumentelor structurii Scop.
Acest predicat are o importanţă deosebită în Prolog deoarece el permite sinteza
dinamică a scopurilor Prolog, deci construirea de clauze pe parcursul execuţiei
programului. Lucrul acesta este posibil deoarece clauzele Prolog au tot forma unor structuri
formate din functor şi argument, sintaxa codului fiind aceeaşi cu sintaxa datelor în Prolog.
Exemple:
?- sir(a, b, c) =.. X.
X = [sir, a, b, c]

?- (f(a) + g(b)) =.. [+, f(X), Y].


X = a, Y = g(b)
?- a + b + c =.. L. % a + b + c = '+'(a, '+'(b, c))
L=[+, a+b, c]
?- a * b + c =.. L. % a * b + c = '+'('*'(a, b), c)
L = [+, a * b, c]
?- a + b * c =.. L. % a + b * c = '+'(a, '*'(b, c))
L = [+, a, b * c]
?- Scop =.. [parinte, mihai, gina].
Scop = parinte(mihai, gina)

?- Scop =.. [membru, a, [a, b, c]].


Scop = membru(a, [a, b, c])

?- conc([1, 2], [3], [1, 2, 3]) =.. [Functor | ListArg]


48
Functor = conc, ListArg = [[1, 2], [3], [1, 2, 3]]

?- S =.. [f, X, a,Y].


S = f( _004C, a, _0060 ), X = _004C, Y = _0060

?- f =.. L.
L = [f]

Folosind univ putem verifica faptul că listele se reprezintă prin structuri cu punct de
forma: '.'(PrimulElement, RestulListei). Lista [1, 2] este totuna cu '.'(1, [2]) sau cu
'.'(1, '.'(2, []).
Exemplu.
?- [1,2] =.. L.
L = [. , 1, [2]].

• listing, listing(+Nume), listing(+Nume/Aritate), listing(+[Nume/Aritate…])


Predicatul listing tipăreşte din baza de cunoştinţe toate clauzele, toate clauzele unui
predicat sau toate clauzele unor predicate dintr-o listă. De exemplu, pentru predicatele:
p.

p(1).
p(X) :- X > 2, X < 5.
p(X).

p(1,2).
p(X,Y) :- X < Y.
se pot face următoarele interogări (redenumirile variabilelor sunt făcute automat de sistem):
?- listing(p/1).
p(1).
p(A) :- A > 2, A < 5.
p(A).
yes
?- listing([p/0, p/2]).
p.
p(1,2).
p(A, B) :- A < B.
yes

(d) Predicate pentru execuţia dinamică a scopurilor


49
• call(Scop)
Se presupune că argumentul Scop este instanţiat la un scop Prolog. Predicatul
call(Scop) reuşeşte dacă Scop reuşeşte şi eşuează în caz contrar. La prima vedere acest
predicat pare redundant deoarece execuţia scopului Scop se poate face direct în Prolog.
Dacă se doreste execuţia dinamică a scopurilor care au fost sintetizate pe parcursul execuţiei
unui program Prolog, atunci predicatul call este necesar. În plus, dacă nu se cunoaşte scopul
de executat, de exemplu se doreşte execuţia a diverse scopuri cu argumente diferite sau cu
aceleaşi argumente, se poate folosi predicatul call cu argument o variabilă care se va
instanţia în funcţie de întrebarea utilizatorului sau de context.
Execuţia dinamică a scopurilor poate fi exprimată prin următoarea secvenţă de
scopuri:
... obţine(Functor), calculează(ArgList), Scop =.. [Functor | ArgList], call(Scop), ...
Se prezintă în continuare diverse variante de predicate Prolog care aplică un predicat
primit ca argument fiecărui element al unei liste sau al mai multor liste, rezultatele fiecărei
aplicări fiind cumulate într-o listă rezultat. Acest tip de predicate reprezintă o categorie de
prelucrări ale listelor, frecvent întâlnite şi necesare.
Se defineşte întâi predicatul mapcar(Pred, ListaArg, ListaRez) care primeşte două
argumente instanţiate Pred şi ListaArg şi calculează ListaRez. Pred este un predicat
Prolog care are la rândul lui două argumente: primul, instanţiat, este folosit în prelucrare
pentru obţinerea celui de al doilea şi este obţinut ca element curent din ListaArg; cel de al
doilea, neinstanţiat, este calculat de Pred şi este depus ca element curent în ListaRez. În
programul care urmează se vor folosi ca predicate Pred următoarele:
• prim(Lista, El) care obţine primul element dintr-o listă,
• neg(Element, -Element) care schimbă semnul unui element şi
• adaug(Nume, NumeFrumos) care adaugă în faţa unui nume apelativul de politeţe
dna dacă persoana este femeie şi dl dacă persoana este bărbat.
Se observă că argumentele lui Pred din definiţia lui mapcar pot fi atât elemente atomice
(atomi sau numere) cât şi liste.
% Se definesc predicatele de prelucrare a listelor: mapcar, mapcar2 şi mapcarnl.
prim([], []).
prim([X | _ ], X).
neg(X, -X).
adaug(Nume, NumeFrumos) :-
write(Nume),
write(' este femeie sau barbat (f/b)? '),
read(Sex),
(Sex = b, NumeFrumos = [dl, Nume] ;
50
Sex = f, NumeFrumos = [dna, Nume]).
% mapcar(Pred, ListaArg, ListaRez) - evaluează predicatul
% Pred pentru fiecare element din lista ListaArg şi
% construieşte rezultatul în ListaRez.
% Predicatul Pred are două argumente, atomice sau liste.
mapcar( _ , [], []).
mapcar(P, [Arg | RestArg], [X | Rest]) :-
Scop =.. [P, Arg, X],
call(Scop),
mapcar(P, RestArg, Rest).

În definirea scopului adaug s-a utilizat simbolul ;. Acesta este un operator predefinit
în Prolog care marchează o disjuncţie de scopuri, deci are semnificaţia de disjuncţie logică.
Efectul operatorului ; poate fi reprezentat în următorul fel:
X ; X :- X.
X ; Y :- Y.

Deci aşa cum operatorul Prolog , corespunde unei conjuncţii de scopuri, operatorul ;
corespunde unei disjuncţii de scopuri.
Comportarea acestui program este următoarea:
?- mapcar(prim, [[alain, turing], [ada, byron]], Rez).
Rez = [alain, ada]
?- mapcar(neg, [1, 2, 3, 4], Rez).
Rez = [-1, -2, -3, -4]
?- mapcar(adaug, [maria, mihai, george, ana], Rez).
maria este femeie sau barbat (f/b)? f
mihai este femeie sau barbat (f/b)? b
george este femeie sau barbat (f/b)? b
ana este femeie sau barbat (f/b)? f
Rez = [[dna, maria], [dl, mihai], [dl, george], [dna, ana]]

Dacă se doreste execuţia unor prelucrări similare, dar pentru predicate Prolog care
admit trei argumente, primele doua instanţiate şi cel de al treilea sintetizat de predicatul
Pred pe baza primelor două, se poate defini de o maniera similară predicatul:
mapcar2(Pred, ListaArgPrim, ListaArgSecund, ListaRez)
Se va demonstra funcţionarea acestui predicat în cazurile în care predicatul Pred, care
se mapează pe elementele listelor ListaArgPrim şi ListaArgSecund, este întâi
plus(A, B, Rez) care calculează în Rez suma lui A şi B, apoi conc(List1, Lista2, ListaRez)

51
care concatenează două liste şi depune rezultatul în ListaRez. Argumentele predicatului
Pred pot fi argumente atomice sau liste.
conc([], L, L).
conc([X|Rest], L, [X|Rest1]) :- conc(Rest, L, Rest1).
plus(A, B, Rez) :- Rez is A + B.
% mapcar2(Pred, ListaArgPrim, ListaArgSecund, ListaRez) -
% evaluează predicatul Pred pentru fiecare pereche
% de argumente, unul din ListaArgPrim, celălalt din ListaArgSecund
% şi construieşte rezultatul în ListaRez.
% Predicatul Pred are trei argumente, atomice sau liste.
mapcar2( _ , [], _ , []).
mapcar2(P, [Arg1 | RestArg1], [Arg2 | RestArg2], [X | Rest]) :-
Scop =.. [P, Arg1, Arg2, X],
call(Scop),
mapcar2(P, RestArg1, RestArg2, Rest).
?- mapcar2(plus, [1, 2, 3, 4], [10, 20, 30, 40], Rez).
Rez = [11, 22, 33, 44]
?- mapcar2(conc, [[1, 2], [a, b]], [[3, 4], [c, d]], Rez).
Rez = [[1, 2, 3, 4], [a, b, c, d]]

Se observă că este necesară definirea unui predicat de tip mapcar diferit pentru fiecare
categorie de aritate N a predicatelor care pot instanţia legal Pred. Dacă se impune restricţia
conform căreia predicatul Pred poate avea numai argumente atomice, atunci se poate defini
predicatul mapcarnl(Pred, ListaListeArg, ListaRez) cu un efect similar. ListaListeArg
este fie o listă de elemente atomice, dacă Pred are două argumente, dintre care primul
instanţiat va fi obţinut ca elementul curent al acestei liste, fie o listă de liste, unde fiecare
element listă din ListaListeArg conţine primele N-1 argumente atomice ale lui Pred.
% mapcarnl(Pred, ListaListeArg, ListaRez) -
% evaluează predicatul Pred cu primele N-1 argumente din lista
% ListaListeArg şi construieşte rezultatul în ListaRez.
% Predicatul Pred poate avea oricâte argumente, dar toate trebuie sa fie atomice.
mapcarnl(_, [], []).
Mapcarnl(P, [Arg|RestArg], [X|Rest]) :-
(atomic(Arg), FormaScop = [P,Arg,X] ;
not atomic(Arg), conc([P], Arg, Temp), conc(Temp, [X], FormaScop)),
Scop =.. FormaScop,
call(Scop),
mapcarnl(P, RestArg, Rest).
?- mapcarnl(neg, [1, 2, 3, 4], Rez]
Rez = [-1, -2, -3, -4]
52
?- mapcarnl[[1, 10], [2, 20], [3, 30], [4, 40]], Rez).
Rez = [11, 22, 33, 44].

• findall(X, Scop, Lista)


Predicatul findall(X, Scop, Lista) are ca efect obţinerea tuturor termenilor X care
satisfac predicatul Scop în baza de cunoştinţe a programului şi cumularea acestor termeni
în lista Lista. Scop trebuie să fie instanţiat la un predicat Prolog în care, în mod normal,
trebuie să apară X. Dacă Scop nu reuşeşte, findall reuşeşte, instanţiind Lista la lista vidă
(această ultimă caracteristică este specifică mediului ARITY).
Exemple:
prieten(vlad, ana).
prieten(vlad, george).
prieten(vlad, mihai).
prieten(liviu, ana).
?- findall(X, prieten(vlad, X), L).
X = _0038, L = [ana, george, mihai]

Efectul predicatului findall poate fi explicat şi prin definiţia explicită a unui predicat
cu acelaşi efect: find_all(X, Scop, Lista)
% find_all(Arg, Scop, Lista) -
% are acelaşi efect ca predicatul standard findall(Arg, Scop, Lista).
prieten(vlad, ana).
prieten(vlad, george).
prieten(vlad, mihai).
prieten(liviu, ana).
find_all(X, S, _) :- asserta(stiva(baza_stivei)),
call(S), % Scopul S conţine X.
asserta(stiva(X)), fail.
find_all( _ , _ , L) :- colecteaza([], M), !, L = M.

colecteaza(S, L) :- urmator(X), !, colecteaza([X | S], L).


colectează(L, L).

următor(X) :- retract(stiva(X)), !, X \= baza_stivei.

?- find_all(X, prieten(vlad, X), L).


X = _0038, L = [ana, george, mihai].
?- find_all(X, prieten(toma, X), L).
X = _0038, L = []

53
În definiţia predicatului find_all se observă utilizarea unei tehnici de programare
Prolog interesante. Utilizând predicatul asserta se simulează o structură de stivă în Prolog.
Baza stivei este marcata de faptul stiva(baza_stivei) şi în stivă se introduc, ca fapte
suplimentare adăugate la începutul bazei de cunoştinţe Prolog, valorile X care satisfac
scopul S sub forma stiva(X), cu X instanţiat de apelul call(S). Acest lucru este executat
pentru toate soluţiile posibile ale predicatului call(S) datorită predicatului fail introdus în
finalul definiţiei primei clauze a predicatului find_all. În acest fel toate valorile lui X din
baza de cunoştinţe Prolog care satisfac S sunt introduse în stiva simulată. Cea de a doua
clauză a predicatului find_all, care se execută după ce nu mai există nici o posibilitate de
resatisfacere a primeia, are ca scop golirea stivei şi colectarea valorilor introduse în stivă,
până la baza ei, stiva(baza_stivei), în lista M.
• bagof(X, Scop, Lista)
Predicatul bagof(X, Scop, Lista) are ca efect colectarea în Lista a tuturor termenilor
X care satisfac Scop, ţinând cont de diversele instanţieri posibil diferite ale celorlalte
variabile din Scop. Scop este un argument care trebuie să fie instanţiat la un scop Prolog.
Dacă Scop nu conţine alte variabile în afara lui X, atunci efectul predicatului bagof este
identic cu cel al lui findall. Dacă Scop nu reuşeşte cel puţin o dată atunci bagof eşuează.
Exemple:
clasificare(a, vocala).
clasificare(b, consoana).
clasificare(c, consoana).
clasificare(d, consoana).
clasificare(e, vocala).
clasificare(f, consoana).
?- findall(X, clasificare(X, C), L).
X = _0038, C = _004C, L = [a, b, c, e, f] % o soluţie
?- bagof(X, clasificare(X, C), L).
X = _0038, C = consoana, L = [b, c, d, f];
X = _0038, C = vocala, L = [a, e]
no % două soluţii

• setof(X, Scop, Lista)


Predicatul setof(X, Scop, Lista) are acelaşi efect cu predicatul bagof, cu excepţia
faptului că valorile cumulate în lista Lista sunt sortate crescător şi se elimină apariţiile
multiple de valori, deci Lista devine o mulţime ordonată.Dacă Scop nu reuşeşte cel puţin o
dată atunci setof eşuează.
Considerând faptele de definire a consoanelor şi variabilelor prezentate anterior, se
poate construi următorul exemplu, în care mai introducem două fapte:

54
asserta(clasificare(d, consoana)).
asserta(clasificare(e, vocala)).
?- bagof(X, clasificare(X, C), L).
X = _0038, C = consoana, L = [d, b, c, d, f]; % consoana d apare de două ori
X = _0038, C = vocala, L = [e, a, e]; % vocala e apare de două ori
no
?- setof(X, clasificare(X, C), L).
X = _0038, C = consoana, L = [b, c, d, f]; % consoanele apar o singură dată
X = _0038, C = vocala, L = [a, e]; % vocalele apar o singură dată
no

4.4 Direcţia de construire a soluţiilor


Majoritatea programelor Prolog se bazează pe recursivitate. Ca în orice limbaj de
programare recursiv, parametrii de ieşire ai subprogramelor, deci argumentele sintetizate ale
predicatelor în cazul limbajului Prolog, pot fi calculate fie pe ramura de avans în
recursivitate, fie pe ramura de revenire din recursivitate. Se consideră două variante de
definire a predicatului de numărare a elementelor dintr-o listă. Prima varianta,
nr_elem(Lista, NrElem), construieşte soluţia (calculează numărul de elemente din listă) pe
ramura de revenire din recursivitate.
% nr_elem(Lista, NrElem)
nr_elem([], 0).
nr_elem([ _ | Rest], N) :- nr_elem(Rest, N1), N is N1 + 1.

A doua variantă, nr_elem2(Lista, NrElem) calculează numărul de elemente din listă


pe ramura de avans în recursivitate. Pentru a realiza acest lucru, se foloseşte un predicat
ajutător nr_elem1(Lista, NrAcumulat, NrElem) care are un argument suplimentar faţă de
predicatul nr_elem2. Acest argument, NrAcumulat, are rolul de variabilă de acumulare a
numărului de elemente din listă pe măsura avansului în recursivitate şi este instanţiat la
primul apel la valoarea 0. În momentul atingerii punctului de oprire din recursivitate, deci în
cazul în care lista ajunge vidă, valoarea acumulata în argumentul NrAcumulat este copiată
în cel de-al treilea parametru al predicatului nr_elem_1. Se face apoi revenirea din apelurile
recursive succesive fără a efectua nici o altă prelucrare, predicatul nr_elem_1 reuşeşte şi
trimite valoarea calculată în NrElem predicatului iniţial nr_elem2.
% nr_elem2(Lista, NrElem)
% nr_elem1(Lista, NrAcumulat, NrElem)
nr_elem2(Lista, N) :- nr_elem1(Lista, 0, N).
nr_elem1([], N, N).
nr_elem1([ _ | Rest], N1, N2) :- N is N1 + 1, nr_elem1(Rest, N, N2).

55
O abordare similară se poate vedea în cazul celor două variante de definire a
predicatului de intersecţie a două liste (determinarea elementelor comune celor două liste).
Prima variantă, inter(Lista1, Lista2, ListaRez), calculează soluţia (lista intersecţie) pe
ramura de revenire din recursivitate. Cea de a doua variantă, int(Lista1, Lista2, ListaRez),
calculează soluţia pe ramura de avans în recursivitate, utilizând
int1(Lista1, Lista2, ListaAcumulare, ListaRez) ca predicat ajutător cu parametrul
suplimentar ListaAcumulare, instanţiată la primul apel la lista vidă.
% inter(Lista1, Lista2, ListaRez)
member(Elem, [Elem|_]) :- !.
member(Elem, [_|Rest]) :- member(Elem, Rest).
inter([], _, []).
inter([Prim|Rest], Lista2, [Prim|LRez]) :-
member(Prim, Lista2), !,
inter(Rest, Lista2, LRez).
inter([ _ | Rest], Lista2, LRez) :- inter(Rest, Lista2, LRez).

% int(Lista1, Lista2, ListaRez)


% int1(Lista1, Lista2, ListaAcumulare, ListaRez)
int(L1, L2, LRez) :- int1(L1, L2, [], LRez).
int1([], _, L, L).
int1([Prim|Rest], L, L1, L2) :-
member(Prim,L), !,
int1(Rest, L, [Prim | L1], L2).
int1([ _ | Rest], L, L1, L2) :- int1(Rest, L, L1, L2).

Această tehnică de programare, des întîlnită în programele Prolog, are o serie de


avantaje, printre care o claritate crescută şi, în principal, utilizarea definiţiilor de predicate
recursive la urmă. Un predicat recursiv la urmă este un predicat în care apelul recursiv al
acestuia este ultimul scop din conjuncţia de scopuri care îl defineşte şi pentru care nu mai
există nici o regulă posibilă de aplicat după apelul recursiv. Un astfel de predicat are
avantajul că poate fi apelat recursiv de oricâte ori fără a genera o depăşire a stivei. În plus
execuţia unui astfel de predicat este mai eficientă.
Pentru a pune în evidenţă această comportare se definesc patru variante ale unui
predicat care afişează (numără) valori întregi succesive la infinit, începând de la o valoare
fixată N.
% Predicatele numără(N), numără1(N), rău_numără(N) şi rău_numără1(N)
% afişează întregi succesivi pornind de la valoarea N.
numara(N) :- write(N), nl, N1 is N + 1, numara(N1).

56
numara1(N) :- N >= 0, !, write(N), nl, N1 is N + 1, numara1(N1).
numara1( _ ) :- write("Valoare negativa.").

rau_numara(N) :- write(N), nl, N1 is N + 1, rau_numara(N1), nl.

rau_numara1(N) :- N >= 0, write(N), nl, N1 is N + 1, rau_numara1(N1).


rau_numara1(N) :- N < 0, write("Valoare negativa.").

Primele două variante ale acestui predicat, numara(N) şi numaraa(N), sunt predicate
recursive la urmă. Predicatul numara(N) este definit printr-o singură regulă şi apelul
recursiv este ultimul scop al definiţiei. Predicatul numara1(N) este definit prin două reguli,
dar, datorită predicatului cut din prima regulă, dacă N ≥ 0 nu mai există nici o altă regulă
posibilă de aplicat în momentul apelului recursiv. Ambele variante vor număra la infinit
începând de la N şi afişând valori succesive. Următoarele două variante, rau_numara(N) şi
rau_numara1(N), nu sunt predicate recursive la urmă. Predicatul rau_numara(N) nu are
apelul recursiv ca ultim scop în definiţie, iar rau_numara1(N) are o variantă (regula)
neîncercată în momentul apelului recursiv. Execuţia ambelor predicate se va opri după un
anumit număr de valori afişate, cu afişarea unui mesaj de eroare cauzat de depăşirea stivei.

57
58
Partea a II-a

Aplicaţii
În continuare se vor prezenta o serie de mecanisme de prelucrare specifice limbajului
Prolog. Anumite programe implementează algoritmi şi prelucrări de structuri de date
presupuse cunoscute, cum ar fi arbori sau metode de sortare. Alte programe, cum ar fi cele
de căutare în spaţiul stărilor sau în arbori Şi/Sau, sunt specifice inteligenţei artificiale şi este
recomandată parcurgerea capitolelor corespunzătoare din [Flo93] unde se descriu principiile
de bază ale acestor aplicaţii.

5 Mediul ARITY Prolog


În această secţiune se prezintă comenzile de editare şi depanare din mediul de programare
ARITY Prolog şi predicatele predefinite în această versiune.
5.1 Generalităţi
Pentru a putea lucra cu editorul din ARITY Prolog, trebuie inserată în fişierul config.sys
linia de comandă DEVICE=C:DOS\ANSI.SYS /K, dacă se utilizează MS-DOS sau
Windows 3.x, respectiv linia DEVICE=C:\95\COMMAND\ANSI.SYS /K, dacă se
utilizează Windows 95.
Unele din opţiunile din meniurile ARITY Prolog sunt echivalente cu combinaţii de
taste. În aceste cazuri, ele vor fi specificate între paranteze, alături de combinaţiile
echivalente. Pentru a selecta un meniu se tastează combinaţia Alt + prima literă a numelui.
Selectarea unei opţiuni dintr-un meniu se face tastând litera evidenţiată din numele opţiunii,
sau folosind combinaţia de taste asociată (dacă există). Atunci când, într-un meniu, după
numele unei opţiuni urmează trei puncte, înseamnă că, eventual, utilizatorul mai are de
completat o cutie de dialog. Se poate naviga cu Tab sau Shift + Tab între câmpurile,
butoanele şi listele cutiei de dialog, iar în cadrul câmpurilor şi listelor se poate naviga cu
tastele cu săgeţi (←, ↑, → şi ↓). O opţiune din meniu poate fi oricând părăsită tastând Esc.
5.2 Comenzile editorului
Editorul are două moduri de editare: inserare sau suprascriere de caractere. Trecerea de la un
mod la altul se face cu tasta Ins. Deplasarea cursorului şi poziţionarea pe ecran se pot face
cu următoarele taste sau combinaţii de taste:
Săgeată la stânga ← Un caracter la stânga.
Săgeată la dreapta → Un caracter la dreapta.
Săgeată în sus ↑ O linie în sus.

59
Săgeată în jos ↓ O linie în jos.
Ctrl + ← Un cuvânt la stânga.
Ctrl + → Un cuvânt la dreapta.
Home Începutul liniei.
End Sfârşitul liniei.
PageUp O pagină în sus.
PageDown O pagină în jos.
Ctrl + Home Începutul fişierului.
Ctrl + End Sfârşitul fişierului
Ctrl + g Deplasare la linia specificată.

Selectarea, copierea şi căutarea în text se fac astfel:


Backspace (←) Şterge un caracter la stânga cursorului.
Del Şterge un caracter la dreapta cursorului.
Shift + ←, ↑, → sau ↓ Selectează o porţiune de text, începând de la poziţia
curentă a cursorului.
Ctrl + r (Undo) Restaurează o linie la starea în care era când cursorul a
fost mutat pe ea.
Shift + Del (Cut) Şterge textul selectat şi îl mută în clipboard.
F2 (Copy) copiază în clipboard textul selectat.
Shift + Ins (Paste) Copiază textul din clipboard.
Del (Clear) Şterge textul selectat, fără a-l copia în clipboard.
F4 (Find) Caută un şir în text.
Ctrl + \ (Find Selected) Caută în text un şir selectat anterior.
F3 (Repeat Last Find) Repetă ultima căutare.
F5 (Change) Caută un şir specificat şi îl înlocuieşte cu alt şir
specificat.

Mediul ARITY constă dintr-o fereastră principală (main window) şi zece fişiere
tampon (buffers). Clipboard-ul este buffer-ul 0, în buffer-ele de la 1 încolo fiind încărcate
fişierele cu cod Prolog. Interpretarea de către ARITY Prolog a conţinutului unui buffer se
numeşte consultare. Operaţiile posibile cu aceste fişire (meniul Buffers) sunt:
F6 (Go to) Buffer-ul activ (în care se face editarea curentă) devine cel
selectat.
F7 (Go to Last) Bufer-ul anterior selectat devine activ.
Erase Buffer Şterge conţinutul buffer-ului curent.
Reconsult Buffer Reconsultă conţinutul buffer-ului curent.
Save on Exit Conţinutul buffer-ului curent este salvat pe discul local atunci
când buffer-ul devine inactiv (buffer-ul este închis, sau nu mai
e activ pentru că s-a trecut în alt buffer sau în fereastra
60
principală).

Reconsult on Exit Conţinutul buffer-ului curent este reconsultat când buffer-ul


devine inactiv (buffer-ul este închis, sau nu mai e activ pentru
că s-a trecut în alt buffer sau în fereastra principală).
Indent Indentează o linie nouă la fel cu linia anterioară în buffer-ul
curent.
Read Only Marchează buffer-ul curent ca putând fi doar citit; buffer-ul nu
mai poate fi modificat.

Tastele funcţionale F1 ÷ F8 au asociate următoarele acţiuni:


F1 (Help) Deschide fişierul cu informaţii despre predicatele ARITY
predefinite.
F2 (Copy) Copiază în clipboard textul selectat.
F3 (Repeat Last Find) Repetă ultima căutare.
F4 (Search) Căutarea unui şir în text.
F5 (Change) Caută un şir specificat şi îl înlocuieşte cu alt şir
specificat.
F6 (Go to) Buffer-ul activ (în care se face editarea curentă) devine
cel selectat.
F7 (Go to Last) Bufer-ul anterior selectat devine activ.
F8 (Switch) Comutare din buffer-ul activ în fereastra principală (main
window) sau invers.

Meniul File:
Operaţiile posibile cu fişiere sunt următoarele:
New Deschide un fişier nou într-un buffer nou.
Open File Deschide un fişier deja creat într-un buffer nou.
Merge File Deschide un fişier şi îl adaugă la sfârşitul buffer-ului curent.
Save File Salvează buffer-ul curent într-un fişier.
Save File As Salvează buffer-ul curent într-un fişier cu un nume nou.
Consult File Consultă un fişier fără să îl încarce într-un buffer.

Operaţiile posibile cu baza de cunoştinţe ARITY Prolog sunt următoarele:


Restore Db Închide toate fişierele deschise şi apoi, pe baza informaţiilor
din fişierul api.idb, restaurează baza de date a interpretorului
şi deschide fişierele deschise la atunci când s-a făcut salvarea
respectivă.
Restore Db From Închide toate fişierele deschise şi apoi, pe baza informaţiilor
din fişierul indicat de utilizator, restaurează baza de date a
61
interpretorului şi deschide fişierele deschise la atunci când s-a
făcut salvarea respectivă.
Save Db Salvează baza de date a interpretorului şi informaţiile depre
fişierele deschise în fişierul api.idb.
Save Db As Salvează baza de date a interpretorului şi informaţiile depre
fişierele deschise într-un fişier indicat de utilizator.
Dos Shell Lansează o sesiune MS-DOS. Revenirea în mediul ARITY se
face prin comanda exit.
Halt Părăsire mediu de programare ARITY Prolog. Dintre fişierele
deschise, modificate şi nesalvate, utilizatorul este întrebat care
trebuie salvate.

5.3 Informaţiile de ajutor şi depanare


Mediul ARITY Prolog are următoarele fişiere de ajutor (help files): arity.hlp (descrierea
predicatelor predefinite), debug.hlp (descrierea comenzilor de depanare a programelor),
editor.hlp (descrierea comenzilor mai importante ale editorului) şi errors.hlp (descrierea
mesajelor de eroare generate la consultarea unui buffer sau în execuţia unui predicat
Prolog). Deoarece aceste fişiere sunt stocate în format ASCII, ele pot vizualizate atât din
mediul ARITY Prolog, cât şi cu un editor simplu de texte, putând fi listate la imprimantă.
Din meniul Help se poate deschide fişierul arity.hlp, selectând opţiunea Arity/Prolog, sau
oricare din cele patru menţionate, cu opţiunea Other Topics.
Notaţia argumentelor din descrierea predicatelor predefinite (din help) denotă
semnificaţia uzuală a acestora şi este următoarea:
+ Argument Argumentul este, în mod normal, de intrare (trebuie
specificat).
-Argument Argumentul este, în mod normal, de ieşire (este calculat).
?Argument Argumentul poate fi de intrare sau de ieşire.
S-a specificat “în mod normal”, deoarece, în unele cazuri, argumentele se pot folosi şi
altfel, folosind puterea generativă a limbajului Prolog. În exemplele descrise în secţiunile
următoare se va folosi aceeaşi convenţie pentru indicarea argumentelor de intrare, ieşire sau
de ambele tipuri.
Meniul Info are două opţiuni Statistics şi Modify Windows. Prima oferă informaţii
despre starea mediului de programare (stivă, câte operaţii de garbage collection care au fost
făcute, memorie utilizată, etc.), iar a doua permite utilizatorului să-şi creeze o interfaţă
personalizată, stabilind proprietăţile ferestrelor mediului de programare (culori, domensiuni,
amplasare, titlu, etc.)
Meniul Debug are următoarele opţiuni:

62
Spy Se specifică ce predicate trebuie urmărite la depanare
Trace On Activează depanatorul pentru trasarea predicatelor urmărite. Este
echivalentă cu apelul predicatului trace.
Următoarele 4 opţiuni specifică din ce punct al evoluţiei predicatelor încep să se
afişeze informaţii de trasare (care arată la ce structuri de date sunt legaţi parametrii
acestora):
Call …de la începutul satisfacerii predicatului.
Exit …de la terminarea satisfacerii predicatului.
Fail …de la eşuarea satisfacerii predicatului.
Redo …de la începutul resatisfacerii predicatului.

Trebuie menţionate două lucruri: în ARITY Prolog, un predicat (scop) este văzut ca o
cutie neagră (black box) cu patru porturi, două de intrare: call, redo şi două de ieşire exit,
fail. Reconsultarea unui fişier în timpul depanării unui predicat definit în acesta duce la
rezultate imprevizibile, soldându-se de obicei cu blocarea definitivă a mediului de
programare şi chiar a calculatorului. De aceea, înainte de a reconsulta un fişier, este indicat
să se oprească eventuala sesiune de depanare în curs de desfăşurare.
În timpul depanării apare o fereastră de depanare în care sunt afişate informaţiile de
trasare şi în care pot fi introduse următoarele comenzi:
Space (toggle Comutare între fereastra depanatorului şi fereastra aplicaţiei.
window)
; (redo) După un exit, pentru a forţa resatisfacerea (redo) pentru scopul
curent.
@ (accept goal) Permite apelul oricărui scop Prolog, cu reîntoarcere imediată în
depanator după terminarea scopului.
a (abort) Terminare forţată a programului (abort) şi reîntoarcere la
prompter-ul interpretorului. Această comandă este indicată în
locul comenzii Ctrl + c.
b (break) Se intră pe un nivel de break, adică se obţine un prompter de
interpretor, fără a întrerupe programul depanat. Revenirea de pe
nivelul de break şi continuarea depanării se fac cu Ctrl + z.
Pentru fiecare nivel nou de break interpretorul adaugă încă un
semn de întrebare la prompter. De exemplu, pe nivelul al treilea,
prompter-ul este “????-“.
c sau Enter Depanatorul trece la următorul port (punct de trasare). Dacă
(creep) portul este marcat ca urmărit în trasare, depanatorul se opreşte şi
aşteaptă noi comenzi, altfel merge mai departe.
d (display goal) Afişează scopul curent de satisfăcut, folosind predicatul
display.

63
e (exit interpreter) Părăsire interpretor şi revenire la prompter-ul DOS.
f (fail goal) Forţează eşuarea scopului curent. Este folositor dacă utilizatorul
ştie deja că scopul curent va eşua.
h (help) Oferă informaţii despre comenzile de depanare.
l (leap) Depanatorul va sări la următorul punct de trasare.
n (notrace) Dezactivează depanatorul. Această comandă este echivalentă
apelul predicatului notrace.
q (quasi-skip) Depanatorul sare la portul de exit sau fail al scopului curent.
Dacă însă există un punct de trasare în scop, depanatorul se va
opri la el. Acestă comandă se poate folosi numai după un call
sau redo pentru un scop. (Vezi şi comenzile s şi z.)
s sau Esc (skip) Sare la portul de exit sau fail, după cum există sau nu puncte de
trasare în cadrul scopului. Acestă comandă se poate folosi
numai după un call sau redo pentru un scop. (Vezi şi comenzile
q şi z.)
w (write goal) Scrie scopul curent, apelând predicatul write.
x (bach to Această comandă poate fi folosită la un port fail sau redo. Ea
choice point) forţează depanatorul să continue eşuarea până când este atins un
port call sau exit. Deşi în acest caz depanatorul nu se opreşte să
accepte comenzi, depanatorul afişează câte un mesaj pentru
fiecare port prin care trece.
z (zip) Sare la portul de exit sau fail, după cum alte puncte de trasare
sunt puse în scopul curent. Execuţia nu păstrează informaţie de
depanare pentru punctele de backtracking din scop, fiind mai
rapidă şi mai puţin consumatoare de memorie. Acestă comandă
se poate folosi numai după un call sau redo pentru un scop.
(Vezi şi comenzile q şi s.)

5.4 Sintaxa ARITY Prolog


În această secţiune se reiau elementele sintactice de bază din Prolog prezentate în prima
secţiune, arătând particularităţile sintactice ale limbajului ARITY Prolog. Clasificarea
obiectelor din ARITY Prolog este prezentată în figura 10.

64
atomi
structuri
şiruri de caractere
constante
obiecte
numere întregi
obiecte elementare
numere reale
variabile

Figura 10. Clasificarea obiectelor din ARITY Prolog


Un atom este un identificator care începe cu litera mică şi se continuă cu orice înşiruire
de litere mici, litere mari, cifre sau underscore “_” . Dacă se doreşte ca atomul să înceapă cu
literă mare sau să conţină caractere speciale, atunci atomul se încadrează între caractere
apostrof.
Exemple de atomi:
dana
nil
x25
x_25
x_25AB
x_
x__y
domnul_popescu
'Ivan Bratko'
'<<------>>'

Reprezentarea şirurilor depinde de implementarea Prolog. În ARITY Prolog,


interpretor care se aliniază la standardul Edinbourg Prolog, şirurile se reprezintă între
caractere $.
Exemple de şiruri de caractere:
$Constantin Noica$
$<<-------->>$

Diferenţa dintre şiruri şi atomi este următoarea: şirurile au o reprezentare internă mai
relaxată şi nu pot fi folosite pe post de atomi, în timp ce atomii au de obicei reprezentări
interne consumatoare de memorie în ideea că ei trebuie regăsiti rapid, căutările Prolog
făcându-se în general după aceşti atomi.
Un număr întreg este un şir de cifre zecimale, eventual precedate de un semn. Exemple
de numere întregi: 1 +23 1515 0 -97. Numerele reale depind de implementarea de Prolog.

65
În general ele sunt reprezentate într-o formă similară celor din limbaje gen Pascal, C, etc.
Exemple de numere reale: 3.14 -0.0035 100.2 +12.02
La fel ca şi în Prolog standard, o variabilă este un şir de litere, cifre sau underscore ( _
) care începe cu literă mare sau underscore. Exemple de variabile:
X
Rezultat
Obiect1
Lista_de_nume
_x23
_23

În Prolog există situaţii în care o variabilă apare o singură dată într-o regulă, caz în
care nu avem nevoie de un nume pentru ea, deoarece nu este referită decât într-un singur
loc. De exemplu, dacă dorim să scriem o regulă care ne spune dacă cineva este fiul cuiva, o
soluţie ar fi:
este_fiu(X) :- parinte(Z, X).
Se observă că s-a denumit cu Z un părinte anonim. În acest caz, nu interesează cine
apare pe post de părinte. Pentru a nu încărca regulile cu nume inutile care pot distrage
atenţia şi îngreuia citirea programelor, se pot considera astfel de variabile că anonime,
notate cu underscore “_”. Deci regula anterioară se poate rescrie astfel:
este_fiu(X) :- parinte( _ , X).
Variabilele din Prolog nu sunt identice ca semnificaţie cu variabilele Pascal sau C,
fiind mai curând similare cu variabilele în sens matematic. O variabilă, odată ce a primit o
valoare, nu mai poate fi modificată. Acest lucru elimină efectele laterale care sunt permise
în limbajele procedurale. O variabilă Prolog neinstanţiată semnifică ceva necunoscut. Pentru
structurile de date care nu sunt variabile şi care au nume, numele lor începe cu minusculă,
urmată de litere, cifre sau underscore.
Obiectele structurate, numite pe scurt structuri, sunt acele obiecte formate din mai
mulţi constituenţi sau componente. Componentele pot fi la rândul lor structuri. Pentru a
mixa constituenţii într-un singur obiect avem nevoie de un functor. De exemplu, dacă
dorim reprezentarea unui obiect structurat data, el poate fi:
data(24, august, 1997).
În acest caz, componentele sunt: 24, august şi 1997, iar liantul este functorul data.
Functorul poate fi orice atom. Constituenţii pot fi orice obiect Prolog. Să vedem un exemplu
de obiect compus din alte obiecte compuse.

66
Fie tipul punct(X, Y). Atunci tipul segment poate fi reprezentat ca segment(P1, P2).
De exemplu, un segment, având capetele în punctele (1, 1) şi (2, 3), poate fi reprezentat
astfel:
segment(punct(1, 1), punct(2, 3)).
Există şi cazuri în care un obiect poate avea constituenţi compuşi de adâncime
variabilă. Astfel se ajunge la al doilea tip de recursivitate în Prolog, şi anume, recursivitatea
obiectelor. De exemplu, un arbore binar, cu chei numerice poate fi definit astfel:
1) Nodul vid este un arbore; putem nota acest nod vid prin constanta nil;
2) Un nod frunză este un arbore, de exemplu nod(15, nil, nil);
3) Un nod cu succesor stâng şi succesor drept este un arbore, de exemplu nod(20,
ArbStâng, ArbDrept).
De exemplu, reprezentarea în Prolog a arborelui

2 3

4 5 6

este nod(1, nod(2, nod(4, nil, nil), nod(5, nod(7, nil, nil), nil)), nod(3, nil, nod(6, nil, nil)))
Aşa cum s-a arătat în prima parte, un program Prolog este o înşiruire de fapte (facts) şi
reguli (rules) care poartă denumirea de clauze (clauses). Faptele şi regulile au o structură
comună ceea ce permite sinteza dinamică de cod care este adăugat în baza de cunoştinţe. Un
fapt poate fi privit ca o regulă care are ca premisă (ipoteză) scopul true, care este adevărat
întotdeauna. Astfel:
fapt.
este echivalent cu
fapt :- true.
O regulă fără antet, deci de forma:
:- scop.
determină execuţia automată a scopului scop la reconsultarea bufer-ului în care apare.
Efectul este similar cu cel obţinut la introducerea în Main Window a întrebării:

67
?- scop.
Exemple de întrebări:
place(ellen, tennis). % lui ellen îi place tenisul
place(john, fotbal). % lui john îi place fotbalul
place(tom, baseball). % lui tom îî place baseball-ul
place(eric, înot). % lui eric îi place înotul
place(mark, tenis). % lui mark îi place tenisul
place(bill, X) :- place(tom, X). % lui bill îi place ce îi place lui tom

% Ce sporturi îi plac lui john?


?- place(john, X).
X = fotbal

% Cui îi place tenisul?


?- place(Y, tenis).
Y = ellen;
Y = mark

% Putem pune şi întrebări particulare:


?- place(ellen, tenis).
yes
?- place(ellen, fotbal).
no
?- place(bill, baseball).
yes

Urmează acum o serie de exemple necomentate pe care cititorul este îndemnat să le


urmărească şi să le înţeleagă.
Ex1. Exprimati in Prolog urmatoarele fapte:
1) susan are un cal;
2) rex mănâncă carne;
3) aurul este preţios;
4) maşina este un mercedes albastru cu capacitatea de cinci călători.
Răspunsuri:
1) are(susan, cal).
2) mananca(rex, carne).

68
3) pretios (aur).
4) masina(mercedes, albastru, 5).
Ex2. Fie faptele:
masina(mercedes, albastru, 5).
masina(chrysler, rosu, 4).
masina(ford, gri, 8).
masina(datsun, rosu, 5).

Întrebările:
1) Ce tipuri de maşini au cinci locuri pentru călători?
2) Ce maşini sunt roşii?
se scriu:
?- masina(Tip, _ , 5).
?- masina(Tip, rosu, _ ).
iar regula:
X este o maşină mare dacă poate transporta cel puţin 5 călători.
se scrie:
masina_mare(X) :- masina(X, _ , Nr_calatori), Nr_calatori >= 5.

Ex3. Doi copii pot juca un meci într-un turneu de tenis dacă au aceeaşi vârstă. Fie
următorii copii şi vârstele lor:
copil(peter, 9).
copil(paul, 10).
copil(chris, 9).
copil(susan, 9).
Toate perechile de copii care pot juca un meci într-un turneu de tenis sunt calculate de
predicatul:
pot_juca(Pers1, Pers2) :-
copil(Pers1, Varsta), copil(Pers2, Varsta), Pers1 \= Pers2.
Ex4. Să scriem un program care să îi găseasca Anei un partener la bal. Ea doreşte sa
mearga cu un bărbat care este nefumător sau vegetarian. Pentru aceasta dispunem de o bază
de date cu informaţii despre câţiva bărbaţii:
barbat(gelu). barbat(bogdan). barbat(toma).
fumator(toma). fumator(dan).
vegetarian(gelu).

69
Pentru a exprima doleanţele Anei, vom scrie două reguli:
1) Ana se întâlneşte cu X dacă X este bărbat şi nu este fumător.
2) Ana se întâlneşte cu X dacă X este bărbat şi este vegetarian.
Adică:
ana_se_intalneste_cu(X) :- barbat(X), not(fumator(X)).
ana_se_intalneste_cu(X) :- barbat(X), vegetarian(X).
5.5 Exerciţii propuse
EP1. Se dă o bază de fapte de forma:
parinte(Parinte, Copil).
barbat(Persoana).
femeie(Persoana).
Se cere:
1) Să se introducă câteva fapte de această formă.
2) Să se scrie regulile care definesc următoarele relaţii de rudenie:
tata(Tata, Copil).
mama(Mama, Copil).
fiu(Parinte, Copil).
fiica(Parinte, Copil).
bunic(Bunic, Copil).
bunica(Bunica, Copil).
nepot(Bunic, Copil).
nepoata(Bunic, Copil).

3) Să se puna următoarele întrebări:


Cine este părintele lui dan?
Cine este fiu?
Cine este bunic?
Cine este fiu şi tată?

EP2. Să se descrie principalele operaţii logice în Prolog: not, or, and, xor, nor, nand.
Pentru aceasta se consideră faptele
op_not(Variabila, Rezultat) şi
op_or(Variabila1,Variabila2, Rezultat).

Să se scrie:
1) faptele necesare descrierii lui op_not şi op_or;

70
2) regulile necesare construcţiei celorlalţi operatori pe baza lui op_not şi op_or. Se
cer reguli pentru:
op_and(Var1, Var2, Rezultat).
op_xor(Var1, Var2, Rezultat).
op_nor(Var1, Var2, Rezultat).
op_nand(Var1, Var2, Rezultat).

EP3. Se dau următoarele enunţuri:


1) Oricine poate citi este literat.
2) Delfinii nu sunt literaţi.
3) Anumiţi delfini sunt inteligenţi.
Să se demonstreze în Prolog că: “Există fiinţe inteligente care nu pot citi”.
EP4. Se dau următoarele enunţuri:
1) Fiecare om îi este devotat cuiva.
2) Oamenii încearcă să asasineze dictatorii faţă de care nu sunt devotaţi.
3) Toţi pompeienii erau romani.
4) Cezar era dictator.
5) Fiecare roman fie îl ura pe Cezar, fie îi era devotat.
6) Marcus era pompeian.
7) Marcus era om.
8) Marcus a încercat să îl asasineze pe Cezar.
Să se demonstreze în Prolog că:
1) Marcus nu îi era devotat lui Cezar.
2) Marcus îl ura pe Cezar.

6 Recursivitate în Prolog
Multe definiţii de concepte sunt definiţii recursive, aşa cum este de exemplu definiţia
recursivă a noţiunii de listă, arbore, sau cum a fost definiţia recursivă a predicatului stramoş
prezentată în secţiunea 4.1. O serie de noţiuni matematice, cum ar fi factorialul unui număr
sau numerele lui Fibonacci au, de asemenea, o definiţie recursivă.
6.1 Relaţii recursive
Limbajul Prolog permite exprimarea definiţiilor recursive prin definiţia recursivă a
predicatelor. Intr-o astfel de definiţie trebuie întotdeauna să se definească un fapt sau o
regulă care marchează punctul de oprire din recursivitate, deci cazul particular al definiţiei
recursive. Aşa cum s-a arătat în secţiunea 4.4, trebuie analizate şi aspecte privind direcţia de
construire a soluţiei.

71
Ex1. Să se scrie un program care calculează n!, unde n! = 1 * 2 * 3 *...* n.
% fact(+N, - NFact)
fact(0, 1). % Factorialul lui 0 este 1, punctul de oprire.
fact(N,NFact) :- % Factorialul lui N este NFact dacă:
N1 is N - 1, % calculăm (N - 1) pentru al putea pasa ca argument
fact(N1, Rezult), % fie Rezult factorial de (N - 1)
NFact is N * Rezult. % atunci NFact este N înmulţit cu factorial de (N - 1)

Ex2. Să se scrie un program care calculează termenul n din şirul lui Fibonacci:
1, 1, 2, 3, 5, 8, 13, 21, ..., în care f(n) = f(n - 1) + f(n - 2), pentru n > 2.
% fib(+N, -F)
fibo(1, 1).
fibo(2, 1).
fibo(N, F) :- N > 2, N1 is N - 1, N2 is N - 2, fibo(N1, F1), fibo(N2, F2), F is F1 +
F2.

Această implementare este ineficientă deoarece în calculul lui fibo(N, _ ), fibo(N-k, _ ) este
apelat de k ori. Implementarea următoare elimină această deficienţă, deoarece predicatul fib,
care calculează F = f(M), este apelat cu ultimii doi termeni F1 = f(M-2) şi F2 = f(M-1),
pentru M = 2÷N:
fibo(N, F) :- fib(2, N, 1, 1, F).
% predicat auxiliar fib(+M, +N, +F1, +F2, -F)
fib(M, N, _ , F2, F2) :- M >= N.
fib(M, N, F1, F2, F) :-
M < N, M1 is M + 1, F1plusF2 is F1 + F2, fib(M1, N, F2, F1plusF2, F).

Ex3. Să se scrie un predicat care calculează cel mai mare divizor comun pentru două
numere. Se foloseşte definiţia recursivă a lui Euclid:
Fie a şi b două numere întregi pozitive.
dacă b = 0
atunci cmmdc(a, b) = a;
altfel cmmdc(a, b) = cmmdc(b, r), unde r este restul împărţirii lui a la b.

Implementarea în Prolog este:


% cmmdc(+A, +B, -Cmmdc)
cmmdc(A, 0, A).
cmmdc(A, B, Rez) :- B > 0, Rest is A mod B, cmmdc(B, Rest, Rez).

Ex4. Să scriem predicatul divizor care testează dacă un număr este divizibil cu altul. Pe
baza acestui predicat să se scrie predicatul prim care verifică dacă un număr este prim.
72
% divizor(+A,+B)
% prin(+N)
divizor(A, B) :- 0 is B mod A.
prim(N):- Div is N - 1, nu_are_div(N, Div).
nu_are_div(N, 1).
nu_are_div(N, Divizor) :-
not(divizor(Divizor, N)), DivNou is Divizor - 1, nu_are_div(N, DivNou).

Ex5. Predicatul rezolvă(N) rezolvă problema turnurilor din Hanoi cu N discuri.


% rezolva(+N)
rezolva (N) :- hanoi(N, stânga, dreapta, mijloc).
hanoi(0, _ , _ , _ ).
hanoi(N, A, B, C) :-
N > 0, N1 is N - 1,
hanoi(N1, A, C, B),
write($Din $), write(A), write($ pe $), write(B), nl,
hanoi(N1, C, B, A).

6.2 Problema trezorierului


In această secţiune şi în secţiunile următoare ale capitolului se vor prezenta rezolvările în
Prolog ale câteva probleme clasice ce implică recursivitate. Fiecare problemă este prezentată
sub următoarea formă:
1) Enunţul problemei (ipoteze şi concluzie);
2) Rescrierea enunţului sub formă de clauze Prolog;
3) Demonstraţia concluziei prin formularea unei interogări corecte în Prolog.

Ipoteze:
1) Nici un membru al clubului nu are datorii la trezorierul clubului.
2) Dacă un membru al clubului nu a plătit taxa, atunci el are datorii la trezorierul
clubului.
3) Trezorierul clubului este un membru al clubului.
Concluzie:
Trezorierul clubului a plătit taxa.

% Ipoteza 1
fara_datorii(X) :- membru(X).
% Ipoteza 2
platit_taxa(X) :- fara_datorii(X).
% Ipoteza 3
membru(trezorier).
73
% Concluzia
% ?- platit_taxa(trezorier).

6.3 Problema parteneriatului


Ipoteze:
1) Tom este corect.
2) Bill nu este partener cu Tom.
3) Dacă două persoane X şi Y sunt corecte, atunci X este partener cu Y.
4) Dacă Bill nu este corect, atunci John este corect.
5) Dacă o persoană X este partener cu Y, atunci şi Y este partener cu X.
Concluzie:
John este partener cu Tom.

% Ipoteza 1
corect(tom).
% Ipoteza 2
not_partener(bill, tom).
% Ipoteza 3
partener(X, Y) :- corect(X), corect(Y), X \= Y.
% Ipoteza 4, se foloseşte şi ipoteza 3, inversată conform principiului reducerii
% la absurd: formula p → q este echivalentă cu !q → !p.
corect(john) :- not_corect(bill).
not_corect(X) :- not_ambii_corecţi(X, Y), corect(Y). % din ipoteza 3
not_ambii_corecţi(X, Y) :- not_partener(X, Y).
% Ipoteza 5
% Este reprezentată în definiţia lui partener (ipoteza 3), care include simetria.
% Concluzia
% ?- partener(john, tom).

6.4 Problema vecinilor


Ipoteze:
1) Ştefan este vecin cu Petre.
2) Ştefan este căsătorit cu o doctoriţă care lucrează la Spitalul de Urgenţă.
3) Petre este căsătorit cu o actriţă care lucreaza la Teatrul Naţional.
4) Ştefan este meloman şi Petre este vânător.
5) Toţi melomanii sunt sentimentali.
6) Toţi vânătorii sunt mincinoşi.
7) Actriţele iubesc bărbaţii sentimentali.
8) Soţii au aceiaşi vecini.
9) Căsătoria şi vecinătatea sunt relaţii simetrice.
74
Concluzie:
Îl iubeşte soţia lui Petre pe Ştefan?

% Ipoteza 1
vecin1(stefan, petre).
% Ipoteza 2
căsătorit1(stefan, sotie_stefan).
doctorita(sotie_stefan).
lucreaza(sotie_stefan,urgenta).
% Ipoteza 3
casatorit1(petre, sotie_petre).
actrita(sotie_petre).
lucreaza(sotie_petre, national).
% Ipoteza 4
meloman(stefan).
vanator(petre).
% Ipoteza 5
sentimental(X) :- meloman(X).
% Ipoteza 6
mincinos(X) :- vanator(X).
% Ipoteza 7
iubeste(X, Y) :- actrita(X), sentimental(Y).
% Ipoteza 8
vecin(X, Y) :- casatorit(X, Z), vecin(Z, Y).
% Ipoteza 9
vecin(X, Y) :- vecin1(X, Y).
vecin(X, Y) :- vecin1(Y, X).
casatorit(X, Y) :- casatorit1(X, Y).
casatorit(X, Y) :- casatorit1(Y, X).
% Concluzia
concluzie :- casatorit(petre, Sotie), iubeste(Sotie, stefan).
% ?- concluzie.

6.5 Problema unicornului


Problema unicornului este o problemă celebră formulată de Lewis Carroll în cartea sa
"Alice în ţara minunilor".
Ipoteze:
1) Leul minte luni, marţi şi miercuri şi spune adevărul în toate celelalte zile.
2) Unicornul minte joi, vineri şi sâmbătă şi spune adevărul în toate celelalte zile.
3) Astăzi Leul spune: "Ieri a fost una din zilele în care eu mint."
75
4) Tot astăzi Unicornul spune: "Ieri a fost una din zilele în care eu mint."
Concluzie:
Ce zi este astăzi? (Răspuns: joi).

Prima variantă:
% zilele săptămânii
urmeaza(luni, marti). urmeaza(marti, miercuri). urmeaza(miercuri, joi).
urmeaza(joi, vineri). urmeaza(vineri, sambata). urmeaza(sambata,duminica).
urmeaza(duminica, luni).
% Ipoteza 1
% minte(Animal, Zi) - Animal minte în ziua Zi.
minte(leu, luni). minte(leu, marti). minte(leu, miercuri).
% Ipoteza 2
minte(unicorn, joi). minte(unicorn, vineri). minte(unicorn, sambata).
% Ipotezele 3 şi 4
% spune(Animal, Zi) - Animal spune adevărul în ziua Zi.
spune(Animal, Azi) :- urmeaza(Ieri, Azi), minte(Animal, Ieri).
posibil(Animal, Azi) :- spune(Animal, Azi), not(minte(Animal, Azi)).
posibil(Animal, Azi) :- minte(Animal, Azi), not(spune(Animal, Azi)).
% Pregătire concluzie
azi(Azi) :- posibil(leu, Azi), posibil(unicorn, Azi).
% Concluzia
% ?- azi(Azi).
A doua variantă:
% zilele săptămânii
urmeaza(luni, marti). urmeaza(marti, miercuri). urmeaza(miercuri, joi).
urmeaza(joi, vineri). urmeaza(vineri, sambata). urmeaza(sambata,duminica).
urmeaza(duminica, luni).
% Ipoteza 1
% spune(Animal, Zi, Ce) - Ce spune Animal în fiecare Zi.
spune(leu, luni, minciuna). spune(leu, marti, minciuna). spune(leu, miercuri,
minciuna).
spune(leu, joi, adevar). spune(leu,vineri, adevar). spune(leu, sambata, adevar).
spune(leu, duminica, adevar).
% Ipoteza 2
spune(unicorn, luni, adevar). spune(unicorn, marti, adevar).
spune(unicorn, miercuri, adevar). spune(unicorn, joi, minciuna).
spune(unicorn, vineri, minciuna). spune(unicorn, sambata, minciuna).
spune(unicorn, duminica, adevar).
% Ipotezele 3 şi 4
76
enunt(Animal, Azi) :- urmeaza(Ieri, Azi), spune(Animal, Ieri, minciuna).
% adevarul - Ce înseamna că minte, pentru Prolog; este un metapredicat.
adevarul(Animal, Zi, Enunt, not(Enunt)) :- spune(Animal, Zi, minciuna).
adevarul(Animal, Zi, Enunt, Enunt) :- spune(Animal, Zi, adevar).
% Pregătire concluzie
azi(Azi) :-
adevarul(leu, Azi, enunt(leu, Azi), Adevar1),
Adevar1, % sau call(Adevar1)
adevarul(unicorn, Azi, enunt(unicorn, Azi), Adevar2),
Adevar2. % sau call(Adevar2)
% Concluzia
% ?- azi(Azi).
6.6 Exerciţii propuse
Să se rescriere enunţurile următoarelor probleme sub formă de clauze Prolog şi să se
demonstreze concluziile prin formularea unor interogări corecte în Prolog.
EP1. Ipoteze
1. Oricine poate citi este literat.
2. Delfinii nu sunt literaţi.
3. Anumiţi delfini sunt inteligenţi.
Concluzie
Există fiinţe inteligente care nu pot citi.

EP2. Ipoteze
1. Dacă oraşul X este legat de oraşul Y prin drumul D şi pot circula biciclete pe
drumul D, atunci se poate merge de la X la Y.
2. Dacă oraşul X este legat de oraşul Y prin drumul D, atunci şi oraşul Y este legat
de oraşul X prin drumul D.
3. Dacă se poate merge de la X la Y şi de la Y la Z, atunci se poate merge de la X
la Z.
4. Oraşul a este legat de oraşul b prin drumul d1.
5. Oraşul b este legat de oraşul c prin drumul d2.
6. Oraşul a este legat de oraşul c prin drumul d3.
7. Pe drumul d1 pot circula biciclete.
8. Pe drumul d2 pot circula biciclete.
Concluzie
Se poate merge de la oraşul a la oraşul c.
EP3. Se înlocuieşte ipoteza 8 de la EP2 cu “Pot circula biciclete fie pe drumul d1, fie pe
drumul d3, dar nu pe ambele în acelaşi timp.” Să se demonstreze aceeaşi concluzie.
EP4. Ipoteze
77
1. Marcus era om.
2. Marcus era pompeian.
3. Toţi pompeienii erau romani.
4. Cezar era dictator.
5. Fiecare roman îi era devotat lui Cezar sau îl ura.
6. Fiecare om îi este devotat cuiva.
7. Oamenii încearcă să îi asasineze pe dictatorii faţă de care nu sunt devotaţi.
8. Marcus a încercat să îl asasineze pe Cezar.
Concluzii
1. Marcus nu îi era devotat lui Cezar.
2. Marcus îl ura pe Cezar.

7 Prelucrarea listelor în Prolog


Structura de date listă este cea mai frecvent utilizată structură de date în programele Prolog.
Acest capitol prezintă în detaliu lucrul cu liste în Prolog.
7.1 Predicate de prelucrare a listelor
Cele mai frecvente predicate utilizate în prelucrarea listelor sunt cel de apartenenţă a unui
element la o listă şi concatenarea a două liste, care au fost prezentate în prima parte a
lucrării. Reamintim aceste definiţii:
member(Elem, [Elem|_]) :- !.
member(Elem, [_|Rest]) :- member(Elem, Rest).
append([], L2, L2).
append([Prim1|Rest1], Lista2, [Prim1|Rest3]) :- append(Rest1, Lista2, Rest3).
In continuare se prezintă o serie de alte predicate utile în prelucrarea listelor.
Eliminarea unui obiect dintr-o listă. Să scriem un predicat care elimină un obiect
dintr-o listă. Astfel, elim(a, [a, b, c], L) va returna în L lista [b, c]. Implementarea în Prolog
este:
% elim(+El,+Lista,-ListaRez)
elim(X, [X | Rest], Rest).
elim(X, [Y | Rest], [Y | Rest1]) :- elim(X, Rest, Rest1).

Conform acestei implementări, elim nu va elimina decât o apariţie a elementului


căutat. Astfel, eliminarea lui a din lista [a,b,a,c] va genera două soluţii posibile:
?- elim(a, [a, b, a, c], L).
L = [b, a, c];
L = [a, b, c];
no

78
Dar este posibilă şi întrebarea “Ce liste din care se elimina a dau ca rezultat lista [b, c]?”:
?- elim(a, L, [b, c]).
L = [a, b, c];
L = [b, a, c];
L = [b, c, a];
no

Incluziunea listelor. Fie un predicat care este adevărat dacă o listă este sublista alteia.
De exemplu,
sublist([c, d, e], [a, b, c, d, e, f]) este adevărat, iar
sublist([b, c, e], [a, b, c, d, e, f]) este fals. Ne putem folosi de predicatul deja scris
append. O listă S este sublistă a listei L dacă:
1) Există o descompunere a lui L în L1 şi L2 şi
2) Există o descompunere a lui L2 în S si L3.
Implementare:
% sublist(+SubLista,+Lista)
sublist(S, L) :- append(L1, L2, L), append(S L3, L2).
Această implementare are un mic defect: afişează lista vidă de mai multe ori. Încercaţi să
aflaţi de ce. O variantă care elimină acest defect este subset:
subset([], L).
subset([X | Rest], L) :- member(X, L), subset(Rest, L).

Liniarizarea listelor. Vom scrie predicatul liniar(ListaListe, Lista), unde ListaListe


este o listă de elemente care pot fi rândul lor liste, iar în Lista se construieşte liniarizarea
listei ListaListe:
% liniar(+Lista,ListaLiniarizata)
liniar([] , []).
liniar([[ ] | Rest], Rez) :- liniar(Rest, Rez).
liniar([X | Rest], [X | Rez]) :- X \= [], X \= [ _ | _ ], liniar(Rest, Rez).
liniar([[X | Rest] | RestList], Rez) :- liniar([X, Rest | RestList], Rez).

Un exemplu de execuţie este:


?- liniar([1, 2, [3, 4], [5, [6, 7], [[8], 9]]], L).
L = [1, 2, 3, 4, 5, 6, 7, 8, 9].
yes

Predicatul descomp(N, Lista) primeşte un număr întreg N şi întoarce o lista factorilor


primi ai numărului N; de exemplu: descomp(12, [2, 2, 3]) este adevărat.
79
% descomp(+N,?L)
descomp(N, L) :- factp(N, L, 2).
factp(1, [ ], _ ).
factp(N, [Divizor | Lista], Divizor) :-
N > 1, 0 is N mod Divizor, N1 is N // Divizor, factp(N1, Lista, Divizor).
factp(N,Lista,Divizor) :-
N > 1, not(0 is N mod Divizor), D1 is Divizor + 1, factp(N, Lista, D1).

Predicatul palindrom(Lista) verifică dacă o listă este palindrom. Un palindrom este o


secvenţă care, dacă este parcursă de la stânga la dreapta sau de la dreapta la stânga, este
identică; de exemplu: [a, b, c, b, a] sau [a, b, c, c, b, a].
% Idee: o listă este palindrom dacă este egală cu inversa ei.
palindrom(L) :- reverse(L, [], L).
reverse([], Acc, Acc).
reverse([X | Rest], Acc, L) :- reverse(Rest, [X | Acc], L).

7.2 Mulţimi
Mulţimile pot fi reprezentate în Prolog ca liste. Predicatul multime(L, M) transformă lista L
în mulţimea M.
multime([], []).
multime([X | Rest], Rez) :- member(X, Rest), mulţime(Rest, Rez).
multime([X | Rest], [X | Rez]) :- not(member(X, Rest)), multime(Rest, Rez).

Predicatul de definire a intersecţiei a două liste prezentat în secţiunea 4.4 se poate


aplica şi pentru obţinerea intersecţiei a două mulţimi. Prezentăm în continuare predicatul de
determinare a reuniunii a două mulţimi.
% reun(+L1,+L2,-L)
reun([],L,L).
reun([X | Rest], L, Rez) :-member(X,L), reun(Rest,L,Rez).
reun([X | Rest], L, [X | Rez]) :-not member(X,L), reun(Rest,L,Rez).

7.3 Problema drumurilor


Fie o bază de date cu drumuri între oraşe, de forma drum(oras1, oras2):
drum(bucuresti, ploiesti).
drum(bucuresti, cheia).
drum(cheia, brasov).
drum(brasov, bucuresti).
drum(cheia, sinaia).
drum(ploiesti, sinaia).

80
drum(ploiesti, brasov).

Predicatul traseu(X, Y, T) este adevărat dacă se poate ajunge de la oraşul X la oraşul


Y, calculând si traseul T între cele două oraşe. Drumurile sunt bidirecţional (dacă exista un
drum de la X la Y, atunci există implicit un drum de la Y la X).
member(X, [Y | T]) :- X == Y, ! ; member(X, T).
traseu(Y, X) :- traseu(X, Y, [X]).
traseu(Y, Y, Traseu) :- write(Traseu), nl.
traseu(X, Y, Traseu) :-
(drum(X, Z) ; drum(Z, X)), not member(Z, Traseu), traseu(Z, Y, [Z | Traseu]).
traseu( _ , _ , _ ) :- write($Nu exista traseu.$), nl.

% cateva teste
test :-
traseu(bucuresti, sinaia), traseu(sinaia, bucuresti),
traseu(bucuresti, ploiesti), traseu(ploiesti, bucuresti),
traseu(cheia, craiova).

Dacă apelăm predicatul test sistemul va răspunde:


?- test.
[bucuresti, brasov, cheia, sinaia]
[sinaia, ploiesti, bucuresti]
[bucuresti, brasov, cheia, sinaia, ploiesti]
[ploiesti, bucuresti]
Nu exista traseu.
yes

7.4 Exerciţii propuse


EP1. Aflaţi care este defectul predicatului sublist.
EP2. Folosind predicatul elim, puneţi întrebarea: “Ce elemente se pot elimina din
[a, b, a, c] şi ce listă rezultă în cazul fiecărei eliminări?”
EP3. Să se definească şi să se exemplifice cu câte două exemple în Prolog următoarele
predicate de prelucrare a listelor:
1) invers(Lista, ListaInversata) - inversează elementele unei liste; să se scrie
două variante ale predicatului de inversare a unei liste: o variantă în care lista
inversată este calculată pe ramura de revenire din recursivitate şi o variantă în
care lista inversată este calculată pe ramura de avans în recursivitate.

81
2) reun(Lista1, Lista2, ListaRez) - produce ListaRez care conţine reuniunea
elementelor din Lista1 şi din Lista2; se va da o implementere alternativă pe
baza predicatului multime din secţiunea 7.2.
3) rotire(Lista, Directie, Nr, ListaRez) - roteşte Lista cu un număr de Nr
elemente la stânga (dacă Directie = stg) sau la dreapta (dacă Directie = dr),
depunând rezultatul în ListaRez;
EP4. Să se scrie predicatul Prolog substitutie(X, Y, L1, L2), unde L2 este rezultatul
substituirii tuturor apariţiilor lui X din lista L1 cu Y, producând lista L2.
Ex: substitutie(a, x, [a, [b,a,] c], L2) va produce: L2 = [x, [b, x], c].
EP5. Să se scrie predicatul imparte(L, L1, L2) care împarte lista L în două subliste L1 şi
L2, care au un număr de elemente aproximativ egal, fără a calcula lungimea listei L.
Ex: imparte([a, b, c, d, e], L1, L2) va produce: L2 = [a, b, c] şi L3 = [d, e].

8 Mecanisme specifice Prolog


8.1 Exemple de utilizare a mecanismului cut
Reamintim funcţionarea mecanismului cut: să denumim scop părinte scopul care
identifică cu antetul clauzei ce conţine cut. Când cut este întâlnit ca scop, el reuşeşte
imediat, dar obligă sistemul să rămână la toate opţiunile făcute între momentul apelului
scopului părinte şi momentul întâlnirii cut-lui. Toate alternativele care rămân între scopului
părinte şi cut sunt ignorate. Să investigăm această funcţionare pentru următorul exemplu:
C :- P, Q, R, !, S, T, U.
C :- V.

A :- B, C, D.

?- A.

Execuţia scopului C va fi afectată de cut astfel: procesul de backtracking va fi posibil


pe lista de scopuri P, Q, R; totuşi, îndată ce cut-ul este atins, toate soluţiile alternative ale
listei de scopuri P, Q, R sunt eliminate. Clauza alternativă despre C:
C :- V.

va fi şi ea ignorată. Procesul de acktracking va fi totuşi încă posibil pe lista de scopuri


S, T, U. Scopul părinte al clauzei care conţine cut-ul este scopul C din clauza:
A :- B, C, D.

82
Prin urmare cut-ul va afecta doar execuţia scopului C. Pe de altă parte, el nu va fi vizibil în
scopul A. Astfel, procesul de backtracking în cadrul listei de scopuri B, C, D va rămâne
activ indiferent de cut-ul din clauza folosită pentru a satisface C.
Am definit predicatul care calculează maximul dintre două numere (în prima parte)
astfel:
% max(+X, +Y, -Max).
max(X, Y, X) :- X >= Y.
max(X, Y, Y) :- X < Y.
Cele două reguli sunt mutual exclusive. Dacă prima reuşeşte atunci a doua va eşua. Dacă
prima eşuează, atunci a doua trebuie să reuşească. Prin urmare, o reformulare mai eficientă
este posibilă:
dacă X ≥ Y atunci Max = X
altfel Max = Y.
Aceasta se scrie în Prolog utilizând cut astfel:
max(X, Y, X) :- X >= Y, !.
max( _ , Y, Y).
Să vedem ce se întâmplă însă atunci când dorim să folosind puterea generativă a
limbajului Prolog:
% max(?X, ?Y, ?Max) % max(?X, ?Y, ?Max)
max(X, Y, X) :- X >= Y. max(X, Y, X) :- X >= Y, !.
max(X, Y, Y) :- X < Y. max( _ , Y, Y).

?- max(X, 3, 4). X=4 X=4


?- max(X, 3, 3). X=3 X=3
?- max(3, X, 4). X=4 X=4
?- max(X, X, 3). X=3 X=3
Cazuri în care predicatul max trebuie să eşueze:
?- max(X, 3, 2). No no
?- max(3, X, 3). No X=3
?- max(3, X, 2). No X=2
?- max(X, Y, 3). No X = _0038 Y = 3
?- max(X, 3, X). no X=3
?- max(3, X, Y). no X = _0038 Y = _0038
?- max(X, 3, Y). no X = _0038 Y = 3

Din exemplele prezentate se observă că varianta mai “explicită” se comportă corect în


toate cazurile, în timp ce varianta “eficientă” generează rezultate eronate. De aceea, înainte

83
de a utiliza cut trebuie să ne gândim în ce context şi cum vor fi apelate predicatele în care el
apare, deoarece adesea folosirea lui cut creşte posibilitatea apariţiei erorilor de programare.
8.2 Negaţia ca insucces
Faptul că ceva nu este adevărat în Prolog poate fi exprimat explicit în Prolog utilizând
predicatul special fail, care eşuează întotdeauna, forţând scopul părinte să eşueze. Enunţul
“Maria iubeşte toate animalele cu excepţia şerpilor” se exprimă în Prolog astfel:
iubeşte(maria, X) :- şarpe(X), !, fail.
iubeşte(maria, X) :- animal(X).
Prima regulă va avea grijă de şerpi: dacă X este şarpe atunci cut-ul va împiedica
backtracking-ul (excluzând astfel a doua regulă) şi predicatul fail va cauza eşuarea. Cele
două clauze pot fi compactate într-una singură astfel:
iubeşte(maria, X) :- şarpe(X), !, fail ; animal(X).
La fel se poate scrie un predicat care testează dacă două numere sunt diferite:
diferite(X, X) :- !, fail.
diferite( _ , _ ).
sau:
diferite(X, Y) :- X = Y, !, fail ; true.
Predicatul not este predefinit în ARITY Prolog ca un operator prefixat, astfel încât
not(şarpe(X)) se poate scrie ca not şarpe(X). Exemplele anterioare se pot rescrie cu not
astfel:
iubeşte(maria, X) :- animal(X), not(şarpe(X)).
diferite(X, Y) :- not ( X = Y ).
Predicatul not funcţionează deoarece raţionamentul Prolog se bazează pe ipoteza
lumii închise. Not-ul definit prin eşuare nu corespunde exact cu negaţia din logica
matematică. De aceea folosirea lui not trebuie făcută cu atenţie.
In ARITY Prolog, mai există un cut parţial, numit snip “[! !]”, care specifică o listă
de scopuri care sunt ignorate în backtracking. Fie scopul p definit prin regulile:
p :- a, b, [! c, d, e !], f, g.
p :- t, u, v.

Dacă scopurile a, b, c, d şi e reuşesc scopul părinte al lui p rămâne fixat pe prima


regulă a lui p şi prin backtracking se pot resatisface scopurile a, b, f şi g. De asemenea, atât
timp cât nu s-au satisfăcut toate scopurile din cadrul snip-ului, până la satisfacerea scopului
e se pot resatisface prin backtracking scopurile a, b, c şi d. Dacă nu se poate satisface deloc

84
f, după execuţia tuturor satisfacerilor scopurilor a, b, c şi d, se trece la regula a doua a
predicatului p.
Ca o aplicaţie la cele discutate până acum, iată trei exemple:
Ex1: Plasarea a opt regine pe o tablă de şah astfel încât ele să nu se atace între ele:
% solutie(?Solutie)
solutie([]).
solutie([X/Y | CelelalteRegine]) :-
solutie(CelelalteRegine),
member(Y, [1, 2, 3, 4, 5, 6, 7, 8]),
not ataca(X/Y, CelelalteRegine).

% ataca(+Regina, +CelelalteRegine) - verifică dacă Regina atacă CelelalteRegine


ataca(X/Y, CelelalteRegine) :-
member(X1/Y1, CelelalteRegine),
(Y1 = Y; Y1 is Y + X1 - X; Y1 is Y - X1 + X).

% member(?X, ?Lista) - member generativ


member(X, [X| _ ]).
member(X, [ _ |L]) :- member(X, L).

% model de soluţie
model([1/Y1, 2/Y2, 3/Y3, 4/Y4, 5/Y5, 6/Y6, 7/Y7, 8/Y8]).

% toate soluţiile
toate :- model(L), assert(count(1)), !,
( soluţie(L), retract(count(N)), N1 is N + 1, assert(count(N1)),
write(N), write($.$), [! N < 10, tab(3) ; tab(2) !], write(L), nl, fail
; retract(count( _ )), true).

Cut-ul a fost introdus doar pentru eficienţă (cut verde), iar parantezele care închid
disjuncţia de conjuncţii de scopuri au fost puse din cauza lui. Clauzele
count(NumărSoluţie) au fost folosite pentru numerotarea soluţiilor, iar snip-ul pentru
indentarea lor. Predicatul predefinit tab(N) afişează N blank-uri.
Ex2: Fie următoarele predicate:
a(X) :- (X is 1; X is 2), tab(1), write(a(X)).
b(X) :- X = 1, write($ !b$), !, fail ; write($ b$).
c(X) :- X > 1, write($ !c$), !, fail ; write($ c$).

p :- nl, write([p1]), a(X), [! b(X), fail !], c(X).


85
p :- nl, write([p2]), a(X), [! b(X) !], c(X).
p :- nl, write([p3]), a(X), [! b(X) !], not c(X).
p :- nl, write([p4]).

La interogarea:
?- p.

sistemul va răspunde:
[p1] a(1) !b a(2) b
[p2] a(1) !b a(2) b !c
[p3] a(1) !b a(2) b !c
yes

Ex3. Să scriem predicatul unifică(X, Y) care reuşeşte dacă X şi Y unifică, fără însă a
modifica pe X sau pe Y. Folosind predicatul not, putem scrie:
unifica(X, Y) :- not not (X = Y).

Dacă X şi Y unifică, adică X = Y reuşeşte, atunci not (X = Y) eşuează, unificarea fiind


desfăcută, şi not not (X = Y) reuşeşte (deci unifică(X, Y) reuşeşte), X şi Y rămânând
neunificate.
8.3 Utilizarea operatorilor
Notaţia cu operatori permite programatorului să ajusteze sintaxa programelor conform
propriilor dorinţe. Înţelegerea programelor este mult îmbunătăţită atunci când se folosesc
operatori.
Exemplu: Dacă definim operatorii joaca şi si:
:- op(300,xfx, joaca).
:- op(200, xfy, si).

atunci următorii termeni sunt obiecte legale din punct de veder sintactic:
Termen1 = tom joaca fotbal si tenis.
Termen2 = susan joaca tenis si volei si ping-pong.

Calculele aritmetice sunt făcute prin proceduri predefinite. Evaluarea unei expresii
aritmetice este forţată de procedura is şi de predicatele de comparaţie <, =<, etc. De
exemplu:
?- X = 3 / 2
X=3/2
?- X is 3 / 2
X = 1.5
86
În continuare este prezentat ca exemplu un mini-sistem de reguli de producţie:
% operatorii fundamentali
:- op(100, yfx, si).
:- op(110, yfx, sau).
:- op(120, fx, daca).
:- op(130, xfy, atunci).

% intreb(+Fapt) - deduce valoare de adevar unui fapt


X si Y :- intreb(X), intreb(Y).
X sau Y :- intreb(X) ; intreb(Y).
intreb(X) :- cunosc(X).
intreb(X) :- call(X).
intreb(X) :- daca Y atunci X, [! intreb(Y) !], adaug(X).
intreb(X) :- intreb_utiliz(X).

% adaug(+Fapt) - adauga un fapt cunoscut ca fiind adevarat in memoria


% sistemului sub forma cunosc(Fapt).
adaug(X) :- not var(X), (cunosc(X) ; asserta(cunosc(X))), !.

% intreb_utiliz(F) - intreaba utilizatorul despre valoarea de adevar a unui fapt


% despre care nu se poate deduce nimic
intreb_utiliz(X) :- X =.. [Op, Y], var(Y), !, fail.
intreb_utiliz(X) :- X =.. [Op, Y, Z], (var(Y) ; var(Z)), !, fail.
intreb_utiliz(X) :- X =.. [Op, _ , _ , _ | _ ], !, fail.
intreb_utiliz(X) :-
not(var(X)), write(X), write('? [y/_ ] '), (read(y), adaug(X) ; fail ), !.

% ret - elimina toate faptele din memoria sistemului


ret(X) :- retract(cunosc(X)), fail ; true.
ret :-
nl, write('fapte retractate:'), nl,
retract(cunosc(X)), tab(2), write(cunosc(X)), nl, fail
; true.

% operatorii utilizatorului
:- op(10, xf, zboara).
:- op(20, xfx, are).
:- op(20, xfx, este).

% faptele cunoscute de utilizator (fapte Prolog).


87
coco are cioc. % echivalent cu: are(coco, cioc)
coco are pene.
coco are picioare.

% regulile utilizatorului pentru generarea de noi fapte


% (aceste reguli sunt tot fapte Prolog)
daca X are picioare atunci X este fiinta.
% echivalenta cu: atunci(daca(are(X, picioare)), este(X, fiinta)).
daca X este fiinta si X are pene atunci X zboara.
daca X este fiinta si X are pene atunci X are cioc.
daca X zboara si X are cioc atunci X este pasare.
daca X este pasare si X este fiinta atunci X este frumos.
daca X este frumos sau X este peşte atunci X este vânat.
daca X are solzi atunci X este peşte.
daca X este vânat atunci X este nefericit.

% posibile intrebari ale utilizatorului


a :- intreb(coco este peşte), ret.
b :- intreb(coco este X), write(coco este X), nl, fail ; ret.
c :- intreb(coco are X), write(coco are X), nl, fail ; ret.
d :- intreb(Z), write(Z), nl, fail ; ret.
e :- intreb(X este Y), write(X este Y), nl, fail ; ret.

Predicatul ret a fost apelat doar pentru a porni de fiecare dată cu memoria vidă. Altfel,
pe măsură ce răspunde întrebărilor utilizatorul, memoria sistemului creşte (sistemul
“învaţă”).
8.4 Fişiere
Încărcarea programelor în sistem se poate face prin două predicate predefinite,
consult(Fişier) şi reconsult(Fişier). Efectul lui consult este acela că toate clauzele din
Fişier sunt citite şi vor fi folosite de Prolog atunci când va răspunde întrebărilor puse de
utilizator. Dacă mai este consultat şi alt fişier, clauzele din acesta vor fi adăugate la sfârşitul
setului de clauze în baza de cunoştinţe Prolog.
Prolog poate accepta şi comenzi direct de la terminal, care corespunde
pseudo-fişierului user. După evaluarea scopului:
?- consult(user)
Prolog aşteaptă introducerea clauzelor de la terminal.
Există o prescurtare a comenzii de consultare a fişierelor. Fişierele sunt consultate
dacă numele lor sunt puse într-o listă scrisă ca scop. De exemplu:

88
?- [fisier1, fisier2, fisier3]
este echivalentă cu:
?- consult(fisier1), consult(fisier2), consult(fisier3).
Predicatul reconsult(Fişier) are acelaşi efect ca şi consult(Fişier), cu o singură
excepţie. Dacă există clauze în Fişier despre o relaţie care a fost definită anterior, vechea
definiţie va fi suprascrisă de noile clauze despre relaţie din Fişier. Diferenţa dintre consult
şi reconsult este aceea că predicatul consult adaugă întotdeauna noi clauze, în timp ce
reconsult redefineşte relaţii definite anterior. Reconsultarea cu reconsult nu va afecta însă
relaţiile despre care nu există nici o clauză în Fişier. Detaliile despre (re)consultare depind
de implementarea de Prolog.
În ARITY Prolog, la reconsultare, redefinirea unor relaţii poate fi parţială. ARITY
Prolog are şi alte predicate predefinite pentru manipularea fişierelor şi bazei de cunoştinţe
Prolog (ele sunt descrise în fişierul arity.hlp). De exemplu, predicatul listing(…) listează
clauzele din baza de date.
Intrarea şi ieşirea pentru date (altele decât cele asociate interogării programului) sunt
făcute prin proceduri predefinite. Fişierele sunt accesate secvenţial. Există un flux curent de
date de intrare şi un flux curent de date de ieşire. Terminalul utilizatorului este tratat ca un
fişier, fiind denumit user.
Comutarea între fluxuri de date se poate face cu:
• see(Fisier) - fişierul Fisier devine fluxul curent de intrare
• tell(Fisier) - fişierul Fisier devine fluxul curent de ieşire
• seen - închide fluxul curent de intrare
• told - închide fluxul curent de ieşire

Fişierele pot fi citite şi scrise în două moduri: ca secvenţe de caractere şi ca secvenţe


de termeni; în al doilea caz termenii sunt urmaţi de punct şi despărţiţi de cel puţin un blank
sau un caracter sfârşit de linie (CR). Procedurile predefinite pentru citirea şi scrierea
caracterelor şi termenilor sunt:
• read(Termen) - citeşte în Termen următorul termen din fluxul curent de intrare
• write(Termen) - scrie termenul Termen în fluxul curent de ieşire
• put(CodCaracter) - scrie caracterul care are codul ASCII CodCaracter
• get0(CodCaracter) - citeşte următorul caracter şi întoarce codul lui ASCII
• get(CodCaracter) - citeşte următorul caracter tipăribil şi întoarce codul lui
ASCII

Pentru formatarea ieşirii există două proceduri:


• nl - scrie un caracter sfârşit de linie
• tab(N) - scrie N caractere blank
89
Procedura name(Atom, ListăCoduri) descompune şi construieşte atomi.
ListăCoduri este lista codurilor ASCII ale caracterelor din Atom. Exemple:
name( ana, [97, 110, 97] ).
name( 'Ana', [65, 110, 97] ).
name( 'Ana vine.', [65, 110, 97, 32, 118, 105, 110, 101, 46] ).

8.5 Exerciţii propuse


EP1. Fie următorul program Prolog:
este(a).
este(b).
este(c).
exista(X) :- este(X) ; true.

Câte soluţii are fiecare din următoarele scopuri:


?- exista(A), exista(B), exista(C), A \= a, B \= b, C \= c.
?- exista(A), exista(B), exista(C), !, A \= a, B \= b, C \= c.
?- exista(A), !, exista(B), exista(C).

EP2. Să se scrie două variante ale predicatului de calcul al sumei unei liste de întregi: o
variantă în care calculul se face pe ramura de avans în recursivitate şi o a doua variantă în
care calculul sumei se face pe ramura de revenire din recursivitate, afişând lista parţială la
fiecare apel recursiv.
EP3. Să se scrie un predicat care verifică dacă un număr întreg pozitiv poate fi exprimat
ca suma a două pătrate perfecte. (Ex: 65 = 16 + 49 = 4 2 + 7 2 ).
EP4. Să se scrie un predicatul care citeşte în buclă numere şi răspunde dacă sunt prime
sau nu, până la întâlnirea unui număr negativ, folosind predicatul predefinit repeat.
EP5. Să se scrie un program care calculează numărul de zile dintre două date exprimate
sub forma Zi-Luna, presupunând că datele se referă la acelaşi an. Caracterul “-” se va defini
ca un operator infixat.
Ex: interval(3-martie, 7-aprilie, I) produce I = 35.
EP6. Să se scrie un predicat care implementează operaţia de diferenţă între două mulţimi.
Mulţimile sunt reprezentate prin liste.
Ex: diferenta([1, 2, a, b], [b, 3, 2, d], D) produce D = [1, a].
EP7. Să se scrie un program care citeşte propoziţii simple (afirmaţii şi întrebări, pe care le
adaugă în baza de cunoştinţe Prolog), având una din formele:
_ este un _.
_ este o _.
90
Un _ este un/o _.
O _ este un/o _.
Este _ un/o _ ?

şi răspunde adecvat (da, nu sau necunoscut) la întrebări pe baza afirmaţiilor introduse.


Exemplu: Radu este un student. Un om este o fiinţă. Radu este un om. Este Maria o
persoană?
EP8. Să se defineasca operatorii Prolog este, are, un etc. astfel încât să poata fi introduse
clauze de forma:
diana este secretara lui toma.
maria are un pian.
şi sistemul să poata răspunde la întrebări de tipul:
?-Cine este secretara lui toma.
Cine=Diana
?-diana este Ce.
Ce=secretara lui toma
?-maria are un Ce.
Ce=pian
EP9. Să se scrie un program Prolog de evaluare a unei expresii aritmetice care conţine
variabile, constante întregi, paranteze şi operatorii: +, -, *, /. Valorile variabilelor sunt date
sub forma de fapte Prolog prin predicatul valoare(Variabila, Valoare); de exemplu
valoare(x, 100) sau valoare(y, -50).
EP10. Să se scrie un program care determină negarea unei formule în logica cu propoziţii.
Conectorii logici consideraţi sunt: not, and, or şi implies. Să se dea definiţii adecvate
pentru aceşti operatori.
Ex: neaga(p implies (q and not r),E) va produce E = p and (not q or r)
EP11. Să se scrie predicatul cauta(Cuvant, F) care caută cuvântul Cuvant în fişierul text
F.
EP12. Să se scrie predicatul cuburi(F1, F2), care citeşte numere întregi din fişierul text F1
şi scrie cuburile lor în fişierul text F2.
EP13. Utilizând predicatul bagof, scrieţi predicatul parti_mult(Mult, Submult) care
calculează în Submult mulţimea părţilor mulţimii Mult. Reprezentaţi mulţimile sub formă
de liste.
Exemplu: parti_mult( [1, 2, 3], [ [ ], [1], [2], [3], [1, 2], [1, 3], [2, 3] ] ).

91
9 Sortare şi căutare
9.1 Metoda de sortare prin generare şi testare
Utilizând structura de control a limbajului Prolog, se poate realiza foarte simplu sortarea
unei secvenţe de elemente utilizând metoda generare şi testare. Această metodă de
rezolvare a problemelor, utilizată în inteligenţa artificială dar puţin potrivită pentru o
sortare, are la bază următoarea idee: o componentă generatoare construieşte soluţii
candidate şi o a doua componentă, componenta de testare, verifică fiecare soluţie candidată
pentru a vedea dacă este sau nu o soluţie a problemei. În acest fel se pot obţine fie toate
soluţiile problemei, fie una singură. Această metodă poate fi exprimată succint în Prolog
astfel:
gaseste(Solutie) :-
genereaza(Solutie),
testeaza(Solutie).

Metoda este în general ineficientă chiar în cazul problemelor tipice de inteligenţă


artificială care necesită un proces de căutare a soluţiei. Metoda este extrem de ineficientă în
cazul rezolvării problemelor de sortare, pentru care există algoritmi eficienţi de rezolvare.
Cu toate acestea, se prezintă în continuare soluţia de sortare în ordine crescătoare a unei liste
de întregi prin metoda generare şi testare ca exerciţiu Prolog.
% sortare(+Lista, -ListaSortata) - sortare prin metoda generare şi testare
sortare(Lista, ListaSortata) :- permut(Lista, ListaSortata), ordonată(ListaSortata).

% permut(+Lista, -PermutareLista)
permut([], []).
permut(Lista, [Prim | Rest]) :- elim(Prim, Lista, L), permut(L, Rest).

% elim(+Element, +Lista, -ListaMinusElement)


elim(Elem, [Elem | Rest], Rest).
elim(Elem, [Prim | Rest], [Prim | L]) :- elim(Elem, Rest, L).

% rel(+X, +Y) - verifică dacă X şi Y respectă relaţia de ordine rel


rel(X, Y) :- X =< Y.

% ordonata(+Lista) - verifică dacă Lista este sortată după relaţia rel(X, Y)


ordonata([ _ ]).
ordonata([Prim, Secund | Rest]) :- rel(Prim, Secund), ordonata([Secund | Rest]).

Se observă că sortarea se face prin generarea permutărilor elementelor din listă şi


verificarea dacă o permutare generată este o secvenţă sortată. Componenta generatoare este

92
predicatul permut(Lista, ListaSortata) şi componenta de testare este predicatul
ordonata(Lista). Deşi implementarea este simplă, exploatând facilităţile nedeterministe ale
limbajului Prolog, ea este foarte ineficientă.
9.2 Metoda de sortare prin inserţie
Sortarea prin inserţie a unei liste de elemente L = [H | T] se poate exprima recursiv astfel:
se sortează mai întâi lista T în TS şi apoi se inserează elementul H în lista TS acolo unde îi
este locul conform relaţiei de ordine rel(X, Y).
% isort1(+Lista, -ListaSortata) - sortare prin insertie, varianta 1
isort1([], []) :- !.
isort1([H | T], LS) :- isort1(T, TS), insereaza(H, TS, LS).

% rel(+X, +Y) - verifica daca X si Y respecta relatia de ordine


rel(X, Y) :- X =< Y.

% insereaza(+Element, +Lista, -ListaPlusElement) -


% insereaza Element in Lista sortata astfel incat ListaPlusElement sa ramana sortata
insereaza(Elem, [], [Elem]).
insereaza(Elem, [Prim | Rest], [Elem, Prim | Rest]) :- rel(Elem, Prim), !.
insereaza(Elem, [Prim | Rest], [Prim | L]) :-
not rel(Elem, Prim), insereaza(Elem, Rest, L).

Predicatul cut folosit în definirea predicatului insereaza este un cut verde. Se poate rescrie
predicatul insereaza folosind un cut roşu astfel:
insereaza(Elem, [], [Elem]).
insereaza(Elem, [Prim | Rest], [Elem, Prim | Rest]) :- rel(Elem, Prim), !.
insereaza(Elem, [Prim | Rest], [Prim | L]) :- insereaza(Elem, Rest, L).

A doua variantă de sortare prin inserţie este următoarea: Lista poate fi privita la un
moment dat ca L' = PS::PN, unde PS este partea sortată şi PN = [X | T] este partea
nesortată. Se ia primul element X din PN şi se inserează în PS. Algoritmul porneşte cu
PS = [] şi PN = L, şi se opreşte când PN = [], în acel moment PS fiind chiar lista sortată. Se
observa că PS are rol de acumulator, rezultatul final fiind întors în al treilea parametru
(constructia rezultatului se face pe apelul recursiv).
% isort2(+Lista, -ListaSortata) - sortare prin insertie, varianta 2
isort2(L, LS) :- isort2( [], L, LS).
isort2(PS, [], PS).
isort2(PS, [X | T], LS) :- insereaza(X, PS, PS1), isort2(PS1, T, LS).

93
9.3 Metoda de sortare rapidă
Sortarea rapidă (quicksort) a unei liste de elemente L se defineşte recursiv astfel:
1. Elimină un element Pivot din lista L şi obţine Rest = L - Pivot.
2. Împarte lista Rest în două liste: ListaInf, cu toate elementele din Rest inferioare
elementului Pivot şi ListaSup cu toate lementele din Rest superioare elementului
Pivot.
3. Sortează ListaInf şi obţine ListaInfSort.
4. Sortează ListaSup şi obţine ListaSupSort.
5. Concatenează listele ListaInfSort, lista formata din Pivot, ListaSupSort şi obţine
ListaSortată.
Predicatul quicksort(Lista, ListaSortată), definit mai jos, implementează acest
algoritm. Elementul Pivot este considerat, în implementare următoare, primul element al
listei Lista, iar împărţirea listei în ListaInf şi ListaSup în funcţie de elementul pivot se face
cu ajutorul predicatului scindează(Element, Lista, ListaInf, ListaSup).
% quicksort(+Lista, -ListaSortata) - sortare prin metoda sortării rapide
quicksort([], []).
quicksort([Pivot | Rest], ListaSortata) :-
scindează(Pivot, Rest, L1, L2),
quicksort(L1, L1Sortata), quicksort(L2, L2Sortata),

% conc(+L1, +L2, -L) - concatenează listele L1 şi L2 în lista L


conc(L1Sortata, [Pivot | L2Sortată], ListaSortata).
conc([], L, L).
conc([Prim | Rest], L, [Prim | Rest1]) :- conc(Rest, L, Rest1).

% scindeaza(+Element, +Lista, -ListaInf, -ListaSup) -


% împarte Lista în ListaInf (cu elemente inferioare lui Element) şi ListaSup
% (cu elemente superioare lui Element) în functie de relaţia rel(X, Y)
scindeaza(Elem, [], [], []).
scindeaza(Elem, [Prim | Rest], [Prim | L1], L2) :-
not rel(Elem, Prim), !, scindeaza(Elem, Rest, L1, L2).
scindeaza(Elem, [Prim | Rest], L1, [Prim | L2]) :-
rel(Elem, Prim), scindeaza(Elem, Rest, L1, L2).

Să testăm acum implementările metodelor de sortare prezentate şi să comparăm


rezultatele lor cu rezultatul produs de predicatul sort, care este predefinit în ARITY Prolog:
% testarea metodelor de sortare
test(F) :- L = [2, 2, 4, 6, 9, 8, 1, 3, 5, 7, 0], P =.. [F, L, S], call(P), write(P), nl.
test :- test(isort1), test(isort2), test(quicksort), test(sort).
94
Testarea va decurge astfel:
?- test.
isort1([2,2,4,6,9,8,1,3,5,7,0], [0,1,2,2,3,4,5,6,7,8,9])
isort2([2,2,4,6,9,8,1,3,5,7,0], [0,1,2,2,3,4,5,6,7,8,9])
quicksort([2,2,4,6,9,8,1,3,5,7,0], [0,1,2,2,3,4,5,6,7,8,9])
sort([2,2,4,6,9,8,1,3,5,7,0], [0,1,2,2,3,4,5,6,7,8,9])

95
9.4 Arbori binari
Fie reprezentarea în Prolog a arborilor binari definită în secţiunea 5.4:
1) arborele vid este codificat cu nil;
2) un arbore nevid este codificat cu arb(Cheie, SubarboreStang,
SubarboreDrept).
Traversarea unui arbore binar, cu afişarea cheilor din noduri, se implementează în
Prolog foarte uşor folosind facilităţile recursive ale limbajului. Predicatul rsd(Arbore)
realizează afişarea cheilor din Arbore în ordinea rădăcină-stânga-dreapta.
% rsd(+Arbore) - parcurge arborele binar Arbore
% în ordinea rădăcină-stânga-dreapta, afişând cheile arborelui
rsd(nil).
rsd(arb(Radacina, SubarboreStang, SubarboreDrept)) :-
write(Radacina), write(' '),
rsd(SubarboreStang),
rsd(SubarboreDrept).

Dacă se consideră cazul arborilor binari de căutare (cu chei întregi), se pot defini trei
predicate: caut(Cheie, Arbore), de căutare a unei chei în arbore, care reuşeşte dacă cheia
este în arbore şi eşuează în caz contrar; inser(Cheie, Arbore, ArboreRez), de inserare a
unei chei în arbore, cu argumentele Cheie şi Arbore instanţiate şi argumentul ArboreRez
sintetizat de program; şi elim(Cheie, Arbore, ArboreRez), care şterge o cheie dintr-un
arbore.
% caut(+Cheie, +Arbore) - reuşeşte dacă Cheie este în
% arborele binar de căutare Arbore, eşuează în caz contrar
caut(Cheie, arb(Cheie, _ , _ )) :- !.
caut(Cheie, arb(Radacina, ArbStg, _)) :- Cheie < Radacina, caut(Cheie, ArbStg).
caut(Cheie, arb(Radacina, _ , ArbDr)) :- Cheie > Radacina, caut(Cheie, ArbDr).
Prima clauză a predicatului caut reuşeşte dacă cheia este în arbore. Pentru a impiedica
o posibilă resatisfacere, deci găsirea unei alte apariţii a cheii de căutare în arbore, s-a
introdus în acest caz predicatul cut. Dacă se doreste, de exemplu, afişarea tuturor apariţiilor
cheii de căutare în arbore, se va elimina acest cut şi predicatul caut va avea atâtea soluţii
câte apariţii ale cheii de căutare există în arbore.
% inser(+Cheie, +Arbore, -ArboreRez) - inserează Cheie în
% arborele binar de căutare Arbore şi produce ArboreRez
inser(Cheie, nil, arb(Cheie, nil, nil)).
inser(Cheie, arb(Cheie, ArbStg, ArbDr), arb(Cheie, ArbStg, ArbDr)):-!.
inser(Cheie, arb(Radacina, ArbStg, ArbDr), arb(Radacina, ArbStg1, ArbDr)) :-
Cheie < Radacina, !, inser(Cheie, ArbStg, ArbStg1).
96
inser(Cheie, arb(Radacina, ArbStg, ArbDr), arb(Radacina, ArbStg, ArbDr1)) :-
Cheie > Radacina, inser(Cheie, ArbDr, ArbDr1).

Predicatul de inserare a unei chei într-un arbore de căutare, inser, utilizează în


definiţie un cut verde pentru creşterea eficientei. Se poate elimina condiţia
Cheie > Radacina, din cea de a treia regulă a predicatului inser, caz în care predicatul cut
se transformă într-un cut roşu. Programul Prolog care urmează foloseşte definiţiile
predicatelor caut şi inser pentru a implementa mai multe operaţii de prelucrare a arborilor
binari de căutare.
Eliminarea unei chei dintr-un arbore binar de căutare se face după algoritmul standard:
% elim(+Cheie,+Arb,-ArbNou) elimina Cheie din Arb cu rezultat in ArbNou
elim(Cheie, nil, nil).
elim(Cheie, arb(Cheie, nil, nil), nil).
elim(Cheie, arb(Cheie, ArbStg, nil), ArbStg).
elim(Cheie, arb(Cheie, nil, ArbDr), ArbDr).
elim(Cheie, arb(Cheie, ArbStg, ArbDr), arb(Cheie1, Stg1, ArbDr)) :-
drept(ArbStg, Cheie1, Stg1).
elim(Cheie, arb(Cheie1, ArbStg, ArbDr), arb(Cheie1, ArbStg1, ArbDr1)) :-
(Cheie < Cheie1, !, elim(Cheie, ArbStg, ArbStg1), ArbDr1=ArbDr) ;
elim(Cheie, ArbDr, ArbDr1), ArbStg1=ArbStg.

% drept(+Arb,+Cheie,-SuccDr) - intoarce cel mai din dreapta succesor din


% subarborele stâng al nodului cu cheia Cheie in arborele Arb.
drept(arb(Cheie, ArbStg, nil), Cheie, ArbStg).
drept(arb(Cheie, ArbStg, ArbDr), Cheie1, arb(Cheie, ArbStg, ArbDr1)) :-
drept(ArbDr, Cheie1, ArbDr1).

Se poate defini un meniu de prelucrare arborilor binari de căutare care să permită


execuţia operaţiilor definite anterior, la cerere.
meniu(Arb):- nl,
write('1. Sfarsit'), nl,
write('2. Creeaza arbore'), nl,
write('3. Insereaza o cheie'), nl,
write('4. Cauta o cheie'), nl,
write('5. Sterge o cheie'), nl,
write('6. Afiseaza arbore'), nl,
read(Opt), Opt \= 1, !, acţiune(Opt, Arb, ArbNou),
meniu(ArbNou).
meniu( _ ) :- write('Sfarşit'), nl.

97
actiune(2, Arb, ArbNou) :- creare(Arb, ArbNou).
actiune(3, Arb, ArbNou) :-
write('Introduceti cheia: '), read(Cheie), inser(Cheie, Arb, ArbNou).
actiune(4, Arb, Arb) :-
write('Introduceti cheia: '), read(Cheie),
(caut(Cheie, Arb), write('Cheia a fost gasita');
write('Cheia nu este in arbore')), nl.
actiune(5, Arb, ArbNou) :-
write('Introduceti cheia: '), read(Cheie), elim(Cheie, Arb, ArbNou).
actiune(6, Arb, Arb) :-
write('In ce ordine? '), read(Ordine), afisare(Arb, Ordine).

creare(Arb, ArbNou) :-
write('Introduceti o cheie, 0 pentru terminare: '), read(Cheie), Cheie \= 0, !,
inser(Cheie, Arb, A), creare(A, ArbNou).
creare(Arb, Arb).

% afisare(Arbore, Ordine) - afişeaza cheile arborelui binar Arbore, parcurgând


% arborele în ordinea specificată de Ordine: rsd, srd, sdr
afisare(nil, _ ).
afisare(arb(Radacina, SubarboreStang, SubarboreDrept), Ordine) :-
(Ordine = rsd, write(Radacina), write(' '); true),
afisare( SubarboreStang, Ordine),
(Ordine = srd, write(Radacina), write(' '); true),
afisare( SubarboreDrept, Ordine),
(Ordine = sdr, write(Radacina), write(' '); true).

Afişarea arborilor cu ajutorul predicatului afisare(Arbore, Ordine) se poate face în


trei moduri diferite, în funcţie de valoarea argumentului Ordine: rsd
(rădăcină-stânga-dreapta), srd (stânga-rădăcină-dreapta) şi sdr (stânga-dreapta-rădăcină).
Se observă utilizarea operatorului de disjuncţie a scopurilor cu ajutorul căruia se exprimă
cele trei modalităţi de parcurgere a arborelui. Afişarea repetată a meniului de acţiuni este
făcută de predicatul meniu(Arb) prin apelul recursiv al acestuia cât timp nu s-a selectat
acţiunea Sfarsit. Predicatul are argumentul Arb instanţiat. Acesta poate fi iniţial arborele
vid şi este actualizat de apelul recursiv la noua valoare ArbNou, care depinde de acţiunea
selectată. În funcţie de acţiunea selectată în NrActiune şi de arborele dat Arb, predicatul
actiune(NrActiune, Arb, ArbNou) decide care prelucrări sunt necesare pentru a obţine
arborele rezultat ArbNou.

98
Să vedem acum un predicat de sortare a listelor, care utilizează arbori binari de
căutare. Predicatul binsort(Lista, ListaSortata) sortează lista Lista în lista ListaSortata.
El construieşte un arbore binar de căutare prin inserarea succesivă a elementelor din Lista
cu ajutorul predicatului inser prezentat anterior. ListaSortata se obţine prin parcurgerea în
ordine a arborelui obţinut.
% binsort(+Lista, -ListaSortata)
binsort(L, LSort) :-
constr_arb(L, Tree, nil), write($Arborele este: $), nl,
write(Tree), nl, parc_ord(Tree, LSort, []).

% constr_arb - construieşte un arbore binar de căutare


constr_arb([], Acc, Acc) :- !.
constr_arb([K | Rest], Sol, Acc) :-
inser(K, Acc, NoulAcc), constr_arb(Rest, Sol, NoulAcc).

% parc_ord - parcurge în ordine un arbore binar, construind lista cheilor sale


parc_ord(nil, Acc, Acc) :- !.
parc_ord(nod(Cheie, Stang, Drept), L, Acc) :-
parc_ord(Stang, L1, Acc), în_ord(Drept, L, [Cheie | L1]).

9.5 Exerciţii propuse


EP1. Care este complexitatea timp a algoritmului de sortare prin metoda generare şi
testare, prezentat în Secţiunea 9.1 ?
EP2. Comentaţi asupra complexităţii timp a unei implementari Prolog a algoritmului de
căutare binară a unei chei într-o listă sortată crescător.
EP3. Scrieţi predicatul ssort(-L,+LS) care sortează lista L în lista LS conform metodei
de sortare prin selecţie. La un moment dat lista este L' = PS::PN, unde PS este partea
sortată şi PN partea nesortată. Se extrage în mod repetat din PN elementul X de valoare
minimă şi se adaugă la sfârşitul listei PS. Algoritmul începe cu PS = [] şi PN = L, şi se
termină când PN = [], în acel moment PS fiind chiar lista sortată LS. Pentru eliminarea unui
element dintr-o listă şi concatenarea a două liste trebuie să mai scrieţi două predicate: elim
şi append. Pentru a evita folosirea lui append se poate extrage maximul din PN la fiecare
pas şi insera înaintea lui SP.
EP4. Să se scrie un program Prolog care realizează sortarea prin interclasare.
EP5. O concordanţă este o listă de cuvinte care apar într-un text, lista sortată în ordine
alfabetică împreună cu numărul de apariţii ale fiecărui cuvânt în text. Să se scrie un program
care generează o concordanţă pornind de la un text dat. Să se propună o reprezentare pentru
text şi o reprezentare pentru concordanţă.

99
EP6. Să se scrie un predicat care elimină o cheie dintr-un arbore binar de căutare.
EP7. Să se rescrie predicatul meniu(Arb), înlocuind definiţia recursivă a acestuia cu o
definiţie care utilizează predicatul repeat.
EP8. Să se scrie un predicat Prolog care verifică egalitatea a doi arbori binari, adică dacă
au aceleaşi chei.
EP9. Să se scrie un predicat Prolog care verifică egalitatea structurală a doi arbori binari,
adică dacă au aceleaşi număr de chei şi arată la fel.
EP10. Să se definească o structură Prolog care să reprezinte un arbore multicăi. Să se scrie
un predicat Prolog pentru afişarea unui astfel de arbore.
EP11. Să se scrie un predicat Prolog care verifică egalitatea a doi arbori multicăi.

10 Probleme rezolvabile prin backtracking


Mecanismul implicit de funcţionare a limbajului Prolog este bazat pe backtracking. De
aceea problemele care se rezolvă prin backtracking se implementează mai uşor în Prolog
decât în alte limbaje. Să studiem câteva probleme rezolvabile prin backtracking.
10.1 Problema ţăranului
Fie următoarea problemă: Pe malul unui râu se află un ţăran cu un lup, o capră şi o varză.
Ţăranul doreşte să traverseze cu ele râul. Ţăranul poate face traversări înot ale râului
împreună doar cu unul din cele trei personaje de transportat, sau singur. Dacă lupul rămâne
pe acelaşi mal împreună cu capra şi ţăranul este pe celălalt mal, capra va fi mâncată de lup.
Similar se întâmplă cu varza şi capra. Să se scrie un program Prolog care să îi dea ţăranului
toate soluţiile de traversare.
Vom prezenta o rezolvare pe bază de backtracking. Definim starea problemei la un
moment dat ca fiind:
stare(Taran, Lup, Capra, Varza)
unde Taran, Lup, Capra şi Varza indică poziţia (malul) fiecarui element şi pot avea
valorile stang sau drept. În acest fel ştim exact pe ce mal se afla fiecare dintre personaje.
De remarcat că aceasta este o structură şi nu un predicat. De aici rezultă că starea inţială este
(de exemplu):
stare(stang, stang, stang, stang)
iar starea finală este:
stare(drept, drept, drept, drept).
Rezolvarea problemei prin backtracking presupune încercarea unei mişcări posibile în
starea curentă şi apoi reluarea algoritmului. O mişcare generează o nouă stare, din care se

100
vor încerca apoi alte mişcări. Dacă se ajunge într-o stare finală, s-a găsit o solutie. În acest
caz trebuie făcută o revenire pentru a încerca găsirea altor soluţii. Trebuie specificate,
evident, starea iniţială şi starea sau stările finale.
Dacă se ajunge într-o stare ilegală, cum ar fi de exemplu, în cazul acestei probleme,
stare(stang, stang, drept, drept), se execută tot o revenire în starea anterioară. Acelaşi lucru
trebuie să se întâmple şi dacă starea curentă este una în care s-a mai ajuns o dată. Nu are rost
să trecem de mai multe ori prin aceeaşi configuraţie deoarece acest lucru nu face decât să
lungeasca numărul de mişcări necesare şi poate conduce la bucle infinite.
Deci vom avea nevoie de un predicat care, dându-se o stare, să ştie să execute
mişcările posibile din acea stare. Fie acest predicat:
miscare(Stare, StareUrmatoare).
Să vedem cum se defineşte predicatul mişcare în cazul acestei problemei:
1) Ţăranul poate lua lupul cu el pe celălat mal:
miscare(stare(Taran, Taran, Capra,Varza), stare(Taran1, Taran1, Capra,Varza)) :-
opus((Taran, Taran1).

2) Ţăranul poate lua capra cu el pe celălat mal:


miscare(stare(Taran, Lup, Taran,Varza), stare(Taran1, Lup, Taran1, Varza)) :-
opus(Taran, Taran1).

3) Ţăranul poate lua varza cu el pe celălat mal:


miscare(stare(Taran, Lup, Capra, Taran), stare(Taran1, Lup, Capra, Taran1)) :-
opus(Taran, Taran1).

4) Ţăranul poate traversa singur râul:


mişcare(stare(Taran, Lup, Capră, Varza), stare(Taran1, Lup, Capra, Varza)) :-
opus(Taran, Taran1).

Se observă că am folosit predicatul opus(Mal1, Mal2), care determină malul opus


unui mal dat. Definiţia lui este:
opus(stang, drept).
opus(drept, stang).

Predicatele initiala(S) şi finala(S) specifică dacă o stare este sau nu iniţială, respectiv
finală. Definiţiile lor sunt:
initiala(stare(stang, stang, stang, stang)).
finala(stare(drept, drept, drept, drept)).

101
Predicatul ilegala(Stare) spune dacă o stare este ilegală:
1) Dacă lupul este pe acelaşi mal cu capra şi ţăranul este pe celălalt mal:
ilegala(stare(Taran, Lup, Lup, _ )) :- opus(Taran, Lup).

2) Dacă varza este pe acelaşi mal cu capra şi ţăranul este pe celălalt mal:
ilegala(stare(Taran, _ ,Capra, Capra)) :- opus(Taran, Capra).

Se poate scrie acum predicatul de găsire a unei succesiuni de mutări care rezolvă
problema. Deoarece nu trebuie să repetam stări, predicatul va primi, pe lângă starea curentă,
şi o listă de stări deja parcurse, întorcând lista de stări (Solutie) care constituie rezolvarea:
lcv(+Stare, -Solutie, -Vizitate).

unde Stare este starea curentă, Solutie este soluţia ce trebuie găsită, iar Vizitate este lista de
stări parcurse. Acest predicat se scrie ţinând cont de:
1) Dacă starea curentă este o stare finală, atunci soluţie este lista de stări parcurse
împreună cu starea curentă:
lcv(Stare, [Stare|Vizitate], Vizitate) :- finala(Stare).

2) Altfel generează o nouă mutare, testează dacă este legală, testează dacă nu a mai
fost parcursă şi relansează căutarea din noua stare:
lcv(Stare, Solutie, Vizitate) :-
miscare(Stare, StareUrmatoare), % o nouă mutare
not(ilegala(StareUrmatoare)), % este ilegală?
not(member(StareUrmatoare, [Stare | Vizitate])), % este deja parcursă?
lcv(StareUrmatoare, Solutie, [Stare | Vizitate]). % reia cautarea

Predicatul member testează dacă un element face parte dintr-o listă şi a fost detaliat
anterior. Predicatul rezolva rezolvă problema, întorcând lista stărilor parcurse din starea
iniţială în starea finală, deci soluţia problemei.
rezolva(Solutie) :- initiala(Stare), lcv(Stare, Solutie, [] ).

10.2 Problema misionarilor şi canibalilor


Trei misionari şi trei canibali ajung la malul estic al unui râu. Aici se află o barcă cu două
locuri cu care se poate traversa râul (râul nu se poate traversarea înot deoarece în el trăiesc
peşti piranha). Dacă pe unul dintre maluri numărul de canibali este mai mare decât numărul
de misionari, atunci misionarii de pe acel mal vor fi mâncaţi de canibali. Problema întreabă
cum pot trece toţi râul fără ca misionarii să fie mâncaţi de canibali. Pentru a afla soluţia vom
parcurge paşii de la problema anterioară:

102
Structura stării curente este:
stare(MalBarca, NMisionariVest, NCanibaliVest, NMisionariEst, NCanibaliEst).
rezolva(Solutie) :- initiala(Stare), mc(Stare, Solutie, [] ).

% mc(+Stare, -Solutie, +StariVizitate)


mc(Stare, [Stare | Vizitate], Vizitate) :- finala(Stare).
mc(Stare,Soluţie,Vizitate) :-
miscare(Stare, StareUrmatoare),
not(ilegala(StareUrmatoare)),
not(member(StareUrmatoare, [Stare | Vizitate])),
mc(StareUrmatoare, Solutie, [Stare | Vizitate]).

% miscare(+Stare, -StareUrmatoare)
miscare(stare(est, MV,CV, ME, CE), stare(vest, MV1, CV, ME1, CE)) :-
oameni(N), ME >= N, ME1 is ME - N, MV1 is MV + N.
miscare(stare(est, MV, CV, ME, CE), stare(vest, MV, CV1, ME, CE1)) :-
oameni(N), CE >= N, CE1 is CE - N, CV1 is CV + N.
miscare(stare(est, MV, CV, ME, CE), stare(vest, MV1, CV1, ME1, CE1)) :-
ME >= 1, CE >= 1, ME1 is ME - 1, MV1 is MV + 1,
CE1 is CE - 1, CV1 is CV + 1.

miscare(stare(vest, MV, CV, ME, CE), stare(est, MV1, CV, ME1, CE)) :-
oameni(N), MV >= N, MV1 is MV - N, ME1 is ME + N.
miscare(stare(vest, MV, CV, ME, CE), stare(est, MV, CV1, ME, CE1)) :-
oameni(N), CV >= N, CV1 is CV - N, CE1 is CE + N.
miscare(state(vest, MV, CV, ME, CE), stare(est, MV1, CV1, ME1, CE1)) :-
MV >= 1, CV >= 1, MV1 is MV - 1, ME1 is ME + 1,
CV1 is CV - 1, CE1 is CE + 1.

oameni(1).
oameni(2).
% ilegala(+Stare)
ilegala(stare( _ , MV,CV , _ , _ )) :- MV > 0, CV > MV.
ilegala(stare( _ , _ , _ , ME, CE)) :- ME > 0, CE > ME.

initiala(stare(est, 0, 0, 3, 3)).
finala(stare(vest, 3, 3, 0, 0)).

103
10.3 Problema găleţilor cu apă
Există două găleţi cu capacităţile de 8 şi respectiv 5 litri, fără alte marcaje. Se cere să se
măsoare exact 4 litri dintr-un vas mare care conţine cel puţin 20 de litri. Operaţiile admise
sunt: umplerea unei găleţi din vasul mare, golirea unei găleti în vasul mare şi transferul
conţinutului unei găleţi în altă găleată, până când găleata din care se toarna s-a golit
complet, sau găleata în care se toarnă s-a umplut până la refuz.
Pentru rezolvarea problemei vom urmări aceeaşi schemă de la cele două problemele
anterioare.
rezolva(Solutie) :- initiala(Stare), galeti(Stare, [], Solutie).
galeti(Stare, Solutie, Vizitate) :-
miscare(Stare, StareUrmatoare), % se genereaza starea urmatoare
not(member(StareUrmatoare, [Stare | Vizitate])), % starea a mai fost generata?
galeti(StareUrmatoare, Solutie, [Stare | Vizitate]).
% daca nu, mergem mai departe
O stare este o structură cu cinci câmpuri: stare(D1, D2, G1, G2, M), care au
următoarele semnificaţii: D1 = câţi litri de apă sunt în găleata 1, D2 = câţi litri de apă sunt în
găleata 2, G1 = capacitatea găleţii 1, G2 = capacitatea găleţii 2, M = mesajul explicativ
asociat tranziţiei în această stare (M spune ce acţiune a fost executată când s-a trecut în
această stare).
% miscare(+Stare,-StareUrmatoare)
% Regulile comentate nu sunt necesare, ele generând soluţii sau execuţii mai lungi.
miscare(stare(D1, D2, G1, G2, _ ), stare(0, D2, G1, G2, 'Golesc G1 in vas.')).
miscare(stare(D1, D2, G1, G2, _ ), stare(D1, 0, G1, G2, 'Golesc G2 in vas.')).
miscare(stare(D1, D2, G1, G2, _ ), stare(G1, D2, G1, G2, 'Umplu G1 din vas.')).
miscare(stare(D1, D2, G1, G2, _ ), stare(D1, G2, G1, G2, 'Umplu G2 din vas.')).
miscare(stare(D1, D2, G1, G2, _ ), stare(0, D22, G1, G2, 'Golesc G1 in G2.')) :-
D1 > 0, D22 is D1 + D2, D22 =< G2.
% miscare(stare(D1, D2, G1, G2, _ ), stare(D11, 0, G1, G2, 'Golesc G2 in G1.')) :-
% D2 > 0, D11 is D1 + D2, D11 =< G1.
miscare(stare(D1, D2, G1, G2, _ ), stare(D11, G2, G1, G2, M)) :-
T is G2 - D2, D11 is D1 - T, D11 > 0,
string_term(S, T), concat(['Torn', S, ' litri din G1 in G2.'], M).
% miscare(stare(D1, D2, G1, G2, _ ), stare(G1, D22, G1, G2, M)) :-
% T is G1 - D1, D22 is D2 - (G1 - D1), D22 > 0,
% string_term(S, T), concat(['Torn', S, ' litri din G2 in G1.'], M).

Se observă că problema a fost rezolvată pentru cazul general, putând varia atât
capacităţile găleţilor, cât şi numărul de litri de apă care trebuie măsurat.
104
% Starea initiala
initiala(stare(0, 0, 8, 5, 'Galetile sunt goale.')).
% Starea finala
finala(stare(4, _ , _ , _ , _ )).
finala(stare( _ , 4, _ , _ , _ )).

Funcţia member a fost rescrisă deoarece mesajul asociat unei stări nu trebuie să
influenţeze procesul de rezolvare a problemei. O stare este dată de fapt doar de primii doi
parametrii ai structurii stare, următorii doi fiind două valori constante, iar ultimul, mesajul,
a fost introdus doar pentru a descrie rezolvarea problemei. Deci putem ajunge în aceeaşi
stare cu două mesaje diferite.
member(stare(X, Y, _ , _ , _ ), [stare(X, Y, _ , _ , _ ) | _ ] ).
member(X, [ _ | Rest] ) :- member(X, Rest).

Pentru afişarea frumoasă a soluţiilor se adaugă predicatele:


run :- rezolva(Solutie), scrie_solutie(Solutie).

scrie_solutie([]) :- nl.
scrie_solutie([S | Rest]) :- scrie_solutie(Rest), scrie_stare(S).

scrie_stare(stare(X, Y, _ , _ )) :- write(X), write($ $), write(Y), nl.

Funcţionarea programului este următoarea:


?- run.
0 0 Galetile sunt goale.
8 0 Umplu G1 din vas.
3 5 Torn 5 litri din G1 in G2.
3 0 Golesc G2 in vas.
0 3 Golesc G1 in G2.
8 3 Umplu G1 din vas.
6 5 Torn 2 litri din G1 in G2.
6 0 Golesc G2 in vas.
1 5 Torn 5 litri din G1 in G2.
1 0 Golesc G2 in vas.
0 1 Golesc G1 in G2.
8 1 Umplu G1 din vas.
4 5 Torn 4 litri din G1 in G2.
yes

105
10.4 Exerciţii propuse
EP1. Fie un graf neorientat definit de legăturile dintre nodurile sale, exprimate ca fapte
Prolog:
leg(a, b). leg(b, c). leg(a, c). leg(a, d).

1) Să se definească predicatul drum(N1, N2), care verifică dacă există un drum în


graf între nodurile N1 şi N2.
2) Să se critice următoarea implementare a predicatului drum:
drum(N, N).
drum(N1, N2) :- leg(N1, N), drum(N, N2).

3) Să se modifice definiţia predicatului drum la drum(N1, N2, ListaN) astfel încât


să se obţină în ListaN lista nodurilor parcurse între N1 şi N2, în cazul în care
există un drum în graf între nodurile N1 şi N2, şi lista vidă în caz contrar.
EP2. Studiind texte vechi, în încercarea de a reconstitui arborele genealogic al unei
familii boiereşti, un istoric şi-a notat pe fişe separate, relaţiile de rudenie dintre diferitele
persoane ale unei singure familii, sub forma a trei tipuri de relaţii:
x este soţul (soţia) lui y
x este fiul (fiica) lui y
x este fratele (sora) lui y
unde x şi y sunt prenumele unor membri din aceeaşi familie.
În ipoteza că nu există două persoane cu acelaşi prenume, să se scrie un program
care citeşte dintr-un fişier relaţii de rudenie între perechi de persoane şi care stabileşte
următoarele:
- dacă informaţiile privind relaţiile de rudenie sunt compatibile sau contradictorii, ştiind că
în familie nu s-au făcut căsătorii între rude. În caz că informaţiile sunt contradictorii se dă
mesajul 'Informaţii incompatibile' şi nu se mai fac alte operaţii privind familia în cauză;
- dacă informaţiile nu sunt complete pentru alcătuirea arborelui genealogic, se afişează
numai mesajul 'Informaţii incomplete'. Dacă datele permit alcătuirea arborelui genealogic,
se afişează mai întâi relaţiile de rudenie deduse în afara celor date iniţial şi se afişează
arborele genealogic sub o formă sugestivă. (Concurs de Programare, U.P.B., 1995, Etapa
locală).

11 Strategii de căutare în spaţiul stărilor


Unul dintre cele mai utilizate modele de rezolvare a problemelor este prin reprezentarea
căutării sub forma unui graf orientat în care nodurile sunt stări succesive în rezolvare iar
arcele corespund tranziţiilor sau operatorilor legali ce pot fi aplicaţi pentru trecerea dintr-o
stare în alta [Flo93,LS93].

106
11.1 Căutarea soluţiilor în spaţiul stărilor
Pentru a construi o descrierea unei probleme rezolvate prin reprezentare în spaţiul stărilor
trebuie parcurse următoarele etape:
1) Se defineşte spaţiul stărilor care conţine toate toate configuraţiile posibile ale
obiectelor relevante (si poate si unele imposibile). Spaţiul se poate defini explicit
prin indicarea tuturor stărilor (acesta fiind un caz particular şi rar întâlnit în
practică) sau implicit prin indicarea transformărilor care generează o nouă stare
dintr-o stare dată.
2) Se specifică una sau mai multe stări din spaţiu care descriu situaţii posibile de la
care poate porni procesul de rezolvare a problemei (starea iniţială).
3) Se specifică una sau mai multe stări care ar fi acceptabile ca soluţii ale
problemei. Aceste stări se numesc stări scop (sau stări finale sau scopuri).
4) Se specifică un set de reguli care descriu acţiunile (operatorii) disponibile şi care
definesc tranziţiile sau transformările între stări. În această etapă trebuie
analizate următoarele probleme:
• Ce presupuneri nedeclarate explicit sunt prezente în descrierea informală a
problemei?
• Cât de generale trebuie să fie regulile?
• Cât de mult din calculul necesar rezolvării problemei ar trebui făcut înainte
şi reprezentat în reguli?
Problema poate fi rezolvată apoi utilizând o strategie de control adecvată, pentru a
parcurge o parte din spaţiul stărilor (eventual tot) până când este găsită o cale de la o stare
iniţială la o stare finală, în cazul în care problema admite soluţie. Căutarea este un mecanism
general care poate fi utilizat atunci când o metodă directă (deterministă) de rezolvare a
problemei nu este cunoscută. În acelaşi timp, ea oferă cadrul în care pot fi încapsulate
metode mai directe de rezolvare a anumitor părţi ale problemei (subproblemelor).
11.2 Căutare prin backtracking
Rezolvarea unei probleme folosind strategia de backtracking a fost expusă in capitolul 10.
În continuare se prezintă schema generală a unei astfel de căutări care poate fi aplicată
pentru orice descriere a unei subprobleme în termeni de stări iniţiale, stări finale şi operatori
sau tranziţii între stări. Pentru ilustrare, se defineşte explicit un spaţiu de căutare finit,
ipotetic, prin definirea predicatului succ(stare, stare_urmatoare). O definire a predicatului
succ specifică unei probleme particulare cuplată cu schema generală de rezolvare prin
backtracking ce este prezentată în continuare rezolvă problema.

107
% Graful care descrie spaţiul de căutare complet.
succ(a,b). % a stare iniţială
succ(b,c).
succ(c,d).
succ(d,g).
succ(a,e).
succ(e,f).
succ(f,g).
final(g). % Stare finală

% rez(+Stare, -Sol)
rez(Stare, Sol) :- bkt(Stare, [], Sol).
bkt(Stare, Drum, [Stare | Drum]) :- final(Stare).
bkt(Stare, Drum, Sol) :-
succ(Stare, N1), not (member(N1, Drum)), bkt(N1, [Stare | Drum], Sol).

?- rez(a,Sol).
Sol=[g,d,c,b,a]
Căutarea prin backtracking poate fi cuplată cu impunerea unei adâncimi maxime de
căutare, necesară în special în cazul spaţiilor de căutare infinite în care soluţia poate să nu se
afle pe ramura curentă pe care a început căutarea. Stabilirea unei adâncimi maxime de
căutare se poate face astfel:
% rez1(Stare, Sol) impune o adâncimea maximă Max = 10
rez1(Stare, Sol) :- bkt1(Stare, Sol,10).
bkt1(Stare, [Stare], _ ) :- final(Stare).
bkt1(Stare, [Stare | Sol], Max) :-
Max > 0, succ(Stare, N1),
Max1 is Max-1, bkt1(N1, Sol, Max1).
In acest caz, s-a eliminat testul de stări anterior parcurse deoarece, existând o adâncime
maximă de căutare se elimină buclele dar cu penalizarea eventuală a reparcurgerii unor stări.
Exerciţiul propus 1 al acestui capitol va cere o combinare a acestor două soluţii.
11.3 Căutare pe nivel şi în adâncime
Pentru implementarea acestor două strategii de căutare de bază se folosesc două liste: lista
OPEN a nodurilor explorate în căutare (sau FRONTIERA porţiunii cunoscute a spaţiului de
cautarea la un moment dat cu partea necunoscută încă a acestui spaţiu) şi lista CLOSED a
nodurilor expandate (sau TERITORIUL porţiunii cunoscute din spaţiul de căutare). Detalii
suplimentare asupra acestor strategii de căutare se pot găsi în [Flo93]. Pentru a putea obţine
calea de la starea iniţială la starea finală, odată ce s-a găsit starea finală, ambele liste vor

108
conţine perechi [Stare, Predecesor], unde Predecesor este starea predecesoare a stării Stare.
Pentru starea iniţială se va introduce în OPEN perechea [StareInitială, nil], cu nil o
constantă arbitrar fixată.
Implementarea foloseşte două tipuri de date, stivă şi coadă, pentru exploatarea listei
OPEN, respectiv stiva în cazul căutării în adâncime şi coada în cazul căutării pe nivel.
Definirea operaţiilor tipice asociate acestor structuri de date abstracte în Prolog este pusă în
evidenţă de implementarea ce urmează.
% Căutare pe nivel şi în adâncime
% Se folosesc tipurile de date abstracte Stack and Queue.

% TDA Stack
% emptys(+S) - testează dacă stiva este vidă
% emptys(-S) - iniţializează stiva
emptyst([]).
% stack(-Top, -SNou, +S) - are efect de pop
% stack(+Top, +S, -SNouă) - are efect de push
% stack(-Top, _, +S) - are efect de top
stack(Top, Stack, [Top | Stack]).

% TDA Queue
% empty(+Q) - testează dacă coada este vidă
% empty(-Q) - iniţializează coada
emptyq([]).
% enqueue(+El, +Q, -QNouă) introduce un element în coadă
enqueue(El, [], [El]).
enqueue(El, [X | Rest], [X | R1]) :- enqueue(El, Rest, R1).
% dequeue(-El, +Q, -QNouă) elimină un element din coadă
dequeue(El, [El | R], R).

% Spaţiul de căutare
succ(a, b). succ(b, c). succ(c, d). succ(d, g). succ(a, e). succ(e, f). succ(f, g).
final(g).

% Rezolvare cu parcurgere pe nivel


% rezbreadth(+StareInitiala)
rezbreadth(Si) :-
emptyq(Open), emptyq(Closed), enqueue([Si, nil], Open, Open1),
breadth(Open1, Closed).
breadth(Open, _) :- emptyq(Open), !, write('Nu exista solutie'), nl.

109
breadth(Open, Closed) :-
dequeue([Stare | Predec], Open, _), final(Stare),
write('S-a gasit o solutie'), nl, scriecale([Stare | Predec], Closed).
breadth(Open, Closed) :-
dequeue([Stare| Predec], Open, _), final(Stare),
write('S-a gasit o solutie'),nl,
showpath([Stare| Predec], Closed).
breadth(Open, Closed) :-
dequeue([Stare, Pred], Open, RestOpen),
enqueue([Stare, Pred], Closed, Closed1),
listsucc(Stare, RestOpen, Closed1, LSucc),
append(RestOpen, Lsucc, Open1),
breadth(Open1, Closed1).
listsucc(Stare, RestOpen, Closed, Lsucc) :-
bagof([S, Stare], (succ(Stare, S), not member([S,_], RestOpen),
not member([S, _], Closed) ), LSucc).
listsucc(Stare, RestOpen, Closed, []).

% Rezolvare cu parcurgere pe în adâncime


% rezdepth(+StareInitiala)
rezdepth(Si) :-
emptyst(Open), emptyst(Closed), stack([Si, nil], Open, Open1),
depth(Open1, Closed).
depth(Open, _) :- emptyst(Open), write('Nu exista solutie'), nl.
depth(Open, Closed) :-
stack([Stare | Predec], RestOpen, Open), final(Stare),
write('S-a gasit o solutie'), nl, scriecale([Stare | Predec], Closed).
depth(Open, Closed) :-
stack([Stare, Pred], RestOpen, Open),
stack([Stare, Pred], Closed, Closed1),
listsucc(Stare, RestOpen, Closed1, LSucc),
append(LSucc, RestOpen, Open1),
depth(Open1, Closed1).

% Afişează calea de la Si la Sf
% scriecale(+Cale, +Closed)
scriecale([S, nil], _) :- scrie(S), nl.
scriecale([S, Predec], Closed) :-
member([Predec, P], Closed), scriecale([Predec, P], Closed), scrie(S), nl.
scrie(S) :- write(S).

110
member(El, [El | _]).
member(El ,[_ | Rest]) :- member(El, Rest).
append([], L, L).
append([X | L1], L2, [X | L3]) :- append(L1, L2, L3).

?- rezbreadth(a).
a
e
f
g
?- rezdepth(a).
a
b
c
d
g
Strategia de căutare pe nivel este o strategie completă care, în plus, găseşte calea de la
starea iniţială la starea finală cu numărul minim de tranziţii de stări. Strategia de cautare în
adâncime nu este completă pentru orice spaţii de căutare dar consumă mai puţină memorie
decât cea în adâncime.
Pentru a pune în evidenţă diferenţa între strategia de căutare în adâncime şi cea de
backtracking, se va rescrie schema generală de backtracking din secţiunea precedentă pe
acelaşi şablon cu cel de la strategiile precedente.
rezbkt(Si) :-emptyst(Open), stack(Si, Open, NewOpen), bkt(NewOpen).
bkt(Open) :- stack(S, _, Open), final(S), write('S-a găsit o soluţie'), afiş(Open).
bkt(Open) :-
stack(S, _, Open), succ(S, S1), not member(S1, Open),
stack(S1, Open, NewOpen), bkt(NewOpen).
afis([]) :- nl.
afis([S | Rest]) :- afis(Rest), scrie(S).
Se observă că, deoarece strategia de backtracking pastrează numai stările de pe calea
curentă de căutare şi face testul pentru stări anterior parcurse numai pe această cale, este
suficient menţinerea numai a listei OPEN. În plus, această listă nu mai trebuie să fie o listă
de perechi [Stare, Predecesor] ci numai o listă de stări parcurse, la detecţia stării finale
conţinutul lui OPEN fiind chiar calea spre soluţie. Implementarea de mai sus este
echivalentă cu cea din secţiunea precedentă. Predicatul afis permite afişarea stărilor
parcurse în ordinea de la starea iniţială la cea finală.

111
11.4 Algoritmul A*
A* este un algoritm de căutare în spaţiul stărilor a soluţiei de cost minim (soluţia optimă) ce
utilizează o funcţie euristică de estimare a distanţei stării curent parcurse faţă de starea
finală. Pe parcursul căutării, stările sunt considerate în ordinea crescătoare a valorii funcţiei
f(S)=g(S)+h(S), unde g(S) este funcţia de cost a porţiunii parcurse până în starea S iar h(S)
este funcţia euristică de estimare a distanţei din S la starea finală. Funcţia euristică trebuie să
fie admisibilă (mai mică sau cel mult egală cu distanţa reală) pentru a garanta optimalitatea
soluţiei. O prezentare sistematică şi detalii suplimentare asupra algoritmului A* pot fi găsite
în [Flo93]. Deşi are o complexitate timp tot exponenţială, ca orice procedură de căutare,
algoritmul este mai eficient, în general, decât stategiile neinformate datorită componentei
euristice care ghidează căutarea.
Trebuie observat că algoritmul A* poate fi aplicat şi în cazul în care într-o problemă
nu există costuri, considerând implicit toate costurile tranziţiilor de stări egale cu 1. În acest
caz, rezultatele algoritmului sunt similare cu cele ale parcurgerii pe nivel, în sensul găsirii
drumului de la starea iniţială la starea finală cu un număr minim de tranziţii de stări, dar
căutarea este, în general, mai rapidă. Programul Prolog ce urmează presupune, implicit,
costurile tranziţiilor de stări egale cu 1.
Implementarea algoritmului A* se va face pe baza schemelor anterioare de căutare, cu
următoarele modificări. În loc de a exploata lista OPEN ca o stivă sau ca o coada, acestă
listă va fi tratată ca o listă de priorităţi în funcţie de f(S). Fiecărei tranziţii de stări i se va
asocia un cost, în funcţie de problemă, şi fiecărei stări i se va asocia o valoare pentru funcţia
euristică h(S). Lista OPEN va fi o listă de liste de patru elemente de forma [Stare,
Predecesor, G, H, F]. La fel ca în cazurile precedente, programul va fi exemplificat pe un
spaţiu de căutare ipotetic pentru ca în capitolul următoar schema generală de căutare să fie
aplicată pe probleme reale.
% Spaţiul de căutare
succ(a, b). succ(b, c). succ(c, d). succ(d, g). succ(a, e). succ(e, f). succ(f, g).
final(g).
% Valorile funcţiei euristice folosite în algoritmul A*
euristic(a, g, 3).
euristic(b, g, 3).
euristic(c, g, 2).
euristic(d, g, 1).
euristic(g, g, 0).
euristic(e, g, 2).
euristic(f, g, 1).
euristic(_, _, 0).

112
% Coadă de priorităţi (coada este sortată crescator în funcţie de cheia F1).
inspq(El, [], [El]).
inspq(El, [X | Rest], [El, X | Rest]) :- precedes(El, X), !.
inspq(El, [X | Rest], [X | R1]) :- inspq(El, Rest, R1).
precedes([_, _, _, _, F1], [_, _, _, _, F2]) :- F1<F2.

rezastar(Si, Scop) :-
emptyq(Open), emptyq(Closed),
euristic(Si, Scop, H),
inspq([Si, nil, 0, H, H], Open, Open1),
astar(Open1, Closed, Scop).

astar(Open, _, _) :- emptyq(Open), !, write('Nu exista solutie'), nl.


astar(Open, Closed, Scop) :-
dequeue([S, Pred, _, _, _], Open, _),
S=Scop,
write('S-a găsit o soluţie'), nl,
scriecale1([S, Pred, _, _, _], Closed).
astar(Open, Closed, Scop) :-
dequeue([S, Pred, G, H, F], Open, RestOpen),
inspq([S, Pred, G, H, F], Closed, Closed1),
(bagof([Urmator, H1], (succ(S, Urmator), euristic(Urmator, Scop, H1)),
LSucc),!,
G1 is G+1,
actual_toti(S, G1, LSucc, RestOpen, Closed1, OpenR, ClosedR);
OpenR=RestOpen, ClosedR=Closed1),
astar(OpenR, ClosedR, Scop).

actual_toti(_, _, [], Open, Closed, Open, Closed) :- !.


actual_toti(Stare, G, [[S, H] | Rest], Open, Closed, OpenR, ClosedR) :-
actual(Stare, G, [S, H], Open, Closed, Open1, Closed1),
actual_toti(Stare, G, Rest, Open1, Closed1, OpenR, ClosedR).
actual(Stare, G, [S, H], Open, Closed, OpenR, Closed) :-
member([S, Pred, G1, _, _], Open), !,
( G1=<G, OpenR=Open, !;
F is G+H,
elim([S, Pred, G1, _, _], Open, Open1),
inspq([S, Stare, G, H, F], Open1, OpenR)).

113
actual(Stare, G, [S, H], Open, Closed, OpenR, ClosedR) :-
member([S, Pred, G1, _, _], Closed), !,
( G1=<G, ClosedR=Closed, OpenR=Open, !;
F is G+H,
elim([S, Pred, G1, _, _], Closed, ClosedR),
inspq([S, Stare, G, H, F], Open, OpenR)).

actual(Stare, G, [S, H], Open, Closed, OpenR, Closed) :-


F is G+H,
inspq([S, Stare, G, H, F], Open, OpenR).

scriecale1([S, nil, _, _, _], _) :- scrie(S), nl.


scriecale1([S, Pred, _, _, _], Closed) :-
member([Pred, P, _, _, _], Closed),
scriecale1([Pred, P, _, _, _], Closed), scrie(S), nl.
scrie(S) :- write(S).
elim(_, [], []).
elim(X, [X | Rest], Rest) :- !.
elim(X, [Y | Rest], [Y | Rest1]) :- elim(X, Rest, Rest1).
?- rezastar(a).
a
e
f
g

11.5 Exerciţii propuse


EP1. Să se rescrie schema de cautare cu backtracking combinând varianta care
memorează lista stărilor parcurse cu cea care impune o adâncime maximă de căutare.
EP2. Adâncimea maximă de căutare trebuie impusă, în anumite cazuri (spaţii de căutare
infinite) şi pentru căutarea în adâncime. Să se modifice schema căutarii în adâncime prin
includerea unei adâncimi maxime de căutare.
EP3. Strategiile de căutare backtracking, în adâncime şi pe nivel prezentate determină
prima soluţie, în cazul în care problema admite soluţie. Să se modifice programele
corespunzătoare acestor strategii astfel încât să determine şi să afişeze toate soluţiile
problemei, dacă problema admite mai multe soluţii.
EP4. Să se modifice algoritmul A* din secţiunea 11.4 prin adăugarea unui cost explicit,
diferit de 1, tranziţiilor de stări.

114
12 Utilizarea căutării în rezolvarea problemelor
În acest capitol se aplică schemele de strategii de căutare dezvoltate în capitolul precedent
pentru rezolvarea unor probleme "clasice" în inteligenţă artificială. În acest scop, pentru
fiecare problemă, se indică reprezentarea stărilor cu identificarea stării iniţiale şi a stării
finale, se definesc tranziţiile legale de stări şi, acolo unde este cazul, costuri şi funcţii
euristice de ghidare a căutării.

12.1 Problema misionarilor şi canibalilor


Reluăm problema misionarilor şi canibalilor din secţiunea 10.2 şi arătăm cum se poate
rezolva acestă problemă pe baza schemelor de căutare prezentate în capitolul 11. Spre
deosebire de rezolvarea din secţiunea 10.2, se modifică reprezentarea unei stări a problemei
din
stare(MalBarca, NMisionariVest, NCanibaliVest, NMisionariEst, NCanibaliEst)
în
stare(MalBarca, NMisMalBarca, NCanMalbarca, NMisMalOpus, NCanMalOpus)
unde MalBarca poate fi, la fel ca înainte, est sau vest. Implementarea care urmează pune în
evidenţă avantajele acestei noi reprezentări din punct de vedere al simplificării prelucrărilor
necesare.
Se consideră starea iniţială în care cei trei misionari şi canibali sunt pe malul de est, ei
dorind să ajungă pe malul de vest fără ca misionarii să fie mâncaţi de canibali.
% Stare iniţială
initial(st(est, 3, 3, 0, 0)).
% Starea finală
final(st(vest, 3, 3, 0, 0)).

% Malurile opuse
opus(est, vest). opus(vest, est).

% sigur(+NrMisionari, +NrCanibali) - stare sigură


sigur(0, _ ).
sigur(X, Y) :- X > 0, X >= Y.

% Tranziţii între stări, succ(+StareCurenta, - StareUrmatoare)


% mută doi misionari
succ(st(X, MX, CX, MY, CY), st(Y, MY1, CY, MX1, CX)) :-
opus(X, Y),
modifica(2, MX, MY, MX1, MY1),

115
sigur(MX1, CX), sigur(MY1, CY).
% mută doi canibali
succ(st(X, MX, CX, MY, CY), st(Y, MY, CY1, MX, CX1)) :-
opus(X, Y),
modifica(2, CX, CY, CX1, CY1),
sigur(MX, CX1), sigur(MY, CY1).
% muta un misionar şi un canibal
succ(st(X, MX, CX, MY, CY), st(Y, MY1, CY1, MX1, CX1)) :-
opus(X, Y),
modifica(1, MX, MY, MX1, MY1), modifica(1, CX, CY, CX1, CY1),
sigur(MX1, CX1), sigur(MY1, CY1).
% mută un misionar
succ(st(X, MX, CX, MY, CY), st(Y, MY1, CY, MX1, CX)) :-
opus(X, Y),
modifica(1, MX, MY, MX1, MY1),
sigur(MX1, CX), sigur(MY1, CY).
% mută un canibal
succ(st(X, MX, CX, MY, CY), st(Y, MY, CY1, MX, CX1)) :-
opus(X, Y),
modifica(1, CX, CY, CX1, CY1),
sigur(MX, CX1), sigur(MY, CY1).

% modifica(+Cati, +NrInit1, +NrInit2, -NrRez1, -NrRez2)


modifica(N, NX, NY, NX1, NY1) :- NX >= N, NX1 is NX - N, NY1 is NY + N.

% Afişează soluţia
% Predicatul scrie din capitolul precedent se redefineşte astfel:
scrie(st(B, MX, CX, MY, CY)) :- nl, scrielista([B, ' ', MX, ' ', CX, ' ', MY, ' ', CY]).
scrielista([]).
scrielista([X | Rest]) :- write(X), scrielista(Rest).
Rezolvarea problemei este acum imediată folosind schemele de căutare din capitolul
precedent. Selecţia predicatului rezbkt conduce la o rezolvare echivalentă cu cea prezentată
în secţiunea 10.2.
% Se alege strategia dorită (nivel, adâncime sau backtracking)
solutie :-
nl, initial(Si),
rezbreadth(Si).
% rezdepth(st(est, 3, 3, 0, 0)).
% rezbkt(Si).

116
Pentru a aplica o strategie A* se fixează implicit costuri de 1 pentru fiecare tranziţie
între două stări şi trebuie definită o funcţie euristică admisibilă. Se notează cu Ne numărul
persoanelor de pe un anumit mal (misionari plus canibali) în starea S şi se definesc
următoarele trei funcţii euristice1 :
h1(S) = nE(S), numarul de persoane de pe malul de est în starea S
h2(S) = nE(S) / 2
nE(S) + 1, dacă barca este la vest şi nE(S) ≠ 0
h3(S) = nE(S) - 1, dacă barca este la est şi nE(S) ≠ 0
0 dacă nE(S) = 0
Funcţia h1 nu este admisibilă, funcţiile h2 şi h3 sunt admisibile şi monotone, cu h3
funcţie euristică mai informată decât h2. Pentru a rezolva problema cu algoritmul A* se
defineşte, de exemplu, funcţia h3 în Prolog, se păstreză specificaţiile anterioare ale
problemei şi se foloseşte strategia de căutare informată definită în secţiunea 11.4. Soluţia
obţinută este minimă (optimă) în termeni de număr de tranziţii de stare.
% Funcţia euristică h3
euristic(st(est, MX, CX, _ , _ ), Sfin, H) : -
final(Sfin), Ne is MX + CX, (Ne \= 0, !, H is Ne - 1; H = 0).
euristic(st(vest, _ , _ , MY, CY), Sfin, H) : -
final(Sfin), Ne is MY + CY, (Ne \= 0, !, H is Ne + 1; H=0).
% Rezolvare
solutie :-
nl, initial(Si), final(Sfin),
rezastar(Si, Sfin).

12.2 Problema mozaicului cu opt cifre


Problema mozaicului cu opt cifre constă în găsirea mutărilor de executat pentru a ajunge
dintr-o configuraţie iniţială a mozaicului într-o configuraţie finală. O configuraţie finală
poate fi cea prezentată în figura 11.
1 2 3
8 4
7 6 5

Figura 11. O configuraţie finală a mozaicului cu opt cifre


Această problemă, deşi aparent simplă, are un spaţiu de căutare foarte mare în cazul
general şi este un bun exemplu ce pune în evidenţă utilitatea folosirii funcţiilor euristice de

1
Datorăm aceste funcţii euristice profesorului Cristian Giumale.

117
ghidare a căutării. În secţiunile ce urmează vom arata cum se rezolvă problema cu diverse
strategii şi vom pune în evidenţă incapacitatea strategiilor de căutare neinformate de a găsi
soluţia pentru multe din configuraţiile iniţiale propuse. Vă sugerăm să testaţi, pentru diverse
poziţii iniţiale, care metode reuşesc şi care nu. Proprietăţile importante care determină
eşuarea unor metode şi reuşita altora sunt numărul de stări generate, numărul de apeluri
recursive şi numărul de mutări care rezolvă problema.
Problema nu conţine costuri dar, dacă ne interesează rezolvarea mozaicului printr-un
număr minim de mişcări (tranziţii de stare) se poate considera costul tuturor tranziţiilor egal
cu 1. Pentru rezolvarea problemei printr-un algoritm A* se pot defini următoarele funcţii
euristice alternative [Bra88]:
1) h1(S) = numărul de cifre care nu sunt la locul lor în starea S faţă de starea finală;
2) h2(S) = suma distanţelor, pe orizontală şi verticală, la care se află cifrele în starea
S faţă de poziţia lor finală (distanţa Manhattan);
3) h3(S) = h2(S) + 3 ∗ D(S), unde D(S) este sumă din d(c) pentru fiecare cifră c,
mai puţin ultima, în starea S. Funcţia d(c) se calculează astfel:
• Dacă c este pe poziţia în care se va afla în final spaţiul liber, atunci
d(c) = 1;
• Dacă c este pe poziţia p, iar c + 1 se află pe poziţia p + 1, atunci d(c) = 0;
• Dacă c este pe poziţia p, iar c + 1 nu se află pe poziţia p + 1, atunci
d(c) = 2.

Funcţiile h1 şi h2 sunt admisibile, h2 fiind mai informată decât h1 deoarece încearcă să


surprindă şi dificultatea structurală a configuraţiei iniţiale. Funcţia h3 nu este admisibilă
deoarece în unele situaţii este mai mare decât h* (distanţa reală). Ea este utilă deoarece este
mai informată decât h2 şi ajunge mult mai repede la o soluţie bună, chiar dacă soluţia nu
este optimă (soluţia este destul de aproape de soluţia optimă). Aşa cum se va vedea mai
departe, h3 este singura funcţie care poate rezolva problema pentru anumite configuraţii
iniţiale.
În continuare se va prezenta rezolvarea acestei probleme prin trei metode neinformate,
backtracking, parcurgere în adâncime, parcurgere pe nivel, apoi rezolvarea pe baza
algoritmului A*, utilizând funcţiile h1, h2 şi h3.
Se va vedea că o căutare în adâncime funcţionează până la maxim 2-3 mutări necesare
pentru rezolvarea problemei, căutarea pe nivel poate ajunge până la 5-6 mutări şi A* cu h1
până la 8-9 mutari, cu h2 până la 18-20 de mutări, iar cu h3 chiar până la 25 de mutări.
12.3 Rezolvarea problemei mozaicului prin căutări neinformate
O primă posibilitate de reprezentare a stărilor problemei ar fi printr-o listă de perechi,
o pereche fiind de forma [poziţie, cifră], unde poziţie este poziţia pe tablă (de exemplu

118
p1,...p9) iar cifră este cifra existentă într-o anumită stare pe acea poziţie. Astfel, starea finală
s-ar specifica în Prolog prin:
final([[p1,1],[p2,2],[p3,3],[p4,8],[p5,bl],[p6,4],[p7,7],[p8,6],[p9,5]])
O astfel de reprezentare este mai intuitivă şi mai convenabilă pentru o afişare frumoasă
a soluţiei dar este ineficientă pentru prelucrare. Din acest motiv vom folosi o reprezentare
diferită, respectiv o listă ordonată de tipul:
[poziţie_blanc, poziţie_cifra1, ..., poziţie_cifra9]
de exemplu, pentru starea finală, avem definiţia:
final([5, 1, 2, 3, 6, 9, 8, 7, 4]).
Pentru a putea testa problema pe mai multe stări iniţiale, predicatul initial va avea un
paramentru suplimentar, respectiv numărul unei stări iniţiale. În continuare se definesc
stările iniţiale alternative, starea finală, tranziţiile de stări şi un predicat de afişare
"frumoasă" a mozaicului.
% Definirea problemei mozaicului cu 8 cifre
% Stare iniţială 1 - merge cu orice strategie
initial(s1, [1, 4, 2, 3, 6, 9, 8, 7, 5]).
% Stare iniţială 2 - merge cu orice strategie
initial(s2, [3, 4, 1, 2, 6, 9, 8, 7, 5]).
% Stare iniţială 3 - merge cu parcurgere pe nivel şi în adâncime, dar nu cu bkt
initial(s3, [5, 1, 6, 2, 3, 9, 8, 7, 4]).
% Stare iniţială 4 - merge cu parcurgere pe nivel, nu merge cu adâncime şi bkt
initial(s4, [8, 4, 1, 3, 6, 9, 5, 7, 2]).
% Stare iniţială 4.1 - merge cu nivel şi adâncime, dar nu cu bkt
initial(s5, [5, 1, 6, 2, 3, 9, 8, 7, 4]).
% Stare iniţială 6 - nu merge cu nici o strategie neinformată
initial(s6, [9, 4, 2, 6, 8, 5, 1, 3, 7]).
% Stare iniţială 7 - nu merge cu nici o strategie neinformată
initial(s7, [2, 4, 5, 6, 3, 9, 8, 7, 1]).
% Stare finală
final([5, 1, 2, 3, 6, 9, 8, 7, 4]).

% mută(+Tabla, -TablaNouă, +Sens) - mută cifra care se poate muta în sensul Sens
muta(Tabla, TablaN, Sens) :-
Tabla = [Poz | Rest], detlist(Sens, L),
not member(Poz, L),
calc(Poz, Poz1, Sens),
repl(Poz1, Poz, Rest, Rest1),
TablaN = [Poz1 | Rest1], !.
119
% succ(+Tablă, -TablăNouă) - determină următoarea stare a tablei
succ(S, S1) :- (muta(S, S1, stg); muta(S, S1, dr);
muta(S, S1, sus); muta(S, S1, jos)).

% detlist determină poziţiile din care nu se poate muta în Sens


detlist(stg, [1, 4, 7]) :- !.
detlist(dr, [3, 6, 9]) :- !.
detlist(sus, [1, 2, 3]) :- !.
detlist(jos, [7, 8, 9]).

% calc(+Poz, -Poz1, +Sens) - calculează noua poziţie după mutare în Sens


calc(Poz, Poz1, stg) :- !, Poz1 is Poz-1.
calc(Poz, Poz1, dr) :- !, Poz1 is Poz+1.
calc(Poz, Poz1, sus) :- !, Poz1 is Poz-3.
calc(Poz, Poz1, jos) :- !, Poz1 is Poz+3.

% repl - actualizează poziţia pe tablă


repl(Poz1, Poz, [Poz1 | Rest], [Poz | Rest]) :- !.
repl(X, X1, [Y | Rest], [Y | Rest1]) :- repl(X, X1, Rest, Rest1).

% Afişare
poza(I) :-
pozitie(1, I, V1), pozitie(2, I, V2), pozitie(3, I, V3),
pozitie(4, I, V4), pozitie(5, I, V5), pozitie(6, I, V6),
pozitie(7, I, V7), pozitie(8, I, V8), pozitie(9, I, V9), nl,
tab(10), write(V1), write(' '), write(V2), write(' '), write(V3), nl,
tab(10), write(V4), write(' '), write(V5), write(' '), write(V6), nl,
tab(10), write(V7), write(' '), write(V8), write(' '), write(V9), nl.
pozitie(X, I, V) :- poz(X, I, 0, N), (N = 0, !, V = ' '; V = N).
poz(X, [X | _ ], N, N) :- !.
poz(X, [Y | Rest], N, NR) :- N1 is N+1, poz(X, Rest, N1, NR).

Predicatul poza(I) permite afisarea sub formă de mozaic a unei stari. El poate fi folosit
în afişarea soluţiei definind predicatul scrie(S) astfel:
scrie(S) :- poza(S).

La căutarea în adâncime se fac foarte multe mutări până la starea finală. Din această
cauză nu se poate folosi pentru orice configuraţie iniţială afisarea cu predicatul poza (este

120
generată eroarea Not enough stack) şi este necesară păstrarea variantei iniţiale de definire a
lui scrie în care afişarea stărilor se va face sub formă de listă, cu semnificaţia definită în
secţiunea precedentă.
Soluţia problemei, pentru o stare iniţială, NrS, fixată dintre cele definite (s1, s2,...), se
obţine astfel:
solutie(NrS) :-
initial(NrS, Si), poza(Si),
rezbreadth(Si).
% rezdepth(Si).
% rezbkt(Si).

12.4 Rezolvarea problemei mozaicului cu A*


Se păstrează reprezentarea stărilor problemei descrisă în secţiunea precedentă, se utilizează
schema de rezolvare cu algoritmul A* prezentată în capitolul 11 şi se definesc trei variante
de predicate pentru calculul funcţiei euristice, conform celor trei funcţii euristice discutate în
secţiunea 12.2. Funcţia euristică h1 se calculează astfel:
% euristic(+S, -H) - corespunzătoare funcţiei h1
euristic(S, H) :- final(Sf), calc(S, Sf, 0, H).
calc([], _ , HR, HR) :- !.
calc(S, Sf, H, HR) :- S = [Poz | Rest], Sf = [Poz1 | Rest1],
(Poz = Poz1, N = 0; Poz\ = Poz1, N = 1),
H1 is H+N, calc(Rest, Rest1, H1, HR).

Funcţia euristică h2, distanţă Manhattan, se calculează astfel:


% euristic(+S, +H) - corespunzătoare funcţiei h2
euristic(S, H) :-
final(Sf), S = [ _ | Rest], Sf = [ _ | Rest1],
calc(Rest, Rest1, 0, H).
% calc(+R, +R1, +ValInit, -H)
calc([], _ , H, H) :- !.
calc(S, Sf, H, HR) :- S = [Poz | Rest], Sf = [Poz1 | Rest1],
lincol(Poz, L, C), lincol(Poz1, L1, C1),
(L> = L1, !, D1 is L-L1; D1 is L1-L),
(C> = C1, !, D2 is C-C1; D2 is C1-C),
Manh is D1+D2, H1 is H+Manh, calc(Rest, Rest1, H1, HR).

Pentru calculul funcţiei h3 se adugă la h2 valoarea 3 * D(S), cu D calculată de


predicatul dep(S,D).
% Daca se adaugă la h2 si 3*D se obtine h3 e-adimisibila
121
% euristic(+S, +H) - corespunzătoare funcţiei h3
euristic(S,H):-
final(Sf), S = [ _ | Rest], Sf = [ _ | Rest1],
calc(Rest, Rest1, 0, H1),
dep(Rest,D), H is H1+ (3 * D).

calc([], _ , H, H) :- !.
calc(S, Sf, H, HR) :- S = [Poz | Rest], Sf = [Poz1 | Rest1],
lincol(Poz, L, C), lincol(Poz1, L1, C1),
(L> = L1, !, D1 is L-L1; D1 is L1-L),
(C> = C1, !, D2 is C-C1; D2 is C1-C),
Manh is D1+D2, H1 is H+Manh, calc(Rest, Rest1, H1, HR).

dep(S, D) :- dep(S, 0, D).


dep([P], D, D).
dep([P, P1 | Rest], D, DR) :-
(P = 5, !, D1 = 1; urm(P, U), (P1 = U, !, D1 = 0;D1 = 2)),
D2 is D+D1, dep([P1 | Rest], D2, DR).

% funcţii ajutătoare
div(X, Y, R) :- div(X, Y, 0, R).
div(X, Y, N, N) :- (X = 0; X < Y), !.
div(X, Y, N, NR) :- X > = Y, X1 is X-Y, N1 is N+1, div(X1, Y, N1, NR).
% P mod 3 = 0 atunci Coloana = 3, Linie = P div 3
% P mod 3 \= 0 atunci Coloana = P mod 3, Linie = P div 3+1
lincol(P, L, C) :- T is P mod 3,
(T = 0, !, C = 3, div(P, 3, L);
C = T, div(P, 3, L1), L is L1+1).

urm(1, 2). urm(2, 3). urm(3, 6). urm(4, 1).


urm(6, 9). urm(9, 8). urm(8, 7). urm(7, 4).

%% Stare initiala 1
initial(s1,[1,4,2,3,6,9,8,7,5]).
%% Stare initiala 2
initial(s2,[3,4,1,2,6,9,8,7,5]).
%% Stare initiala 3
initial(s3,[5,1,6,2,3,9,8,7,4]).
%% Stare initiala 4 - merge cu h1, h2, h3
initial(s4,[8,4,1,3,6,9,5,7,2]).

122
Si Poză Strategii de căutare neinformate Căutare informată cu A*
Nivel Adâncime BKT A* - h1 A* - h2 A* - h3

Si 1 2 3 2 mutări 30 mutări 30 mutări 2 mutări 2 mutări 2 mutări


1 8 4 5 apeluri 28 apeluri 28 apeluri 2 apeluri 2 apeluri 2 apeluri
7 6 5

Si 2 2 3 4 mutări 4 mutări 4 mutări 4 mutări 4 mutări 4 mutări


1 8 4 15 apeluri 4 apeluri 4 apeluri 5 apeluri 4 apeluri 4 apeluri
7 5 5

Si 3 1 3 4 4 mutări 30 mutări Nu merge. 4 mutări 4 mutări 4 mutări


8 2 26 apeluri D. s. 5 apeluri 4 apeluri 4 apeluri
7 6 5

Si 4 2 8 3 5 mutări D.s. D. s. 5 mutări 5 mutări 5 mutări


1 6 4 57 apeluri 163 apeluri 338 apeluri 6 apeluri 5 apeluri 5 apeluri
7 5

Si 5 2 1 6 D. s. D. s. D. s. D. s. 18 mutări 18 mutări
4 8
7 5 3

Si 6 6 2 7 D. s. D. s. D. s. D. s. D. s. 23 mutări
1 5 3 74 apeluri 163 apeluri 124 apeluri
8 4

Si 7 8 4 D. s. D. s. D. s. D. s. D. s. 22 mutări
1 2 3 74 apeluri 163 apeluri 84 apeluri
7 6 5

Figura 12. Performanţele strategiilor de căutare în problema mozaicului

123
%% Stare initiala 5 - merge cu h2, h3, nu merge cu h1
initial(s5,[5,2,1,9,4,8,3,7,6]).
%% Stare initiala 6 - merge numai cu h3
initial(s6,[9,4,2,6,8,5,1,3,7]).
%% Stare initiala 7 - merge numai cu h3
initial(s7,[2,4,5,6,3,9,8,7,1]).

Rezolvarea problemei se obţine astfel (atenţie ! se alege numai una dintre cele trei
definiţii posibile pentru predicatul euristic, corespunzătoare lui h1 sau h2 sau h3):
solutie(NrS):-
initial(NrS,Si), poza(Si),
rezastar(Si).
Pentru exemplificarea performanţelor strategiilor de căutare (neinformate şi informate)
discutate în acest capitol se prezintă rezultatele obţinute pentru şapte stări iniţiale în
problema mozaicului cu opt cifre (D. s. înseamnă depăşirea stivei), în figura 12.
12.5 Exerciţii propuse
EP1. Să se implementeze problema misionarilor şi canibalilor folosind un algoritm A*
bazat pe funcţia euristică h2.
EP2. Să se extindă problema misionarilor şi a canibalilor pentru N misionari, N canibali
şi capacitatea bărcii de M persoane. Să se rezolve această problemă prin strategii de căutare
neinformate şi informate şi să se facă o analiză comparativă a performanţelor metodelor, de
tipul celei din figura 12.
EP3. Să se folosească schemele generale de căutare pentru rezolvarea problemei
maimuţei şi a bananei prezentată în capitolul 4.
EP4. Să se folosească căutări informate şi neinformate pentru rezolvarea problemei
ţăranului (10.1); se va defini o funcţie euristică adecvată. Se va propune, de asemenea, o
reprezentare a stărilor problemei alternativă celei din secţiunea 10.1.
EP5. Să se modifice implementările rezolvării problemei mozaicului astfel încât să se
afişeze statisticile prezentate în figura 12.
EP6. Să se aplice algoritmul A* cu costuri explicite (dezvoltat în cadrul exerciţiului
propus 4 din capitolul 11) pentru rezolvarea problemei comis-voiajorului.

124
13 Strategii de căutare aplicate la jocuri
În această secţiune se discută strategii de căutare specifice unui joc ce implică doi adversari.
Strategia de căutare necesară în acest caz este mai complicată datorită existenţei
adversarului ostil şi imprevizibil în mutări. Există două tipuri de jocuri: jocuri în care spaţiul
de căutare poate fi investigat exhaustiv şi jocuri în care spaţiul de căutare nu poate fi
investigat complet deoarece este prea mare.
13.1 Algoritmul Minimax pentru spaţii de căutare investigate exhaustiv
În procesul de căutare trebuie luate în considerare mişcările posibile ale adversarului. Se
presupune că oponentul are acelaşi cunoştinţe despre spaţiul de căutare ca şi jucătorul şi că
foloseşte aceste cunoştinţe pentru a câştiga. Algoritmul minimax implementează o strategie
de căutare bazată pe această presupunere. În prezentarea algoritmului, ne vom situa pe
poziţia unuia din cei doi participanţi, ales arbitrar şi numit jucător, celălat participant fiind
numit adversar sau oponent.
Jucătorul este referit ca MAX iar adversarul său ca MIN. MAX încearcă să câştige
maximizându-şi avantajul, iar MIN încearcă să câştige minimizând avantajul lui MAX. Se
presupune că MIN va muta întotdeauna în configuraţia care este cea mai defavorabilă
pentru MAX în acel moment.
Pe parcursul jocului se generează spaţiul de căutare ce conţine ca noduri configuraţiile
posibile ale jocului iar ca tranziţii între acestea mutările posibil de executat. Pentru
implementarea căutării se etichetează fiecare nivel din spaţiul de căutare cu MAX sau cu
MIN, după cum mişcarea este făcută de jucător sau de adversar. Se generează tot spaţiul de
căutare şi fiecare nod terminal este evaluat la scorul configuraţiei terminale
corespunzătoare; de exemplu 1 dacă este o configuraţie câştigătoare pentru MAX şi 0 dacă
este necâştigătoare (este câştigătoare pentru MIN). Se pot asocia şi valori (pozitive) de
câstig pentru jucător în fiecare nod terminal, corespunzătoare punctajului obţinut de acesta
la sfârşitul jocului. Apoi se propagă aceste valori în sus în graf, la nodurile părinţi, după
următoarele reguli:
1. dacă nodul părinte este MAX atunci i se atribuie valoarea maximă a succesorilor
săi;
2. dacă nodul părinte este MIN atunci i se atribuie valoarea minimă a succesorilor săi.
Valoarea asociată astfel fiecărei stări descrie cea mai bună valoare pe care jucătorul
poate spera să o atingă (presupunând că oponentul joacă conform algoritmului minimax).
Valorile obţinute sunt folosite pentru a alege între diverse mutări posibile. Un exemplu de
spaţiu de căutare Minimax este prezentat în figura 13.

125
MAX A/3

MIN B/3 C/2 D/2

MAX E/3 F / 12 G/8 H/2 I/4 J/6 K / 14 L/5 M/2

Figura 13. Un posibil spaţiu de căutare Minimax


Fie următorul joc (NIM): Mai multe beţe se pun pe masă într-o grămadă iniţială.
Fiecare participant trebuie să împartă câte o grămadă în două grămezi cu un număr diferit de
beţe. De exemplu, dacă iniţial sunt 6 beţe, acestea pot fi împărţite 5 - 1 sau 4 - 2, dar nu
3 - 3. Primul jucător care nu mai poate face nici o mişcare pierde. Spaţiul de căutare pentru
configuraţia iniţială de 7 beţe este prezentat în figura 14.

MIN 7/1

MAX 6-1/1 5-2/1 4-3/1

MIN 5-1-1/0 4-2-1/1 3-2-2/0 3-3-1/1

MAX 4-1-1-1/0 3-2-1-1/1 2-2-2-1/0

MIN 3-1-1-1-1/0 2-2-1-1-1/1

MAX 2-1-1-1-1-1/0

Figura 14. Spaţiul de căutare Minimax pentru Nim cu 7 beţe


Se observă că dacă începe MIN, jucătorul MAX poate câştiga întotdeauna jocul,
deoarece orice mişcare iniţială a lui MIN duce într-o stare din care MAX poate câştiga dacă
joacă bine.
13.2 Algoritmul Minimax cu adâncime de căutare finită
Dacă nu se poate investiga exhaustiv spaţiul de căutare, se aplică algoritmul minimax. până
la o adâncime finită, arbitrar prestabilită. Configuraţiile aflate la această adâncime sunt
126
considerate stări terminale. Deoarece ele nu sunt (de cele mai multe ori) stări terminale, deci
de sfârşit de joc, punctajul asociat acestora se estimează pe baza unei funcţii euristice care
indică cât de promiţătoare este o astfel de stare pentru câştigul jucătorului.
De pe fiecare nivel curent se caută pe următoarele n nivele ale spaţiului. Valoarea care
se atribuie unui nod în urma investigării a n nivele sub el şi folosind funcţia euristică nu este
o indicaţie dacă se câştigă sau se pierde, ci o estimare euristică a celei mai bune stări la care
se poate ajunge în n mişcări de la nodul dat. Descrierea algoritmului în această abordare
este:
Descriere Minimax( S )
pentru fiecare succesor Sj al lui S (obţinut printr-o mutare opj) execută
val( Sj ) ← Minimax( Sj )
aplică opj pentru care val( Sj ) este maximă
sfârşit
Minimax( S ) { întoarce o estimare a stării S }
dacă nivel( S ) = n atunci întoarce eval( S )
altfel
dacă MAX mută în S atunci
pentru fiecare succesor Sj al lui S execută
val( Sj ) ← Minimax( Sj )
întoarce max( val( Sj ), ∀j )
altfel { MIN mută în S }
pentru fiecare succesor Sj al lui S execută
val( Sj ) ← Minimax( Sj )
întoarce min( val( Sj ), ∀j )
sfârşit
Pentru a da un exemplu de funcţie de estimare, să considerăm jocul de Tic-Tac-Toe (X
şi O). Alegem o funcţie de estimare euristică eval( S ) ce încearcă să măsoare conflictul
existent în joc în starea S. Valorea eval( S ) este numărul total posibil de linii câştigătoare
ale lui MAX în starea S din care se scade numărul total posibil de linii câştigătoare ale lui
MIN în starea S. Prin linie câştigătoare se înţelege o linie orizontală, verticală sau diagonală
care a fost, este doar parţial sau poate fi completată numai cu X sau numai cu O. Dacă S
este o stare din care MAX poate face o mişcare cu care câştigă, atunci eval( S ) = ∞ (o
valoare foarte mare), iar dacă S este o stare din care MIN poate câştiga cu o singură mutare,
atunci eval( S ) = -∞ (o valoare foarte mică). Un exemplu este prezentat în figura 15.

127
X X are 6 linii câştigătoare posibile
O are 5 linii câştigătoare posibile
O eval( S ) = 6 - 5 = 1

Figura 15. Funcţia euristică de evaluare a unei stări în jocul Tic-Tac-Toe


Dezavantajul abordării cu adâncime de căutare finită egală cu n este acela că poate să
apară efectul de orizont, adică la nivelul n se neglijează căi care la nivelul (n + 1) ar putea fi
mai bune. Efectul de orizont poate fi parţial înlăturat dacă se investighează înainte anumite
stări care par a fi foarte promiţătoare. Astfel, anumite stări cheie (de exemplu captura unei
piese) se investighează la nivele suplimentare. De obicei se pune o limită de timp şi se
investighează iterativ nivele din ce în ce mai mari până când se atinge limita de timp
impusă.
Prezentăm în continuare implementarea în Prolog a acestui algoritm şi aplicarea lui la
jocul de Tic-Tac-Toe (X şi O):
% Implementarea algoritmului Minimax
% Partea independentă de jocul particular

% Predicatul minimax: minimax(+Poz, -CelMaiBun, -Val)


% Poz este poziţia considerată, Val este valoarea sa minimax.
% Cea mai bună mutare este întoarsă în CelMaiBun

minimax(Poz, CelMaiBun, Val) :-


% PozList = lista mutarilor posibile din pozitia Poz
mutari(Poz, [Prim | PozList]), !,
minimax(Prim, _ , VPrim),
bun(Poz, PozList, Prim, VPrim, CelMaiBun, Val).
minimax(Poz, _ , Val) :- valstatic(Poz, Val). % Poz nu are succesori

bun(Curenta, [], Poz, Val, Poz, Val) :- !.


bun(Curenta, [Poz | PozList], PozCurenta, ValCurenta, PozBuna, ValBuna) :-
minimax(Poz, _ , Val),
bun_dela(Curenta, Poz, Val, PozCurenta, ValCurenta, PozNoua, ValNoua),
bun(Curenta, PozList, PozNoua, ValNoua, PozBuna, ValBuna).

bun_dela(Curenta, Poz0, Val0, Poz1, Val1, Poz0, Val0) :-


muta_min(Curenta), Val0 < Val1, !.
bun_dela(Curenta, Poz0, Val0, Poz1, Val1, Poz0, Val0) :-
muta_max(Curenta), Val0 > Val1, !.
bun_dela(Curenta, Poz0, Val0, Poz1, Val1, Poz1, Val1).

128
% Jocul X şi 0
% O poziţie se memorează astfel: pos(x, [x, b, 0, b....], 3) unde
% x = jucătorul care mută curent
% [...] este ceea ce se găseşte în tablă, pe linii
% 3 reprezintă câte mutari se mai fac în adâncime, maxim, în minimax

% Generatorul de mutări posibile


mutari(Poz, PozList) :- bagof(P, mutare(Poz, P), PozList).
mutare(pos(P, Tabla, N), pos(O, Tabla1, N1)) :-
N > 0, not(final(Tabla, _ )),
opus(P, O), N1 is N - 1, subst(Tabla, P, Tabla1).

opus(x, 0). opus(0, x).


subst([b | Rest], P, [P | Rest]).
subst([X | Rest], P, [X | Rest1]) :- subst(Rest, P, Rest1).

% Evaluarea statică
valstatic(pos( _ , B, N), Val) :- final(B, P), !, castigator(P, N, Val).
valstatic(pos( _ , Tabla, _ ), Val) :-
lin1(Tabla, L1), lin2(Tabla, L2), lin3(Tabla, L3),
col1(Tabla, C1), col2(Tabla, C2), col3(Tabla, C3),
dia1(Tabla, D1), dia2(Tabla, D2),
Val is L1 + L2 + L3 + C1 + C2 + C3 + D1 + D2.

final([P, P, P | _ ], P) :- P \= b, !.
final([ _ , _ , _ , P, P, P | _ ], P) :- P \= b, !.
final([ _ , _ , _ , _ , _ , _ , P, P, P], P) :- P \= b, !.
final([ P, _ , _ , P, _ , _ , P | _ ], P) :- P \= b, !.
final([ _ , P, _ , _ , P, _ , _ , P | _ ], P) :- P \= b, !.
final([ _ , _ , P, _ , _ , P, _ , _ , P], P) :- P \= b, !.
final([P, _ , _ , _ , P, _ , _ , _ , P], P) :- P \= b, !.
final([ _ , _ , P, _ , P, _ , P | _ ], P) :- P \= b, !.
final(Tabla, b) :- not(member(b, Tabla)).

castigator(x, N, Val) :- !, Val is -10 * (N + 1).


castigator(0, N, Val) :- !, Val is 10 * (N + 1).
castigator( _ , _ , 0).

lin1([b, b, b | _ ], 1) :- !.
129
lin1( _ , 0).
lin2([_ , _ , _ , b, b, b | _ ], 1) :- !.
lin2( _ , 0).
lin3([ _ , _ , _ , _ , _ , _ , b, b, b], 1) :- !.
lin3( _ , 0).

col1([ b, _ , _ , b, _ , _ , b | _ ], 1) :- !.
col1( _ , 0).
col2([ _ , b, _ , _ , b, _ , _ , b | _ ], 1) :- !.
col2( _ , 0).
col3([ _ , _ , b, _ , _ , b, _ , _ , b], 1) :- !.
col3( _ , 0).

dia1([ b, _ , _ , _ , b, _ , _ , _ , b], 1) :- !.
dia1( _ , 0).
dia2([ _ , _ , b, _ , b, _ , b | _ ], 1) :- !.
dia2( _ , 0).

%Tipul nodului (min sau max)


muta_min(pos(x, _ , _ )).
muta_max(pos(0, _ , _ )).

% Jocul efectiv
run :-
primul(P), adancime(N), iniţial(Tabla), scrie_tabla(Tabla),
joc(pos(P, Tabla, N)).

primul(P) :-
write($Incepe un nou joc...$), nl, write($Eu sunt cu 0, tu esti cu x$), nl,
write($Cine incepe (x/0)? $), read(P).

adancime(N) :- write($Numarul de semi-mutari adancime? $), read(N).

iniţial([b, b, b, b, b, b, b, b, b]).

joc(pos(P, Tabla, _ )) :- final(Tabla, P1), !, scrie_castig(P1).


joc(pos(x, Tabla, N)) :- !,
det_mutare(Tabla, Succ), scrie_tabla(Succ),
joc(pos(0, Succ, N)).
joc(pos(0, Tabla, N)) :-
130
minimax(pos(0, Tabla, N), pos(x, Succ, _ ), Val),
scrie_mutare(Succ, Val), joc(pos(x, Succ, N)).

scrie_castig(b) :- !, write($Remiza.$), nl.


scrie_castig(x) :- !, write($Ai castigat.$), nl.
scrie_castig(_ ) :- write($Ai pierdut.$), nl.

det_mutare(Tabla, Succ) :-
repeat, det_coord(L, C), N is L * 3 + C, verifica(N, Tabla, Succ), !.

scrie_mutare(Tabla, Val) :-
write($Mutarea mea este: $), nl,
scrie_tabla(Tabla), write($Punctaj: $), write(Val), nl, anunta(Val).

anunta(10) :- !, write($Acum sunt sigur ca voi castiga.$), nl.


anunta(_ ).

det_coord(L, C) :-
write($Linia si coloana? $), read(L1), read(C1), L is L1 - 1, C is C1 - 1.

verifica(0, [b | Rest], [x | Rest]) :- !.


verifica(N, [X | Tabla], [X | Succ]) :- N1 is N - 1, verifica(N1, Tabla, Succ).

scrie_tabla(Tabla) :-
repl(Tabla, ' ', [E1, E2, E3, E4, E5, E6, E7, E8, E9]),
write($ 1 2 3$), nl,
write($1 $), write(E1), write($ $), write(E2), write($ $), write(E3), nl,
write($2 $), write(E4), write($ $), write(E5), write($ $), write(E6), nl,
write($3 $), write(E7), write($ $), write(E8), write($ $), write(E9), nl.

% Predicate de uz general
repeat.
repeat :- repeat.

member(X, [X | _ ]).
member(X, [_ | Rest]) :- member(X, Rest).

repl([], _ , []) :- !.
repl([b | Rest], X, [X | Rest1]) :- !, repl(Rest, X, Rest1).
repl([E | Rest], X, [E | Rest1]) :- repl(Rest, X, Rest1).
131
13.3 Algoritmul tăierii alfa-beta
Este posibil să se obţină decizia corectă a algoritmului Minimax fără a mai inspecta toate
nodurile din spaţiului de căutare până la un anumit nivel. Procesul de eliminare a unei
ramuri din arborele de căutare se numeşte tăiere (pruning).
Pentru a ilustra ideea acestui algoritm, se consideră spaţiul de căutare din figura 13,
redesenat în figura 16.
MAX A/3

MIN B/3 C/≤2 D/2

MAX E/3 F / 12 G/8 H/2 I/4 J/6 K / 14 L/5 M/2

Figura 16. Tăierea alfa-beta a spaţiului de căutare


Se observă că, odată găsit succesorul H cu valoarea 2 al nodului C, nu mai este necesară
inspectarea celorlaţi succesori ai nodului C, deoarece:
− Nodul C este pe un nivel de MIN şi va primi o valoare mai mică sau egală cu 2;
− Pentru nodul A, care este pe un nivel de MAX, am găsit anterior succesorul B
cu valoarea 3, deci A va avea o valoare mai mare sau egală cu 3, ceea ce
înseamnă că nodul C devine un succesor neinteresant A (valoarea lui C este mai
mică decât 3).
Fie α cea mai bună valoare (cea mai mare) găsită pentru MAX şi β cea mai bună
valoare (cea mai mică) găsită pentru MIN. Algoritmul alfa-beta actualizează α şi β pe
parcursul parcurgerii arborelui şi elimină investigările subarborilor pentru care α sau β sunt
mai proaste. Terminarea căutării (tăierea unei ramuri) se face după două reguli:
1. Căutarea se opreşte sub orice nod MIN cu o valoare β mai mică sau egală cu
valoarea α a oricăruia dintre nodurile MAX predecesoare nodului MIN în cauză.
2. Căutarea se opreşte sub orice nod MAX cu o valoare α mai mare sau egală cu
valoarea β a oricăruia dintre nodurile MIN predecesoare nodului MAX în cauză.
Deci algoritmul exprimă o relaţie între nodurile de pe nivelul n şi nodurile de pe
nivelul (n + 2). Dacă jucătorul MAX are posibilitatea să ajungă la un nod m care este mai
bun decât nodul curent de pe nivelul n, atunci într-un joc real, nodul de pe nivelul n nu
poate fi niciodată considerat, aşa cum se vede din figura 17.

132
Se observă că
A are β = 3 (A este pe un nivel MIN, deci val( A ) nu poate fi mai mare ca 3)
B este β tăiat deoarece 5 > 3
C are α = 3 (C este pe un nivel MAX, deci val( C ) nu va fi mai mică decât 3)
D este α tăiat deoarece O < 3
E este α tăiat deoarece 2 < 3, deci val( C ) este 3.

MAX C/3

MIN A/3 D/0 E/2

MAX F/3 B/3 G/0 H/2

MIN I/2 J/3 K/5 L/0 M/2 N/1

Figura 17. Eliminarea investigării unor noduri în algoritmul alfa-beta


Algoritmul alfa-beta constă în două proceduri: MIN şi MAX. Algoritmul începe fie
prin apelarea procedurii MAX, dacă jucătorul mută primul, fie prin apelarea procedurii
MIN, dacă adversarul mută primul, iniţial α fiind foarte mic (0) şi β foarte mare (∞).
MAX(S, a, b) { Întoarce valoarea maximă a unei stări. }
dacă nivel( S ) = n atunci întoarce eval( S )
altfel
pentru fiecare succesor Sj al lui S execută
a ← max(a, MIN(Sj, a, b))
dacă a ≥ b atunci întoarce b
întoarce a
sfârşit
MIN(S, a, b) { Întoarce valoarea minimă a unei stări. }
dacă nivel( S ) = n atunci întoarce eval( S )
altfel
pentru fiecare succesor Sj al lui S execută
b ← min(b, MAX(Sj, a, b))

133
dacă b ≤ a atunci întoarce a
întoarce b
sfârşit
Algoritmul alfa-beta nu elimină limitările de la Minimax, cum ar fi efectul de orizont.
Avantajul său este însă acela că reduce numărul de noduri inspectate, permiţând o creştere a
nivelului de adâncime în investigarea nodurilor următoare, n. Algoritmul devine eficient
dacă se face o ordonare a succesorilor în inspectare: cele mai bune mişcări ale lui MAX,
respectiv ale lui MIN, din punctul de vedere al lui MAX întâi. De exemplu, pentru jocul de
şah, unde factorul de ramificare este aproximativ egal cu 35, utilizarea algoritmului alfa-
beta şi o ordonare adecvată a stărilor inspectate poate reduce factorul de ramificare la 6,
ceea ce permite dublarea adâncimii de căutare, permiţând trecerea de la un nivel de novice
la un nivel de expert.
Prezentăm în continuare implementarea algoritmului alfa-beta în Prolog şi aplicarea
lui în jocul de Tic-Tac-Toe (X şi O):
% Implementarea algoritmului alfa-beta
% Partea independentă de jocul particular jucat

alfabeta(Poz, Alfa, Beta, SucBun, Val) :-


mutari(Poz, [P1 | LPoz]), !,
alfabeta(P1, Alfa, Beta, _ , V1),
nou(Alfa, Beta, P1, V1, NAlfa, NBeta),
bunsucc(LPoz, NAlfa, NBeta, P1, V1, SucBun, Val).
alfabeta(Poz, Alfa, Beta, SucBun, Val) :- valstatic(Poz, Val).

bunsucc([], _ , _ , P1, V1, P1, V1) :- !.


bunsucc([Poz | LPoz], Alfa, Beta, Poz1, Val1, PozBuna, ValBuna) :-
alfabeta(Poz, Alfa, Beta, _ , Val),
bun_dela(Poz, Val, Poz1, Val1, Poz2, Val2),
destuldebun(LPoz, Alfa, Beta, Poz2, Val2, PozBuna, ValBuna).

destuldebun(_ , Alfa, Beta, Poz, Val, Poz, Val) :-


muta_min(Poz), Val > Beta, !;
muta_max(Poz), Val < Alfa, !.
destuldebun(LPoz, Alfa, Beta, P, Val, PozBuna, ValBuna) :-
nou(Alfa, Beta, P, Val, NAlfa, NBeta),
bunsucc(LPoz, NAlfa, NBeta, P, Val, PozBuna, ValBuna).

nou(Alfa, Beta, Poz, Val, Val, Beta) :- muta_min(Poz), Val > Alfa, !.
nou(Alfa, Beta, Poz, Val, Alfa, Val) :- muta_max(Poz), Val < Beta, !.
134
nou(Alfa, Beta, _ , _ , Alfa, Beta).

bun_dela(Poz, Val, Poz1, Val1, Poz, Val) :-


muta_min(Poz), Val > Val1, !;
muta_max(Poz), Val < Val1, !.
bun_dela(_ , _ , Poz1, Val1, Poz1, Val1).

Pentru a aplica algoritmul alfa-beta la jocul Tic-Tac-Toe, singura modificare faţă de


implementarea prezentată în secţiunea anterioară este următoarea:
% Jocul efectiv
run :-
primul(P), adancime(N), iniţial(Tabla), scrie_tabla(Tabla),
joc(pos(P, Tabla, N)).

joc(pos(P, Tabla, _ )) :- final(Tabla, P1), !, scrie_castig(P1).


joc(pos(x, Tabla, N)) :- !,
det_mutare(Tabla, Succ), scrie_tabla(Succ), joc(pos(0, Succ, N)).
joc(pos(0, Tabla, N)) :-
alfabeta(pos(0, Tabla, N), -100, 100, pos(x, Succ, _ ), Val),
scrie_mutare(Succ, Val), joc(pos(x, Succ, N)).
anunta(X) :- X >= 10, !, write($Acum sint sigur ca voi cistiga.$), nl.
anunta( _ ).

13.4 Exerciţii propuse


EP1. Să se implementeze algoritmul alfa-beta pentru jocul NIM, pentru diferite numere
de beţe ale configuraţiei iniţiale, atât cu investigarea exhaustivă a spaţiului de căutare
(atunci când este posibil) cât şi cu impunerea unei limite în căutare.
EP2. Să se aplice algoritmul alfa-beta pentru jocul NIM.
EP3. Implementaţi jocul dumneavoastră preferat, alegând cea mai potrivită strategie de
joc.

14 Descompunerea problemelor în subprobleme


Rezolvarea unor probleme poate fi convenabil realizată prin descompunerea
alternativă a problemei în subprobleme care, la rândul lor, sunt descompuse în alte
subprobleme până la depistarea unor probleme a căror rezolvare este imediată, probleme
numite elementare. Reprezentarea unui astfel de proces poate fi descrisă prin grafuri sau
arbori Şi/Sau [Flo83].

135
14.1 Arbori Şi/Sau
În continuare se va prezenta căutarea în arbori Şi/Sau pentru anumite cazuri simple.
Într-o primă variantă se specifică compact întreg arborele Şi/Sau al posibilelor rezolvări ale
problemei şi se indică cum poate fi găsită o soluţie în această reprezentare. Un nod Sau este
reprezentat printr-o structură Prolog sau(NumeProb, ListăSuccesori) unde ListăSuccesori
reprezintă nodurile (subproblemele) în care se poate descompune alternativ problema
NumeProb. Un nod Şi este reprezentat printr-o structură Prolog si(NumeNod,
ListaSubprob) unde NumeNod este un nume convenţional asociat nodului Şi iar
ListăSubprob este lista subproblemelor ce trebuie rezolvate pentru a rezolva problema
iniţială. Un nod elementar (cu rezolvare imediată) este marcat de structura Prolog
elementar(NumeProb) iar un nod terminal neelementar (care nu mai poate fi descompus)
prin neelementar(NumeProb), unde NumeProb este numele subproblemei
corespunzătoare. O soluţie a problemei este un subarbore în care nodul problemă iniţială
este nod rezolvat. În soluţie, nodurile problemă elementară vor fi marcate prin structura
frunză(NumeNod) iar un nod Sau va avea un unic succesor, respectiv nodul Şi ales pentru
descompunere, marcat prin structura succ(NumeProb, ArboreSolutie).
% Prima variantă Prolog
% Descrierea compactă a spaţiului de căutare arbore Şi/Sau:
descriere(sau(a, [si(b, [elementar(d), sau(e, [elementar(h)])]),
si(c, [sau(f, [elementar(h), neelementar(i)]), elementar(g)])])).

% Rezolvarea problemei
% În frunze sunt puse problemele elementare ele fiind considerate rezolvate.
rezolv(elementar(Problemă), frunza(Problemă)).
% Un nod Sau este rezolvat dacă cel puţin unul din succesorii săi este rezolvat.
rezolv(sau(Problemă, Succesori), succ(Problemă, ArboreSolutie)) :-
member(S, Succesori),
rezolv(S, ArboreSolutie).
% Un nod Şi este rezolvat dacă toţi succesorii săi sunt rezolvaţi.
rezolv(si(Problemă, Succesori), si(Problema, ArboriSolutie)) :-
rezolv_toti(Succesori, ArboriSolutie).
rezolv_toti([], []).
rezolv_toti([Problema | Rest], [Arbore | Arbori]) :-
rezolv(Problema, Arbore), rezolv_toti(Rest, Arbori).

% Soluţia problemei
solutie(ArboreSolutie) :- descriere(Problema), rezolv(Problema, ArboreSolutie).

136
Într-o a doua variantă de reprezentare, nodurile Şi, Sau şi nodurile asociate
problemelor elementare sunt descrise prin predicatele:
• sau(NumeProb, NumeSuccSi) - indică o posibilă descompunere a problemei
NumeProb într-o mulţime de subprobleme reprezentată prin nodul Şi NumeSuccSi;
există câte un fapt Prolog pentru fiecare descompunere alternativă posibilă;
• si(NumeSi, ListaSucc) - indică un nod Şi cu numele NumeSi corespunzător
subproblemelor indicate in lista ListaSucc; NumeSi trebuie să apară într-un predicat sau;
• elementar(NumeProb) - indică o problemă elementară, cu rezolvare imediată.
Nodurile terminale neelementare vor fi specificate implicit prin faptul că ele nu apar
nici într-un predicat elementar nici într-un predicat sau, deci nu au descompunere.
Reprezentarea Prolog a soluţiei va fi aceeaşi ca în prima variantă.
% A doua variantă Prolog
% Descrierea spaţiului de căutare arbore Şi/Sau:
sau(a,b). sau(a,c). si(b,[d,e]). elementar(d). sau(e,h). elementar(h).
si(c,[f,g]). sau(f,h). sau(f,i). neelementar(i). elementar(g).

% Rezolvarea problemei
rezolv1(Problema, frunza(Problema)) :- elementar(Problema).
rezolv1(Problema, succ(Problema, ArboreSolutie)) :-
sau(Problema, S), rezolv1(S, ArboreSolutie).
rezolv1(Problema, si(Problema, ArboriSolutie)) :-
si(Problema, Succesori), rezolv_toti1(Succesori, ArboriSolutie).
rezolv_toti1([],[]).
rezolv_toti1([Problema | Rest], [Arbore | Arbori]) :-
rezolv1(Problema, Arbore),
rezolv_toti1(Rest, Arbori).

14.2 Căutarea în cazul regulilor de producţie


Rezolvarea problemelor într-un sistem bazat pe reguli de producţie cu înlănţuirea
înapoi a regulilor (backward chaining) [LS93, Flo93] poate fi convenabil reprezentată
printr-un spaţiu de căutare de tip arbore Şi/Sau. Regulile care concluzionează despre o
anumită valoare de atribut reprezintă descompuneri alternative ale problemei ce constă în
determinarea valorii acelui atribut iar ipotezele regulilor reprezintă subproblemele ce trebuie
rezolvate pentru a putea obţine concluzia regulii. În continuare rafinăm căutarea soluţiei
într-un spaţiu de căutare Şi/Sau prezentată în secţiunea anterioară pentru ca apoi să aplicăm
această rezolvarea pentru construirea unui micro-sistem bazat pe reguli de producţie, cu
înlănţuirea înapoi a regulilor. În programul ce urmeză vom face următoarele modificări faţă
de rezolvarea anterioară:

137
• spaţiul de căutare este graf şi nu arbore; pentru aceasta trebuie menţinută lista OPEN în
scopul testării nodurilor anterior parcurse; se reaminteşte că în OPEN se memorează
numai noduri problemă (deci numele nodurilor sau a problemelor elementare) şi nu se
memorează nodurile Şi;
• rezolvarea pemite afişarea explicaţiilor, deci a modului de soluţionare a problemei, prin
afişarea arborelui soluţie găsit (acesta are aceeaşi formă ca în secţiunea anterioară);
această facilitate este esenţială în cazul sistemelor bazate pe reguli de producţie;
• nodurile rezolvate, respectiv cele nerezolvabile, se marcheză ca atare prin adăugarea (cu
assert) a unui predicat corespunzător, în acest fel evitând investigarea multiplă a
aceleiaşi probleme.
Pentru început se consideră spţiul de căutare prezentat în figura 18 şi descris în
programul ce urmează. În figură, toate nodurile terminale sunt considerate elementare, deci
cu rezolvare imediată, un arbore soluţie posibil fiind pus în evidenţă de săgeţile îngroşate.
Dacă se elimină, de exemplu, faptele elem(f) şi elem(i), nodurile f şi i sunt implicit
considerate de program ca noduri terminale neelementare iar arborele marcat îngroşat
devine singurul arbore soluţie pentru problema iniţială a.
a

şi1 şi2

b c d e

şi3 şi4 şi3 şi5

f g h f g i

Figura 18. Spaţiu de căutare Şi/Sau


% Descrierea spaţiului de căutare
% Un arbore soluţie posibil: succ(a, si(si1, [succ(b, si(si4, [elem(h)])), elem(c)])).
% De încercat şi cu f şi g netrecuţi ca elementari;
% apoi şi cu h înlocuit cu a; apoi şi cu i neelementar.
sau(a, si1). sau(a, si2). si(si1, [b, c]). si(si2, [d, e]). sau(b, si3).
sau(b, si4). si(si3, [f, g]). si(si4, [a]). sau(d, si3). sau(d, si5).
si(si5, [i]). elem(c). elem(e). elem(f). elem(g). elem(h). elem(i).

138
rezolv(P) :-
rezolv(P, ArbSol, []),
( rezolvat(P, _ ), write(P), nl, write('Explicatii? (da/nu) '),
read(X), (X = da, afis(ArbSol) ; X = nu)
; nerezolvat(P), write('Nu exista solutie')).
rezolv(P, ArbSol, _ ) :- rezolvat(P, ArbSol), !.
rezolv(P, ArbSol, _ ) :- elem(P), ArbSol = fr(P), marchez_rezolvat(P, ArbSol).
rezolv(P, ArbSol, Open) :-
sau(P, S1), not member(P, Open),
rezolv(S1, ArbSol1, [P | Open]),
rezolvat(S1, _ ), !, ArbSol = succ(P, ArbSol1),
marchez_rezolvat(P, ArbSol).
rezolv(P, ArbSol, Open) :- si(P, Succesori),
rezolv_toti(Succesori, ArboriSol, Open), toti_rezolvati(Succesori), !,
ArbSol = si(P, ArboriSol), marchez_rezolvat(P, ArbSol).
rezolv(P, _ , _ ) :- marchez_nerezolvat(P).

rezolv_toti([], [], _ ).
rezolv_toti([P | Rest], [Arb | RestArb], Open) :-
rezolv(P, Arb, Open), rezolv_toti(Rest, RestArb, Open).

toti_rezolvati([]).
toti_rezolvati([P | Rest]) :- rezolvat(P, _ ), toti_rezolvati(Rest).

marchez_rezolvat(P, ArbSol) :- assert(rezolvat(P, ArbSol)).


marchez_nerezolvat(P) :- assert(nerezolvat(P)).

afis(fr(P)) :- write('Frunza '), write(P).


afis(si(P, LSuc)) :-
nl, write('Nod SI '), write(P), nl, write(' cu succesori '), writelis(LSuc).
afis(succ(P, S)) :- nl, write('Nod SAU '), write(P), write(' cu succesor '), afis(S).

writelis([]).
writelis([El | Rest]) :- afiş(El), write(' '), writelis(Rest).

member(X, [X | _ ]) :- !.
member(X, [ _ | Rest]) :- member(X, Rest).

139
% Se apeleaza init după fiecare rezolvare sau înainte de o nouă rezolvare
% pentru a şterge din baza de date Prolog ceea ce a fost marcat anterior.
init :- retract(rezolvat(X, Y)), fail.
init :- retract(nerezolvat(X)), fail.
init.

Predicatul init are ca scop iniţializarea memoriei de lucru a sistemului, deci eliminarea
marcajelor de rezolvat sau nerezolvat pentru diferite noduri înainte de o nouă consultaţie.
Considerăm acum aplicarea programului în cazul următorului set de reguli de producţie:
% r5: dacă mamifer = da şi carnivor = da şi dungi = da
% atunci tip = tigru
% r6: dacă mamifer = da şi carnivor = da şi pete = da
% atunci tip = leopard
% r3: dacă mănâncă_carne = da
% atunci carnivor = da
% r4: dacă dinţi_ascuţiţi = da şi fălci = da
% atunci carnivor = da
% r1: dacă are_păr = da
% atunci mamifer = da
% r2: dacă dă_lapte = da
% atunci mamifer = da

Propunem următoarea reprezentare a acestor reguli în Prolog. Trebuie menţionat că


există şi alte posibilităţi de reprezentare, de exemplu prin definirea unor operatori Prolog
pentru dacă, atunci, etc. Exerciţiile acestui capitol vor cere realizarea unor reprezentări
alternative a regulilor. Spre deosebire de spaţiul de căutare anterior, unde problemele
elementare erau predefinite, în cazul regulilor de producţie un nod terminal este un atribut
care nu apare în concluzia nici unei reguli. Neexistând nici o regulă care să concluzioneze
despre un atribut nu există o descompunere a problemei găsirii valorii acelui atribut în
subprobleme. Pentru un astfel de nod, sistemul trebuie să întrebe utilizatorul valoarea acelui
nod. Dacă utilizatorul indică ca valoare a atributului o valoare ce apare in ipoteza unei
reguli, nodul corespunzător devine rezolvat. În caz contrar, nodul corespunzător devine
nerezolvabil şi programul trebuie să găsească soluţii alternative. Întrebarea către utilizator
este pusă de predicatul elem([A,R]) care obţine pentru atributul A valoarea R indicată de
utilizator.
Pentru setul de reguli definite sistemul se lansează prin apelul predicatului
rezolv([tip,X]), unde predicatul rezolv este cel definit anterior.

140
sau([tip, tigru], r5).
sau([tip, leopard], r6).
si(r5, [[mamifer, da], [carnivor, da], [dungi, da]]).
si(r6, [[mamifer, da], [carnivor, da], [pete, da]]).
sau([carnivor, da], r3).
sau([carnivor, da], r4).
si(r3, [[mananca_carne, da]]).
si(r4, [[dinti_ascutiti, da], [falci, da]]).
sau([mamifer, da], r1).
sau([mamifer, da], r2).
si(r1, [[are_par, da]]).
si(r2, [[da_lapte, da]]).
elem([A, R]) :- not sau([A, R], _ ), write(A), write('? = '),
read(Raspuns), Raspuns = R.

14.3 Exerciţii propuse


EP1. Folosind operatorii predefiniţi:
:- op(600, xfx, ---> ).
:- op(500, xfx, : ).

nodurile se pot descrie prin clauze de forma:


a ---> or : [b, c]. % nod sau
b ---> and :[d, e]. % nod şi

Să se rescrie predicatele pentru determinarea soluţiei în această reprezentare.


EP2. Să se definească operatori Prolog, în maniera celor definiţi în secţiunea 8.3, pentru a
putea exprima regulile de producţie din sistem direct sub formă de fapte Prolog în forma
iniţială, de exemplu:
r5: dacă mamifer egal da si carnivor egal da si dungi egal da
atunci tip egal tigru.
În aceste condiţii să se rescrie predicatele de rezolvare.
EP3. Care este funcţionarea programelor din secţiunile 14.1 şi 14.2 în cazul în care există
mai multe soluţii. Comentaţi şi faceţi modificări.
EP4. Să se adauge sistemului posibilitatea de răspuns la întrebări de tipul "de ce?" (de ce
sistemul are nevoie de o anumită valoare), prin afişarea regulii curente a cărei satisfacere se
încearcă, şi la întrebări de tipul "cum?" (cum a fost obţinută o anumită valoare de atribut)
prin afişarea regulilor care au dus la deducerea acelei valori, evidenţiind şi valorile introduse
de utilizator.

141
Bibliografie
[Bra88] I. Bratko. Prolog Programming for Artificial Intelligence, Addison-Wesley,
1988.
[CM84] W. F. Clocksin şi C. S. Mellish. Programming in Prolog, 2nd edition, Springer
Verlag, 1984.
[Flo93] A. M. Florea. Elemente de inteligenţă artificială, U.P.B., 1993.
[LS93] G. F. Lugger şi W. A. Stubblefield. Artificial Intelligence Structures and
Stategies for Complex Problem Solving, Benjamin/Cummings, 1993.
[SS86] L. Sterling şi E. Shapiro. The Art of Prolog, The M.I.T. Press, 1986.

142
Cuprins

PARTEA I....................................................................................................................1

LIMBAJUL PROLOG..................................................................................................2

1 Entităţile limbajului Prolog.......................................................................................................................... 3


1.1 Fapte........................................................................................................................................................ 3
1.2 Scopuri .................................................................................................................................................... 4
1.3 Variabile.................................................................................................................................................. 4
1.4 Reguli...................................................................................................................................................... 6
1.5 Un program Prolog simplu...................................................................................................................... 9

2 Sintaxa limbajului Prolog........................................................................................................................... 10


2.1 Constante............................................................................................................................................... 10
2.2 Variabile................................................................................................................................................ 11
2.3 Structuri................................................................................................................................................. 11
2.4 Operatori ............................................................................................................................................... 12
2.5 Operatori definiþi de utilizator.............................................................................................................. 15
2.6 Liste....................................................................................................................................................... 17

3 Limbajul Prolog şi logica cu predicate de ordinul I................................................................................. 21

4 Structura de control a limbajului Prolog .................................................................................................. 24


4.1 Semnificaţia declarativă şi procedurală a programelor Prolog ............................................................. 24
4.2 Controlul procesului de backtracking: cut ºi fail................................................................................... 33
4.3 Predicate predefinite ale limbajului Prolog........................................................................................... 39
4.4 Direcţia de construire a soluţiilor.......................................................................................................... 55

PARTEA A II-A ......................................................................................................... 59

APLICAţII.................................................................................................................. 59

5 Mediul ARITY Prolog ................................................................................................................................ 59


5.1 Generalitãþi........................................................................................................................................... 59
5.2 Comenzile editorului............................................................................................................................. 59
5.3 Informaţiile de ajutor şi depanare ......................................................................................................... 62
5.4 Sintaxa ARITY Prolog.......................................................................................................................... 64
5.5 Exerciþii propuse .................................................................................................................................. 70

6 Recursivitate în Prolog ............................................................................................................................... 71


6.1 Relaþii recursive ................................................................................................................................... 71
6.2 Problema trezorierului........................................................................................................................... 73
6.3 Problema parteneriatului ....................................................................................................................... 74
6.4 Problema vecinilor ................................................................................................................................ 74
6.5 Problema unicornului............................................................................................................................ 75
6.6 Exerciţii propuse ................................................................................................................................... 77

7 Prelucrarea listelor în Prolog ..................................................................................................................... 78


7.1 Predicate de prelucrare a listelor ........................................................................................................... 78
7.2 Mulţimi ................................................................................................................................................. 80
7.3 Problema drumurilor ............................................................................................................................. 80
7.4 Exerciţii propuse ................................................................................................................................... 81

8 Mecanisme specifice Prolog........................................................................................................................ 82


8.1 Exemple de utilizare a mecanismului cut.............................................................................................. 82
8.2 Negaţia ca insucces ............................................................................................................................... 84
8.3 Utilizarea operatorilor ........................................................................................................................... 86
8.4 Fişiere.................................................................................................................................................... 88
8.5 Exerciţii propuse ................................................................................................................................... 90

9 Sortare şi căutare ........................................................................................................................................ 92


9.1 Metoda de sortare prin generare şi testare............................................................................................. 92
9.2 Metoda de sortare prin inserţie.............................................................................................................. 93
9.3 Metoda de sortare rapidă....................................................................................................................... 94
9.4 Arbori binari.......................................................................................................................................... 96
9.5 Exerciţii propuse ................................................................................................................................... 99

10 Probleme rezolvabile prin backtracking............................................................................................... 100


10.1 Problema ţăranului ............................................................................................................................ 100
10.2 Problema misionarilor şi canibalilor ................................................................................................. 102
10.3 Problema găleţilor cu apă.................................................................................................................. 104
10.4 Exerciþii propuse .............................................................................................................................. 106

11 Strategii de căutare în spaţiul stărilor................................................................................................... 106


11.1 Căutarea soluţiilor în spaţiul stărilor ................................................................................................. 107
11.2 Cãutare prin backtracking ................................................................................................................. 107
11.3 Cãutare pe nivel ºi în adâncime......................................................................................................... 108
11.4 Algoritmul A* ................................................................................................................................... 112
11.5 Exerciţii propuse ............................................................................................................................... 114

12 Utilizarea căutării în rezolvarea problemelor ...................................................................................... 115


12.1 Problema misionarilor ºi canibalilor ................................................................................................. 115
12.2 Problema mozaicului cu opt cifre ..................................................................................................... 117

144
12.3 Rezolvarea problemei mozaicului prin căutãri neinformate ............................................................. 118
12.4 Rezolvarea problemei mozaicului cu A* .......................................................................................... 121
12.5 Exerciţii propuse ............................................................................................................................... 124

13 Strategii de căutare aplicate la jocuri.................................................................................................... 125


13.1 Algoritmul Minimax pentru spaţii de căutare investigate exhaustiv................................................. 125
13.2 Algoritmul Minimax cu adâncime de căutare finită.......................................................................... 126
13.3 Algoritmul tăierii alfa-beta................................................................................................................ 132
13.4 Exerciþii propuse .............................................................................................................................. 135

14 Descompunerea problemelor în subprobleme ...................................................................................... 135


14.1 Arbori Şi/Sau .................................................................................................................................... 136
14.2 Căutarea în cazul regulilor de producţie ........................................................................................... 137
14.3 Exerciţii propuse ............................................................................................................................... 141

BIBLIOGRAFIE....................................................................................................... 142

145

You might also like