Sunteți pe pagina 1din 8

TEMA 3

ALGORITMUL LUI LEE

Algoritmul Lee este o soluție posibilă pentru problemele de


rutare a labirintului pe baza căutării Breadth-first. Oferă întotdeauna o
soluție optimă, dacă există, dar este lentă și necesită memorie
considerabilă. Acest algoritm este de fapt o parcurgere în lățime (BFS)
a unui graf, doar că e aplicat pentru o grilă. BFS este un algoritm
destul de eficient, având complexitatea de O(M*N), fiind foarte utilizat
în problemele în care apare un labirint.

Prezentare generală

Algoritmul lui Lee este un algoritm ce determină numărul minim


de pași, pentru a ajunge din punctul x în punctul y în anumite condiții
(de exemplu: evitând obstacole). Cu acest algoritm se face introducerea
unei noi structuri în informatica: coada (sau queue). Inainte de a ne
apuca sa implementam acest algoritm, vreau să vă spun că trebuie să
aveți câteva cunoștiințe de bază în limbajul C++.
Algoritmul lui Lee reprezintă una dintre cele mai cunoscute
aplicații ale cozii (ca structură de date) și este folosit de obicei pentru
determinarea drumului minim dintre două celule ale unei matrice.
Algoritmul lui Lee există mai mult în folclorul românesc; străinii
îl consideră doar un breadth first search pe un caz particular de graf
(matrice). Ei prin Algoritmul lui Lee se referă mai degrabă la un
algoritm de înfășurătoare convexă. Voi prezenta Algoritmul lui Lee, cum
se folosește în problema labirintului, precum și câteva aplicații ale
acestuia.
Se dă un labirint sub forma unei matrice cu
m linii și n coloane, unde dacă o celulă este 0,
aceasta se consideră accesibilă de către
șoarece, iar dacă este - 1 inaccesibilă. De
asemenea, se dau coordonatele pozițiilor în
care se află inițial șoarecele și bucata de
brânză.

1 din 8 Profesor A.D.


La fiecare pas, șoarecele se poate deplasa într-una dintre
pozițiile imediat vecine la nord, sud, est sau vest, cu condiția ca aceasta
să fie accesibilă și, desigur, să se afle în interiorul matricei. Să se
determine lungimea minimă a unui drum de la șoarece la brânză,
precum și un astfel drum. Dacă nu se poate ajunge la brânză (este
înconjurată de obstacole), se va afișa 0.
Iată matricea corespunzătoare exemplului de mai jos. Am
reprezentat cu violet pozițiile accesibile, cu negru pozițiile inaccesibile,
cu roșu poziția șoarecelui, iar cu albastru poziția brânzei.

DATE DE INTRARE (labirint.in)

DATE DE IEȘIRE (labirint.out)

2 din 8 Profesor A.D.


Algoritmul Lee

Vom utiliza o matrice mat, cea pe care o citim, și pe care o vom


folosi de asemenea pentru a calcula niște rezultate parțiale despre care
voi vorbi imediat. Avem nevoie și de o coadă q, cu elemente de tip Pos.
Tipul Pos este un struct pentru reținerea coordonatelor celulelor din
matrice. Acesta conține câmpurile lin și col, pentru linia și coloana
poziției stocate.
1. În coada q se adaugă poziția șoarecelui.
2. Se completează această poziție din mat cu valoarea 1.
3. Cât timp coada nu este vidă și nici nu am găsit lungimea minimă:
 Extragem primul element din coadă. Să-i zicem pos.
 Îi parcurgem vecinii din matrice:
 Dacă vecinul curent este accesibil și nevizitat:
● Îl marcăm drept vizitat, completând celula sa
corespunzătoare din mat cu valoarea mat[pos.lin]
[pos.col] + 1.
● Îl introducem în coadă.
4. Afișăm valoarea din mat de pe poziția unde se află bucata de brânză.

Complexitatea algoritmului este O(m⋅n)O(m⋅n), deoarece


fiecare celulă a matricei este vizitată maxim o singură dată. Această
complexitate este extrem de bună, având în vedere dimensiunile input-
ului.

Explicația algoritmului

În timpul execuției algoritmului, în matricea mat, o poziție are


valoarea -1 dacă este inaccesibilă, 0 dacă încă nu a fost vizitată, sau
distanța minimă de la șoarece la ea altfel. Algoritmul lui Lee se bazează
pe următoarea idee: Dacă știm lungimea drumului optim de la șoarece
până la poziția accesibilă de coordonate i și j (mat[i][j]), putem
actualiza lungimile drumurilor minime pentru vecinii accesibili (și
nevizitați încă) ai ei; aceste valori vor fi egale cu mat[i][j] + 1, pentru
că de la poziția (i, j) până la un vecin de-ai ei se mai face un singur pas.
Asta e practic o relație de recurență, așa că Algoritmul lui Lee poate fi
considerat un algoritm de programare dinamică.

3 din 8 Profesor A.D.


Totuși, recurența asta nu e suficientă. Mai trebuie să ținem cont
de ordinea în care completăm matricea mat. După ce am completat
toate celulele din mat cu valoarea x (cele până la care se poate ajunge
în minim x pași), trebuie să completăm lungimile minime pentru vecinii
lor (cu x + 1), apoi pentru vecinii vecinilor lor (cu x + 2), și tot așa.
Doar în acest fel putem fi siguri că toate rezultatele din mat sunt
corecte. Completarea matricei poate fi asemănată cu modul în care o
picătură de cerneală se extinde pe o bucată de hârtie.
Pentru a menține această ordine a completării matricei, se
folosește o coadă, căci această structură de date funcționează pe
principiul primul venit, primul servit. La fiecare pas, din coadă
extragem primul element pentru a-l prelucra (a completa lungimile
corespunzătoare vecinilor accesibili și a-i pune la sfârșitul cozii).
Întotdeauna, o parte dintre pozițiile din coadă (primele) vor avea o
anumită valoare k, iar restul (vecinii lor) vor avea valoarea cu o unitate
mai mare (k+1).

Găsirea unui drum optim

Acum că avem matricea mat completată, putem folosi soluțiile


subproblemelor pentru construirea unui drum de lungime minimă de la
șoarece (a) la brânză (b). Asta nu mai ține de Algoritmul lui Lee, dar n-
are sens să scriu un alt articol pentru această cerință.
Secretul este să pornim de la brânză și să ne îndreptăm spre
șoarece, nu invers, cum ar fi fost natural. Știm că dacă un vecin ngh (de
la neighbor – vecin în engleză) al celulei pos are valoarea mat[pos.lin]
[pos.col]−1, atunci din ngh sigur se poate ajunge în pos (într-un singur
pas). Cu alte cuvinte, sigur există un drum optim de la șoarece la pos
care trece prin ngh. Dacă porneam de la șoarece n-am fi avut niciodată
cum să știm dacă suntem pe drumul bun sau nu.
Folosind această idee, putem deduce un algoritm foarte simplu
ce rezolvă această cerință. Inițializăm o stivă st cu poziția unde se află
brânza. Căutăm un vecin al acestei celule, care respectă proprietatea de
mai sus, și îl adăugăm în stivă. Apoi, repetăm procedeul pentru el.
Facem asta până când ajungem la poziția șoarecelui. La final, extragem
și afișăm, pe rând, pozițiile din stivă.

4 din 8 Profesor A.D.


Din modul în care m-am exprimat este clar că problema se
poate rezolva și recursiv, mai simplu. Dacă dimensiunile matricei sunt
suficient de mici pentru a nu se produce stack overflow
(supraîncărcarea stivei).

Bordarea matricei

Bordarea matricei este o tehnică des folosită în problemele cu


matrice. Aceasta presupune să înconjurăm (să bordăm) matricea
propriu-zisă cu un anumit număr. Pentru ca bordarea să fie efectuată
corect, trebuie să avem grijă la declararea dimensiunilor maxime ale
matricei. Aceasta trebuie să fie declarată cu măcar două linii și două
coloane în plus. De asemenea, este necesar să poziționăm colțul din
stânga-sus al matricei în (1,1), pentru a lăsa loc bordurii să treacă prin
(0,0).
Putem folosi ideea asta și în problema noastră. Ca să verificăm
dacă poziția pos se află în interiorul matricei propriu-zise, ar trebui să
efectuăm de fiecare dată acest test enervant:

if (1<=pos.lin && pos.lin<=m && 1<=pos.col && pos.col<=n)

Pe lângă faptul că face codul mai complicat, nici nu este


eficient, pentru că s-ar executa de foarte multe ori. Însă, putem borda
inițial matricea cu valoarea −1, ca și cum ar fi înconjurată de
obstacole. Astfel, nu vom ieși niciodată din matrice, pentru că asta ar
însemna să ne expandăm în celule inaccesibile.

Iată implementarea scurtă și simplă a bordării matricei mat:

// Vertical:
for (int i = 0; i <= m + 1; i++)
mat[i][0] = mat[i][n + 1] = -1;

// Orizontal:
for (int j = 1; j <= n; j++)
mat[0][j] = mat[m + 1][j] = -1;

5 din 8 Profesor A.D.


Vectorii de deplasare

Ar fi aiurea să parcurgem vecinii unei poziții ca mai jos, pentru că am


scrie de patru ori aceleași operații:

prelucrare mat[pos.lin - 1][pos.col]


prelucrare mat[pos.lin][pos.col + 1]
prelucrare mat[pos.lin + 1][pos.col]
prelucrare mat[pos.lin][pos.col – 1]

Ar fi frumos să putem parcurge vecinii cu un for. Ei bine, putem face


asta dacă declarăm mai întâi vectorii de deplasare:

const int addLin[] = {-1, 0, 1, 0};


const int addCol[] = { 0, 1, 0, -1};

Acești vectori conțin pe poziția i niște valori care, adunate la


coordonatele celulei curente, conduc la obținerea coordonatelor celui
de-al i-lea vecin al ei:

6 din 8 Profesor A.D.


Nu avem decât să ne folosim de acești vectori pentru a inițializa o
variabilă ngh, ce reține coordonatele vecinului curent, pe care să o
folosim în continuare.

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


Pos ngh;
ngh.lin = pos.lin + addLin[k];
ngh.col = pos.col + addCol[k];
// ...
}

Iată cum arată coordonatele vecinilor lui pos în cazul în care


deplasarea se făcea cu o unitate pe 8 direcții:

7 din 8 Profesor A.D.


Iată cum arată coordonatele vecinilor lui pos în cazul în care
deplasarea se făcea similar cu cea a calului de pe o tablă de șah:

Aplicații ale algoritmului Lee

• Probleme de rutare detaliată;


• Probleme de rutare globală.

8 din 8 Profesor A.D.

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