Sunteți pe pagina 1din 12

Strategii de cautare în spatiul starilor

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.

Cautarea solutiilor în spatiul starilor


Pentru a construi o descrierea unei probleme rezolvate prin reprezentare în spaţiul stărilor trebuie
parcurse urmatoarele etape:

1) Se defineste spatiul starilor care contine toate configuratiile posibile ale obiectelor
relevante (si poate si unele imposibile). Spatiul 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 specifica una sau mai multe stari din spatiu care descriu situatii posibile de la care poate
porni procesul de rezolvare a problemei (starea iniţială).

3) Se specifica una sau mai multe stari care ar fi acceptabile ca solutii ale problemei. Aceste
stari se numesc stari scop (sau stari finale sau scopuri).

4) Se specifica un set de reguli care descriu actiunile (operatorii) disponibile şi care definesc
tranziţiile sau transformările între stări. În aceasta etapa trebuie analizate urmatoarele
probleme:

• Ce presupuneri nedeclarate explicit sunt prezente în descrierea informala a


problemei?

• Cât de generale trebuie sa fie regulile?

• Cât de mult din calculul necesar rezolvarii problemei ar trebui facut înainte si
reprezentat în reguli?

Problema poate fi rezolvata apoi utilizând o strategie de control adecvata, pentru a parcurge o
parte din spatiul starilor (eventual tot) pâna când este gasita o cale de la o stare initiala la o stare finala,
în cazul în care problema admite soluţie. Cautarea este un mecanism general care poate fi utilizat atunci
când o metoda directa (deterministă) de rezolvare a problemei nu este cunoscuta. În acelasi timp, ea
ofera cadrul în care pot fi încapsulate metode mai directe de rezolvare a anumitor parti ale problemei
(subproblemelor).
Căutare prin backtracking
Rezolvarea unei probleme folosind strategia de backtracking a fost expusă in laboratorul anterior. Î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.

% Graful care descrie spatiul de cautare 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 maxima 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.
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 cautare 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). 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 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ă.

% Cautare pe nivel si în adâncime


% Se folosesc tipurile de date abstracte Stack si Queue.

% TDA Stack
% emptys(+S) - testeaza daca stiva este vida
% emptys(-S) - initializeaza stiva
emptyst([]).
% stack(-Top, -SNou, +S) - are efect de pop
% stack(+Top, +S, -SNoua) - are efect de push
% stack(-Top, _, +S) - are efect de top
stack(Top, Stack, [Top | Stack]).

% TDA Queue
% empty(+Q) - testeaza daca coada este vida
% empty(-Q) - initializeaza coada
emptyq([]).
% enqueue(+El, +Q, -QNoua) introduce un element în coada
enqueue(El, [], [El]).
enqueue(El, [X | Rest], [X | R1]) :- enqueue(El, Rest, R1).
% dequeue(-El, +Q, -QNoua) elimina un element din coada
dequeue(El, [El | R], R).

% Spatiul de cautare
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.
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 î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).

% Afiseaza 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).

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

rezbkt(Si) :-emptyst(Open), stack(Si, Open, NewOpen), bkt(NewOpen).


bkt(Open) :- stack(S, _, Open), final(S), write('S-a gasit o solutie'), afis(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ă.

Algoritmul A*
A* este un algoritm de cautare în spatiul starilor 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. Desi are o complexitate timp tot exponentiala, ca orice
procedură de căutare, algoritmul este mai eficient, în general, decât strategiile 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.

% Spatiul de cautare
succ(a, b). succ(b, c). succ(c, d). succ(d, g). succ(a, e). succ(e, f). succ(f, g).
final(g).
% Valorile functiei 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).

% Coada de prioritati (coada este sortata crescator în functie 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 gasit o solutie'), 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)).

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
Utilizarea cautarii în rezolvarea problemelor

Vom aplica acum schemele de strategii de căutare dezvoltate anterior pentru rezolvarea unei probleme
clasice în inteligenţa artificială. În acest scop 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.

Problema misionarilor şi canibalilor


Reprezentarea starii problemei va fi:

stare(MalBarca, NMisMalBarca, NCanMalBarca, NMisMalOpus, NCanMalOpus)

unde MalBarca poate fi est sau vest.

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 finala
final(st(vest, 3, 3, 0, 0)).

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

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


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

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


% muta doi misionari
succ(st(X, MX, CX, MY, CY), st(Y, MY1, CY, MX1, CX)) :-
opus(X, Y),
modifica(2, MX, MY, MX1, MY1),
sigur(MX1, CX), sigur(MY1, CY).
% muta 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 si 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).
% muta 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).
% muta 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.

% Afiseaza solutia
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 prezentate anterior.

% Se alege strategia dorita (nivel, adâncime sau backtracking)


solutie :-
nl, initial(Si),
rezbreadth(Si).
% rezdepth(st(est, 3, 3, 0, 0)).
% rezbkt(Si).

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

h1(S) = nE(S), numarul de persoane de pe malul de est în starea S


h2(S) = nE(S) / 2
nE(S) + 1, dacă barca este la vest şi nE(S) ≠ 0
h3(S) = nE(S) - 1, dacă barca este la est şi nE(S) ≠ 0
0 dacă nE(S) = 0

Funcţia h1 nu este admisibilă, funcţiile h2 şi h3 sunt admisibile şi monotone, cu h3 funcţie


euristică mai informată decât h2. Pentru a rezolva problema cu algoritmul A* se defineşte, de exemplu,
funcţia h3 în Prolog, se păstrează specificaţiile anterioare ale problemei şi se foloseşte strategia de
căutare informată definită anterior. Soluţia obţinută este minimă (optimă) în termeni de număr de
tranziţii de stare.

% Functia euristica 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).

Probleme propuse
Problema mozaicului cu opt cifre
Problema mozaicului cu opt cifre consta în gasirea mutărilor de executat pentru a ajunge dintr-o
configuraţie initiala a mozaicului într-o configuraţie finala. O configuraţie finală poate fi cea prezentată
în figura urmatoare:

1 2 3
8 4
7 6 5

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 ghidare a căutării.

Se cere rezolvarea acestei probleme folosind strategiile prezentate anterior.

Obs. 1 Testati, pentru diverse pozitii initiale, care metode reusesc si care nu. Proprietăţile
importante care determina esuarea unor metode si reusita altora sunt numarul de stări generate, numărul
de apeluri recursive şi numărul de mutari care rezolva problema.
Obs. 2 Pentru rezolvarea problemei printr-un algoritm A* se pot defini următoarele funcţii
euristice alternative:

1) h1(S) = numarul de cifre care nu sunt la locul lor în starea S faţă de starea finală;
2) h2(S) = suma distantelor, pe orizontală şi verticală, la care se afla cifrele în starea S fata de
pozitia lor finala (distanta Manhattan);
3) h3(S) = h2(S) + 3 ∗ D(S), unde D(S) este suma din d(c) pentru fiecare cifră c, mai putin
ultima, în starea S. Functia d(c) se calculeaza astfel:
• Daca c este pe pozitia în care se va afla în final spatiul liber, atunci d(c) = 1;
• Daca c este pe pozitia p, iar c + 1 se afla pe pozitia p + 1, atunci d(c) = 0;
• Daca c este pe pozitia p, iar c + 1 nu se afla pe pozitia 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. Functia h3 nu este admisibila deoarece în unele situatii
este mai mare decât h* (distanţa reală). Ea este utilă deoarece este mai informata decât h2 si ajunge
mult mai repede la o solutie buna, chiar daca solutia nu este optima (soluţia este destul de aproape de
soluţia optimă).

S-ar putea să vă placă și