P. 1
Carte Romaneasca de Prolog

Carte Romaneasca de Prolog

|Views: 4,655|Likes:
Published by chmro
manual de prolog
manual de prolog

More info:

Published by: chmro on Jan 30, 2010
Copyright:Attribution Non-commercial

Availability:

Read on Scribd mobile: iPhone, iPad and Android.
download as PDF, TXT or read online from Scribd
See more
See less

11/03/2015

pdf

text

original

Sections

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: papagal(coco). iubeşte(mihai, maria). iubeşte(mihai, ana). frumoasă(ana). bun(gelu). deplasează(cub, camera1, camera2).

Aritate: 1 2 2 1 1 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
S :- S1, S2, …Sn.

O regulă Prolog exprimă un fapt care depinde de alte fapte şi este de forma:

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). bun(vlad). cunoaste(vlad, maria). cunoaste(vlad, ana). iubeste(mihai, maria). iubeste(X, Y) :bun(X), cunoaste(X, Y), frumoasa(Y). %1 %2 %3 %4 %5 %6

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) %2 bun(vlad)

cunoaste(X,Y) cunoaste(vlad,Y)

%6 frumoasa(Y) frumoasa(maria) %1 frumoasa(ana) SUCCES

SUCCES %3 cunoaste(vlad,maria) %4 cunoaste(vlad,ana) INSUCCES 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 n4 n3

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) (2) numelui structurii ( functorul structurii); 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) (2) (3) operatori infixaţi: operatori prefixaţi: operatori postfixaţi: xfx xfy yfx fx fy 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) (2) lista vidă (lista cu 0 elemente) este o listă 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]). yes ?- membru(X, [a, b, c]). X = a; X = b; X = c; no ?- membru(b, [a, X, b]). X = b; X = _0038; no %1 %2

%3

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) (2) (3) o clauză unitară pozitivă formată dintr-un singur literal pozitiv (literal nenegat); o clauză negativă formată numai din literali negaţi; 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) (b) (c) Orice număr raţional este un număr real. Există un număr prim. 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. 2. există o clauză C a programului; există o instanţă I a clauzei C astfel încât: 2.1. 2.2. Observaţii:
• În definiţia de mai sus clauzele referă atât fapte cât şi reguli Prolog. Antetul unei

antetul lui I să fie identic cu cel al lui S; toate scopurile din corpul lui I sunt adevărate, deci pot fi satisfăcute.

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 de scopuri

Execuþie sistem Prolog

indicator SUCCES/INSUCCES 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) INSUCCES parinte(ada, Y), str1(Y, misu) Y=gelu 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.

str1(gelu, misu)

str3(ada, misu) parinte(ada, misu) INSUCCES str3(ada, Y), parinte(Y, misu) Y=Y' parinte(ada, Y') 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.

parinte(gelu, misu) SUCCES

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) str3(mia, Y'), parinte(Y', Y) str3(mia, Y"), parinte(Y", Y') ... arbore infinit

INSUCCES parinte(mia, Y)

INSUCCES parinte(mia, Y')

INSUCCES parinte(mia, Y") 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) (2) (3) (4) Maimuţa este la uşă. Maimuţa este pe sol. Cutia este la fereastră. 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. (m2) Urcă pe cutie. (m3) Mută cutia. 31 apucă urcă mută(Poziţia1, Poziţia2)

(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. f(X, 2) :- 3 =< X, X < 6. f(X, 4) :- 6 =< X. La întrebarea: 33 %1 %2 %3

?- 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) X=1 Y=0 f(1,0) 1<3 SUCCES X=1 Y=0 f(1,0) 1=<3 2<0

f(1,Y), 2<Y X=1 Y=2 f(1,2) 3=<1 INSUCCES X=1 Y=4 f(1,4) 6=<1 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, !. min1(X, Y, Y) :- X > Y. min2(X, Y, X) :- X =< Y, !. min2(X, Y, Y).

% cut verde % cut roşu

Î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. p :- c. p :- a, !, b. p :- c. p :- c. p :- a, !, b. % Semnificaţia declarativă este: % (a ∧ b) ∨ c % indiferent de ordinea clauzelor (în general). % Semnificaţia declarativă este: % (a ∧ b) ∨ (~ a ∧ c). % Semnificaţia declarativă este: % 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, +Scopelse), pentru dacă-atunci-altfel; şi case([Conditie1 -> Scop1, … Conditien -> Scopn | Scopdefault), 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. 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.

(2)

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. 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. Toţi trei parametrii sunt instanţiati şi predicatul reuşeşte dacă Functor şi ListArg sunt functorul, respectiv lista argumentelor structurii Scop.

(2)

(3)

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. L=[+, a+b, c] ?- a * b + c =.. L. L = [+, a * b, c] ?- a + b * c =.. L. L = [+, a, b * c] % a + b + c = '+'(a, '+'(b, c)) % a * b + c = '+'('*'(a, b), c) % a + b * c = '+'(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] ?- bagof(X, clasificare(X, C), L). X = _0038, C = consoana, L = [b, c, d, f]; X = _0038, C = vocala, L = [a, e] no
• setof(X, Scop, Lista)

% o soluţie

% două soluţii

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]; X = _0038, C = vocala, L = [e, a, e]; no ?- setof(X, clasificare(X, C), L). X = _0038, C = consoana, L = [b, c, d, f]; X = _0038, C = vocala, L = [a, e]; no

% consoana d apare de două ori % vocala e apare de două ori

% consoanele apar o singură dată % vocalele apar o singură dată

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 ← Săgeată la dreapta → Săgeată în sus ↑ Un caracter la stânga. Un caracter la dreapta. O linie în sus. 59

Săgeată în jos ↓ Ctrl + ← Ctrl + → Home End PageUp PageDown Ctrl + Home Ctrl + End Ctrl + g

O linie în jos. Un cuvânt la stânga. Un cuvânt la dreapta. Începutul liniei. Sfârşitul liniei. O pagină în sus. O pagină în jos. Începutul fişierului. Sfârşitul fişierului Deplasare la linia specificată.

Selectarea, copierea şi căutarea în text se fac astfel: Backspace (←) Del Shift + ←, ↑, → sau ↓ Ctrl + r (Undo) Shift + Del (Cut) F2 (Copy) Shift + Ins (Paste) Del (Clear) F4 (Find) Ctrl + \ (Find Selected) F3 (Repeat Last Find) F5 (Change) Şterge un caracter la stânga cursorului. Şterge un caracter la dreapta cursorului. Selectează o porţiune de text, începând de la poziţia curentă a cursorului. Restaurează o linie la starea în care era când cursorul a fost mutat pe ea. Şterge textul selectat şi îl mută în clipboard. copiază în clipboard textul selectat. Copiază textul din clipboard. Şterge textul selectat, fără a-l copia în clipboard. Caută un şir în text. Caută în text un şir selectat anterior. Repetă ultima căutare. 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) F7 (Go to Last) Erase Buffer Reconsult Buffer Save on Exit Buffer-ul activ (în care se face editarea curentă) devine cel selectat. Bufer-ul anterior selectat devine activ. Şterge conţinutul buffer-ului curent. Reconsultă conţinutul buffer-ului curent. 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ă). Indentează o linie nouă la fel cu linia anterioară în buffer-ul curent. Marchează buffer-ul curent ca putând fi doar citit; buffer-ul nu mai poate fi modificat.

Indent Read Only

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

Meniul File: Operaţiile posibile cu fişiere sunt următoarele: New Open File Merge File Save File Save File As Consult File Deschide un fişier nou într-un buffer nou. Deschide un fişier deja creat într-un buffer nou. Deschide un fişier şi îl adaugă la sfârşitul buffer-ului curent. Salvează buffer-ul curent într-un fişier. Salvează buffer-ul curent într-un fişier cu un nume nou. 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ă. Închide toate fişierele deschise şi apoi, pe baza informaţiilor din fişierul indicat de utilizator, restaurează baza de date a 61

Restore Db From

Save Db Save Db As Dos Shell Halt

interpretorului şi deschide fişierele deschise la atunci când s-a făcut salvarea respectivă. Salvează baza de date a interpretorului şi informaţiile depre fişierele deschise în fişierul api.idb. Salvează baza de date a interpretorului şi informaţiile depre fişierele deschise într-un fişier indicat de utilizator. Lansează o sesiune MS-DOS. Revenirea în mediul ARITY se face prin comanda exit. 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: 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 + Argument

Se specifică ce predicate trebuie urmărite la depanare 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): Spy Trace On Call Exit Fail Redo …de la începutul satisfacerii predicatului. …de la terminarea satisfacerii predicatului. …de la eşuarea satisfacerii predicatului. …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 window) ; (redo) @ (accept goal) a (abort) Comutare între fereastra depanatorului şi fereastra aplicaţiei. După un exit, pentru a forţa resatisfacerea (redo) pentru scopul curent. Permite apelul oricărui scop Prolog, cu reîntoarcere imediată în depanator după terminarea scopului. Terminare forţată a programului (abort) şi reîntoarcere la prompter-ul interpretorului. Această comandă este indicată în locul comenzii Ctrl + c. 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 “????-“. Depanatorul trece la următorul port (punct de trasare). Dacă portul este marcat ca urmărit în trasare, depanatorul se opreşte şi aşteaptă noi comenzi, altfel merge mai departe. Afişează scopul curent de satisfăcut, folosind predicatul display. 63

b (break)

c sau Enter (creep) d (display goal)

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 obiecte obiecte elementare numere reale variabile constante numere întregi

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

1 2 4 7 5 3 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). place(john, fotbal). place(tom, baseball). place(eric, înot). place(mark, tenis). place(bill, X) :- place(tom, X). % 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 % lui ellen îi place tenisul % lui john îi place fotbalul % lui tom îî place baseball-ul % lui eric îi place înotul % lui mark îi place tenisul % lui bill îi place ce îi place lui tom

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

Exerciţii propuse
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) 2) 3) 4) 5) 6) 7) 8) Fiecare om îi este devotat cuiva. Oamenii încearcă să asasineze dictatorii faţă de care nu sunt devotaţi. Toţi pompeienii erau romani. Cezar era dictator. Fiecare roman fie îl ura pe Cezar, fie îi era devotat. Marcus era pompeian. Marcus era om. 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). fact(N,NFact) :N1 is N - 1, fact(N1, Rezult), NFact is N * Rezult. % Factorialul lui 0 este 1, punctul de oprire. % Factorialul lui N este NFact dacă: % calculăm (N - 1) pentru al putea pasa ca argument % fie Rezult factorial de (N - 1) % 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
Ştefan este vecin cu Petre. Ştefan este căsătorit cu o doctoriţă care lucrează la Spitalul de Urgenţă. Petre este căsătorit cu o actriţă care lucreaza la Teatrul Naţional. Ştefan este meloman şi Petre este vânător. Toţi melomanii sunt sentimentali. Toţi vânătorii sunt mincinoşi. Actriţele iubesc bărbaţii sentimentali. Soţii au aceiaşi vecini. Căsătoria şi vecinătatea sunt relaţii simetrice. 74

Ipoteze: 1) 2) 3) 4) 5) 6) 7) 8) 9)

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

EP2.

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
drum(bucuresti, ploiesti). drum(bucuresti, cheia). drum(cheia, brasov). drum(brasov, bucuresti). drum(cheia, sinaia). drum(ploiesti, sinaia). 80

Fie o bază de date cu drumuri între oraşe, de forma drum(oras1, oras2):

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

Exerciţii propuse
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
8.1

Mecanisme specifice Prolog
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, X) :- X >= Y. max(X, Y, Y) :- X < Y. ?- max(X, 3, 4). ?- max(X, 3, 3). ?- max(3, X, 4). ?- max(X, X, 3). X=4 X=3 X=4 X=3 % max(?X, ?Y, ?Max) max(X, Y, X) :- X >= Y, !. max( _ , Y, Y). X=4 X=3 X=4 X=3

Cazuri în care predicatul max trebuie să eşueze: ?- max(X, 3, 2). ?- max(3, X, 3). ?- max(3, X, 2). ?- max(X, Y, 3). ?- max(X, 3, X). ?- max(3, X, Y). ?- max(X, 3, Y). No No No No no no no no X=3 X=2 X = _0038 Y = 3 X=3 X = _0038 Y = _0038 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 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] ).

atomi.

8.5
EP1.

Exerciţii propuse
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
9.1

Sortare şi căutare
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ă
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ă.

Sortarea rapidă (quicksort) a unei liste de elemente L se defineşte recursiv astfel:

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
1) arborele vid este codificat cu nil; 2) un arbore nevid este codificat SubarboreDrept).

Fie reprezentarea în Prolog a arborilor binari definită în secţiunea 5.4: cu arb(Cheie, SubarboreStang,

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), not(ilegala(StareUrmatoare)), not(member(StareUrmatoare, [Stare | Vizitate])), lcv(StareUrmatoare, Solutie, [Stare | Vizitate]). % o nouă mutare % este ilegală? % este deja parcursă? % 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 h3(S) = nE(S) + 1, dacă barca este la vest şi nE(S) ≠ 0 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 8 7 6 2 3 4 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 Nivel Adâncime 30 mutări 28 apeluri BKT 30 mutări 28 apeluri

Căutare informată cu A* A* - h1 2 mutări 2 apeluri A* - h2 2 mutări 2 apeluri A* - h3 2 mutări 2 apeluri

Si 1 1 7 Si 2 2 1 7 Si 3 1 8 7 Si 4 2 1 7 Si 5 2 4 7 Si 6 6 1 8 Si 7 8 1 7

2 8 6 3 8 5 3 6 8 6

3 4 5

2 mutări 5 apeluri

4 mutări 4 5 4 2 5 3 4 5 5 mutări 57 apeluri 4 mutări 26 apeluri 15 apeluri

4 mutări 4 apeluri

4 mutări 4 apeluri

4 mutări 5 apeluri

4 mutări 4 apeluri

4 mutări 4 apeluri

30 mutări

Nu merge. D. s.

4 mutări 5 apeluri

4 mutări 4 apeluri

4 mutări 4 apeluri

D.s.

D. s.

5 mutări 6 apeluri

5 mutări 5 apeluri

5 mutări 5 apeluri

163 apeluri 338 apeluri

1 5 2 5 4

6 8 3 7 3

D. s.

D. s.

D. s.

D. s.

18 mutări

18 mutări

D. s. 74 apeluri

D. s. 163 apeluri

D. s.

D. s.

D. s.

23 mutări 124 apeluri

4 2 6 3 5

D. s. 74 apeluri

D. s. 163 apeluri

D. s.

D. s.

D. s.

22 mutări 84 apeluri

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 O

X are 6 linii câştigătoare posibile O are 5 linii câştigătoare posibile 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) 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) altfel pentru fiecare succesor Sj al lui S execută b ← min(b, MAX(Sj, a, b)) 133 { Întoarce valoarea minimă a unei stări. } dacă nivel( S ) = n atunci întoarce eval( S ) { Întoarce valoarea maximă a unei stări. } dacă nivel( S ) = n atunci întoarce eval( S )

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 alfabeta ş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. EP3. joc. Să se aplice algoritmul alfa-beta pentru jocul NIM. Implementaţi jocul dumneavoastră preferat, alegând cea mai potrivită strategie de

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: % % r6: % % r3: % % r4: % % r1: % % r2: % dacă mamifer = da şi carnivor = da şi dungi = da atunci tip = tigru dacă mamifer = da şi carnivor = da şi pete = da atunci tip = leopard dacă mănâncă_carne = da atunci carnivor = da dacă dinţi_ascuţiţi = da şi fălci = da atunci carnivor = da dacă are_păr = da atunci mamifer = da 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
EP1.

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

nodurile se pot descrie prin clauze de forma: a ---> or : [b, c]. b ---> and :[d, e]. % nod sau % 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] [CM84] [Flo93] [LS93] [SS86] I. Bratko. Prolog Programming for Artificial Intelligence, Addison-Wesley, 1988. W. F. Clocksin şi C. S. Mellish. Programming in Prolog, 2nd edition, Springer Verlag, 1984. A. M. Florea. Elemente de inteligenţă artificială, U.P.B., 1993. G. F. Lugger şi W. A. Stubblefield. Artificial Intelligence Structures and Stategies for Complex Problem Solving, Benjamin/Cummings, 1993. 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're Reading a Free Preview

Download
scribd
/*********** DO NOT ALTER ANYTHING BELOW THIS LINE ! ************/ var s_code=s.t();if(s_code)document.write(s_code)//-->