Sunteți pe pagina 1din 5

Lucrarea nr.

8
Algoritmi recursivi

1. Scopul lucrării – îl reprezintă prezentarea în continuare a altor câtorva clase tipice de


algoritmi recursivi, respectiv algoritmii cu revenire (backtracking) şi algoritmii de tipul
“înlănţuie şi limitează” (branch and bound algorithms).

2.Aspecte teoretice
2.1. Algoritmi cu revenire (backtracking)
Principiul acestei tehnici este aceea de descompunere a obiectivului în obiective parţiale
(task-uri parţiale). De regulă acestea sunt exprimate în mod recursiv şi constă în exploatarea
unui număr finit de sub-task-uri. Întregul proces poate fi privit ca un proces de încercare şi
căutare care construieşte în mod gradat şi parcurge în acelaşi timp un arbore de subprobleme.
Obţinerea unor soluţii parţiale sau finale care nu satisfac provoacă revenirea recursivă în
cadrul procesului de calcul, până la obţinerea soluţiei dorite, motiv pentru care astfel de
algoritmi se numesc algoritmi cu revenire (backtracking algorithms)
O formă generală a algoritmului este următoarea :

function incearca;
begin
iniţializează selecţia candidatilor;
repeat
selectează următorul candidat;
if acceptabil then
begin
înregistrează-l;
if soluţie incompletă then
begin
încearcă pasul următor;
if nu este reuşit then sterge inregistrarea
end
end
until (pas reuşit) or (nu mai sunt candidaţi)
end;

Acest model principal, general, poate fi însă concretizat în mai multe forme. Astfel, dacă
presupunem că la fiecare pas numărul candidatilor de examinat este fix (M=8 în cazul
problemei calului prezentată la curs) precum şi că procedura este apelată iniţial prin
încearca(i), unde i reprezintă numărul mutării următoare, rezultă următoarea formă
particulară a algoritmului:

function incearca (i:integer);


var k: integer;
begin
k:=0;
repeat
k:=k+1;{selecteaza al k-lea candidat}
if acceptabil then
begin
înregistrează-l;
if i<n_total_solutie_completa then
begin
incearca(i+1);
if nereusita then
şterge înregistrarea
end
end
until reuşită or (k=m)
end;

Forma de mai sus a algoritmului generează însă o singură soluţie pentru problema dată;
pentru determinarea tuturor soluţiilor unor astfel de probleme (dacă există) este necesar ca
generarea candidaţilor să se facă într-o manieră ordonată, ceea ce garantează că un candidat
nu poate fi generat decât o singură dată. De îndată ce o soluţie a fost găsită şi înregistrată (de
exemplu, tipărită), se trece la determinarea soluţiei următoare pe baza unui proces de selecţie
sistematică, până la epuizarea tuturor posibilităţilor. Schema generală derivată din cea
anterioară (cu k mergând până la un m fix) care rezolvă această problemă este următoarea:

function incearca(i:integer);
var k:integer;
begin
for k:=1 to m do
begin
selectează cel de-al i-lea candidat;
if acceptabil then
begin
înregistrează-l;
if i< n_total_solutie_completa
then incearca(i+1)
else tipăreşte soluţia;
şterge înregistrarea
end
end
end;

Din cauza simplificării la un singur termen a condiţiei de terminare a procesului de selecţie


(k=M în acest caz), ciclul repeat a putut fi înlocuit cu unul for.

Problema turneului calului pe o tablă de șah


În continuare se vor ilustra principiile care stau la baza construirii unor astfel de algoritmi
prin exemplul cunoscut sub denumirea de "turneul calului". Problema este următoarea:
Se dă o tabelă (n x n) cu n2 câmpuri. Un cal căruia îi este permisă deplasarea conform
regulilor șahului, este plasat pe câmpul de coordonate (x0,y0). Problema este de a găsi acel
parcurs al calului, dacă există vreunul, care acoperă toate câmpurile tabelei, trecând o singură
dată prin fiecare poziție.
În realizarea algoritmului se vor considera următoarele:
- în câmpurile tablei T se memorează valori întregi astfel încât:
T[x,y] =0  câmpul de coordonate (x,y) nu a mai fost vizitat
T[x,y] =i  câmpul de coordonate (x,y) a fost vizitat la mutarea i
- se introduce parametrul q, de tip boolean, care marchează prin valoarea sa (true sau false)
reuşita sau nereuşita, din punct de vedere a acceptării mişcării
- “tabela nu e plină” se exprimă prin iN2 sau, dacă se introduc variabilele u şi v pentru a
preciza coordonatele posibile ale mişcării, atunci această condiție se mai poate exprima prin
1<v<N şi 1<u<N
- câmpul nu a mai fost vizitat anterior se exprimă prin t[u,v]=0
- înregistrează mișcarea următoare devine t[u][v]=i
- șterge mișcarea anterioară se exprimă prin t[u][v]=0
- se mai introduce variabila i, în care se va păstra valoarea mișcării curente.
În pasul următor de rafinare se mai ține cont de câteva aspecte. În primul rând, saltul
calului fiind dat de o poziție inițială: în acest caz există 8 posibilități pentru mutare. O metoda
simplă de obținere a poziției următoare (u,v) din poziția inițială (x,y) este de a aduna la acesta
din urmă două tablouri de diferențe. În program, aceste tablouri au fost notate cu a respectiv b
și inițializate în programul principal.
Se mai introduce și un indice k care precizează “următorul candidat“, adică următoarea
mutare posibilă, k=1..8. Când k=8 înseamnă că au fost testate toate mutările posibile. În
cadrul programului procedura este apelată inițial prin încearca(i), unde i reprezintă numărul
mutării următoare.
Deplasarea calului pe o tablă de șah (8*8) trebuie să țină cont de regula: calul mută pe tabla
de șah in formă de L, marcând trei pătrățele în orice direcție. Pentru a parcurge fiecare căsuță
de pe tabla de șah, calul trebuie să facă 64 de pași.

#include <stdio.h>
#include <conio.h>
#include <iostream>
using namespace std;
#define TRUE 1
#define FALSE 0
#define n 5
int i, j, a[9], b[9], t[n + 1][n + 1], q;
int incearca(int i, int x, int y)
{
int k, u, v, q1;
k = 0;
do
{
k = k + 1;
q1 = FALSE;
u = x + a[k]; v = y + b[k];
if ((1 <= u) && (u <= n) && (1 <= v) && (v <= n))
{
if (t[u][v] == 0)
{
t[u][v] = i;
if (i<n*n)
{
q1 = incearca(i + 1, u, v);
if (q1 == FALSE) t[u][v] = 0;
}
else
q1 = TRUE;
}
}
} while ((q1 == FALSE) && (k<8));
return q1;
}

int main()
{
a[1] = 2; b[1] = 1;
a[2] = 1; b[2] = 2;
a[3] = -1; b[3] = 2;
a[4] = -2; b[4] = 1;
a[5] = -2; b[5] = -1;
a[6] = -1; b[6] = -2;
a[7] = 1; b[7] = -2;
a[8] = 2; b[8] = -1;
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++) t[i][j] = 0;
t[1][1] = 1;
q = incearca(2, 1, 1);
if (q == TRUE)
{
cout<<"\n";
for (i = 1; i <= n; i++)
{
for (j = 1; j <= n; j++) cout << "\t" << t[i][j];
cout<<"\n";
}
}
else
cout<<"nu exista solutie";
}

3. Probleme propuse :

1. Fie n>0, natural. Să se scrie un program care să afişeze toate partiţiile unui număr natural
n. Se numește partiţie a unui număr natural nenul n o mulţime de numere naturale nenule {p1,
p2, …, pk} care îndeplinesc condiţia p1+p2+ …+pk = n.
2. Se consideră un labirint tridimensional de dimensiune NxNxN. Să se realizeze programul
cu ajutorul căruia se va calcula un anumit drum prin acest labirint de la o anumită poziţie de
start la o anumită destinaţie.
3. Se consideră un set de NxN piese, fiecare având 4 valori pe acestea. Să se aranjeze şi să se
rotească piesele în aşa fel încât valorile vecine între acestea să coincidă.
Exemplu :
4. Numai 12 din cele 92 de soluţii ale problemei celor 8 regine diferă în mod esenţial,
celelalte pot fi deduse din cele 12 prin considerente de simetrie faţă de axe sau faţă de centru.
Să se scrie programul care determină cele 12 soluţii.