Sunteți pe pagina 1din 9

INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

Problemă: Se dă o matrice binară, reprezentând harta unui teren (labirint, clădire, tablă de
joc, etc.), în care valorile 0 reprezintă zone libere, iar cele egale cu 1 reprezintă obstacole.
Într-o zonă aflată la coordonate cunoscute se află un mobil, care se poate deplasa prin
matrice, trecând din zona curenta în una din cele patru zone vecine de pe aceeași linie sau
aceeași coloană, dacă este liberă. Să se identifice zonele în care poate să ajungă mobilul.

Problema enunțată mai sus face parte din categoria Algoritmilor de umplere sau FILL, iar
prezentul articol este consacrat acestor algoritmi!

1.ALGORITM DE UMPLERE : GENERALITATI:


Pentru a identifica zonele în care poate ajunge mobilul, le vom marca în matrice cu o altă
valoare (de exemplu cu 2); vom face operația pas cu pas, pe următorul principiu: dacă o
anumită zonă a matricei este zonă curentă (mobilul se află în acea zonă), atunci acea zonă
va fi marcată ca vizitată, iar vecinii acelei zone care sunt liberi și nu au fost vizitați vor
deveni la rândul lor zone curente.

Identificarea vecinilor
Înainte de a descrie efectiv modul în care se realizează umplerea, să analizăm conceptul de
vecin al unui element din matrice. Deși nu este nou, este bine să sistematizăm lucrurile,
pentru a obține algoritmi cât mai clari și mai ușor de depanat!

În probleme este precizat care sunt vecinii unui element al matricei. De regulă, sunt
elementele învecinate pe linie și pe coloană, dar pot fi și pe diagonală, sau în formă de L,
așa cum se deplasează calul de șah.

1|Page
INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

Pentru a identifica vecinii elementului curent, trebuie să identifică coordonatele


acestora. Dacă elementul curent are coordonatele i,j, vecinii vor avea coordonatele din
imaginea următoare (am considerat vecinii pe linie și coloană):

Pentru a evita repetarea unor instrucțiuni aproape identice – singura diferență fiind
coordonatele vecinului curent, putem construi două tablouri unidimensionale di[], dj[] –
numiți și vectori de deplasare:

 numărul de elemente ale vectorilor de deplasare este egal cu numărul de vecini ai


unui element;
 valorile elementelor vectorilor de deplasare sunt astfel alese, încât adunându-le la i,
respectiv j, să obținem coordonatele vecinilor elementului de coordonate i,j;

2|Page
INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

 identificarea vecinilor se face parcurgând vectorii de deplasare.

Când considerăm vecinii pe linie și coloană, vectorii de deplasare pot fi – ordinea


elementelor în vectori nu este neapărat importantă, dar este necesară “sincronizarea” între
vectori:

int di[]={-1, 0, 1, 0},


dj[]={ 0, 1, 0,-1};

Pentru vectorii de mai sus:

 i+di[0],j+dj[0] înseamnă i-1,j+0 și reprezintă vecinul de pe linia anterioară –


vecinul din NORD
 i+di[1],j+dj[1] înseamnă i+0,j+1 și reprezintă vecinul de pe coloana următoare
– vecinul din EST
 i+di[2],j+dj[2] înseamnă i+1,j+0 și reprezintă vecinul de pe linia următoare –
vecinul din SUD
 i+di[3],j+dj[3] înseamnă i+0,j-1 și reprezintă vecinul de pe coloana anterioară
– vecinul din VEST

Analiza vecinilor elementului curent se poate face cu o secvență de forma:

for(int k = 0 ; k < 4 ; k ++)


{
// analizăm vecinul i+di[k], j+dj[k]
}

Bordarea matricei
Un alt aspect care trebuie avut în vedere constă evitarea ieșirii din matrice – analiza unor
elemente care nu fac parte din matrice, ceea ce are de regulă consecințe impredictibile. În
cazul vecinilor pe linie și coloană, majoritatea elementelor au patru vecini, dar există și
elemente cu trei vecini – chenarul matricei, precum și elemente cu doi vecini – colțurile:

3|Page
INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

Pentru a evita cazurile particulare putem borda matricea cu obstacole sau putem pentru fiecare
element vecin, determinat cu ajutorul vectorilor de deplasare, să verificăm dacă aparține
matricei, înainte de a-l analiza/prelucra:

for(int k = 0 ; k < 4 ; k ++)


{
int iv = i+di[k], jv = j+dj[k];
if(iv >= 1 && jv >= 1 && iv <=n && jv <= m)
{
//analizam vecinul iv,jv
}
}

2.FILL RECURSIV:
Operația de umplere poate fi sistematizată astfel:

 avem un anumit element al matricei, precizat prin coordonatele sale – elementul


curent;
 dacă elementul curent respectă anumite restricții (este în matrice, nu conține
obstacol, nu a fost deja marcat):
o îl marcăm;
o continuăm în același mod cu fiecare vecin al său.

Ultima etapă – continuarea în mod similar cu vecinii elementului curent, ne duce cu gândul
la recursivitate. Următorul program realizează umplerea pornind dintr-o poziție dată:

#include <iostream>

using namespace std;

const int di[]={-1, 0, 1, 0},


dj[]={ 0, 1, 0,-1};

int A[101][101], n , m, istart , jstart;

void Fill(int i ,int j ,int v)

4|Page
INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

{
// i,j - elementul curent, v - valoarea cu care facem Fill
if(i >= 1 && i <= n && j >= 1 && j <= m && A[i][j] == 0)
{ // in matrice, element liber si nemarcat
A[i][j] = v;
for(int k = 0 ; k < 4 ; k ++)
Fill(i + di[k] , j + dj[k], v);
}
}

int main()
{
cin >> n >> m;
for(int i = 1 ; i <= n ;i ++)
for(int j = 1 ; j <= m ; j ++)
cin >> A[i][j];
cin >> istart >> jstart;

Fill(istart, jstart, 2);

for(int i =1 ; i <= n ;i ++, cout << endl)


for(int j = 1; j <= m ; j ++)
cout << A[i][j] << " ";

return 0;
}

3.FILL CU COADA:
Algoritmul de umplere (FILL) pe matrice funcționează pe ideea: marcăm elementul curent și
analizăm vecinii liberi și nemarcați ai acestuia. Pentru a gestiona ordinea în care se face
acest lucru putem folosi recursivitatea sau putem folosi o structură de date de tip coadă
– queue. Această metodă are o serie de avantaje, fiind de regulă mai rapidă.

5|Page
INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

Reamintim, coada este o structură de date de tip FIFO – first in, first out. Elementele se
elimină strict în ordinea în care au fost adăugate.

2. Algoritmul de umplere cu coadă

Vom folosi o coadă în care elementele sunt perechi de indici ai tabloului. Algoritmul de
umplere este următorul:

 marcăm poziția inițială în matrice și o adăugăm în coadă;


 cât timp coada este nevidă:
o scoatem din coadă un element
o analizăm vecinii acestuia:
 dacă vecinul curent este liber și nemarcat, îl marcăm în matrice și îl
adăugăm în coadă

Algoritmul este eficient, fiecare element accesibil în matrice pornind de la poziția inițială (de
fapt coordonatele lui) este adăugat și eliminat din coadă exact o dată. Astfel numărul de
elemente cozii este cel mult n*m (unde n și m sunt dimensiunile matricei).

Modalități de implementare
Pentru implementarea cozii se pot folosi mai multe metode:

 simulare folosind un tablou unidimensional și doi indici, st dr, unde st este indicele


elementului de la începutul cozii (cel care urmează a fi scos), iar dr este indicele
elementul de la sfârșitul cozii (ultimul adăugat);
 o structură de date de tip coadă specifică limbajului de programare folosit – de
exemplu containerul C++ STL queue;
 implementare folosind alocarea dinamică – liste simplu înlănțuite alocate dinamic.

Acest articol prezintă primele două metode.

Simularea cozii cu tablou


Pentru această variantă avem nevoie de un tablou unidimensional (vector) în care să
memorăm elementele cozii. Observații:

 dimensiunea acestui tablou trebuie să fie suficient de mare pentru a memora toate
elementele parcurse – în caz extrem toate elementele matricei. Dimensiunea trebuie
deci să fie n*m.
 elementele tabloului sunt perechi de indici (linie-coloană). Pentru aceasta vom
declara o structură cu două câmpuri: linie, coloana.
 gestionarea cozii se face folosind doi indici, st și dr:
o dr reprezintă finalul cozii – poziția ultimului element adăugat;
o st reprezintă începutul cozii – poziția elementul care urmează a fi eliminat.
La eliminare, elementul nu se șterge propriu-zis din vector (operația este prea

6|Page
INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

costisitoare ca timp); îl mărim doar pe st, ignorând elementele aflate în


tablou înaintea sa – elementele cu indici între 1 și st-1.

Următoarea secvență conține declarațiile specifice algoritmului cu coadă și definiția unei


funcții C++ care realizează umplerea pornind dintr-o poziție dată – programul complet este
atașat articolului:

struct Coordonate
{
int linie, coloana;
};

Coordonate Q[100 * 100 + 1];

void Fill(int istart ,int jstart ,int v)


{
int st = 1 , dr = 0;
//initializare coada
dr ++;
Q[dr].linie = istart, Q[dr].coloana = jstart;
//marcare pozitie de start
A[istart][jstart] = v;
while(st <= dr) // cat timp coada este nevida
{
int i = Q[st].linie, j = Q[st].coloana; // determinam elementul de la
inceputul cozii
for(int k = 0 ; k < 4 ; k ++)
{
int iv = i + di[k], jv = j + dj[k]; // coordonatele vecinului
if(iv >= 1 && iv <= n && jv >= 1 && jv <= m // element în matrice
&& A[iv][jv] != 1 // element liber
&& A[iv][jv] != v) // nemarcat
{
// marcam elementul vecin
A[iv][jv] = v;

7|Page
INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

// il adaugam in coada
dr ++;
Q[dr].linie = iv , Q[dr].coloana = jv;
}
}
st ++; // eliminam din coada
}
}

Algoritm cu STL queue


Containerul STL queue permite gestionarea unei cozi, având definite toate operațiile
necesare. Înainte de a prezenta secvența C++ care folosește acest container în cadrul
algoritmului de umplere, să facem o scurtă prezentare a containerului queue și a operațiilor
specifice:

 pentru a folosi containerul, trebuie incluse headerul queue: #include <queue>


 declarăm o variabilă pentru coadă: queue<pair<int,int> Q;
 elementele cozii vor fi gestionate dinamic, memoria alocată lor fiind în HEAP, nu
în STACK;
 pentru algoritmul de umplere, în coadă trebuie să memorăm coordonatele unor
elemente din matrice. în acest scop putem folosi clasa C++ pair, care încapsulează
două valori. Accesul la cele două valori se face prin câmpurile first și second.
Crearea unei variabile de tip pair se poate face prin intermediul
funcției make_pair(v1,v2);
 în gestionarea cozii se folosesc patru operații:
o verificarea faptului că în coadă avem sau nu elemente (coada este nevidă
sau vidă): Q.empty() – returnează true sau false;
o identificare elementului de la începutul cozii: Q.front();
o adăugarea unui element în coadă: Q.push(valoare);
o eliminarea unui element din coadă: Q.pop().

Următoarea funcție C++ realizează umplerea pornind dintr-o poziție dată – programul
complet este atașat articolului:

void Fill(int istart ,int jstart ,int v)


{
queue<pair<int,int>> Q;
//initializare coada
Q.push(make_pair(istart , jstart));

8|Page
INFO CLASA A X-A : RECURSIVITATE -> ALGORITMI DE UMPLERE

//marcare pozitie de start


A[istart][jstart] = v;
while(! Q.empty()) // cat timp coada este nevida
{
int i = Q.front().first, j = Q.front().second; // determinam elementul de la
inceputul cozii
for(int k = 0 ; k < 4 ; k ++)
{
int iv = i + di[k], jv = j + dj[k]; // coordonatele vecinului
if(iv >= 1 && iv <= n && jv >= 1 && jv <= m // element în matrice
&& A[iv][jv] != 1 // element liber
&& A[iv][jv] != v) // nemarcat
{
// marcam elementul vecin
A[iv][jv] = v;
// il adaugam in coada
Q.push(make_pair(iv , jv));
}
}
Q.pop(); // eliminam din coada
}
}

9|Page

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