Documente Academic
Documente Profesional
Documente Cultură
08 Greedy
08 Greedy
Consideraţii teoretice
Explicarea numelui
În limba engleză cuvântul greedy înseamnă lacom. Algoritmii de tip greedy sunt algoritmi lacomi.
Ei vor să construiască într-un mod cât mai rapid soluţia problemei, fără a sta mult pe gânduri.
Algoritmii de tip greedy se caracterizează prin luarea unor decizii rapide care duc la găsirea
unei soluţii a problemei. Nu întotdeauna asemenea decizii rapide duc la o soluţie optimă, dar vom
vedea că există anumite tipuri de probleme unde se pot obţine soluţii optime sau foarte apropiate de
optim.
Algoritmii de tip Greedy se aplică la acele probleme unde datele de intrare sunt organizate sub
forma unei mulţimi A şi se cere găsirea unei submulţimi BA care să îndeplinească anumite condiţii
astfel încât să fie acceptată ca soluţie posibilă.
În general pot să existe mai multe submulţimi BA care să reprezinte soluţii posibile ale
problemei. Dintre toate aceste submulţimi B se pot selecta, conform unui anumit criteriu, anumite
submulţimi B* care reprezintă soluţii optime ale problemei. Scopul este de a găsi, dacă este posibil,
una din mulţimile B*. Dacă acest lucru nu este posibil, atunci scopul este găsirea unei mulţimi B
care să fie cât mai aproape de mulţimile B*, conform criteriului de optimalitate impus.
Construirea mulţimii B se face printr-un şir de decizii. Iniţial se porneşte cu mulţimea vidă (B = Ø).
Fiecare decizie constă în alegerea unui element din mulţimea A, analiza lui şi eventual introducerea
lui în mulţimea B. În funcţie de modul în care se iau aceste decizii, mulţimea B se va apropia mai
mult sau mai puţin de soluţia optimă B*. În cazul ideal vom avea B = B*.
Algoritmii de tip greedy nu urmăresc să determine toate soluţiile posibile şi să aleagă dintre
ele, conform criteriului de optimalitate impus, soluţiile optime. După cum spune şi numele,
algoritmii de tip greedy sunt caracterizaţi prin lăcomie şi nu au răbdarea să investigheze toate
variantele posibile de alegere a soluţiei. Ei încep construirea unei soluţii pornind de la mulţimea
vidă, apoi lucrează în paşi, într-un mod cât se poate de hotărât: la fiecare pas se ia câte o decizie şi
se extinde soluţia cu câte un element.
La fiecare pas se analizează câte un element din mulţimea A şi se decide dacă să fie sau nu
inclus în mulţimea B care se construieşte. Astfel se progresează de la Ø cu un sir de mulţimi
intermediare (Ø, B0, B1, B2, ...), până când se obţine o soluţie finală B.
Implementare
Ca şi schemă generală de lucru, există două variante de implementare a algoritmilor de tip Greedy.
Prima variantă foloseşte două funcţii caracteristice: alege şi posibil. alege este o funcţie care
are rolul de a selecta următorul element din mulţimea A care să fie prelucrat. Funcţia posibil verifică
dacă un element poate fi adăugat soluţiei intermediare Bi astfel încât noua soluţie Bi+1 care s-ar
obţine să fie o soluţie validă. Prezentăm în continuare pseudocodul pentru această primă variantă
greedy. Se consideră că numărul de elemente al mulţimii A este n.
B = multimea vida
for (i=0; i<n; i++)
{
x = alege(A)
if (posibil(B,x))
* adauga elementul x la multimea B
}
Dificultatea la această primă variantă constă în scrierea funcţiei alege. Dacă funcţia alege este bine
concepută, atunci putem fi siguri că soluţia B găsită este o soluţie optimă. Dacă funcţia alege nu este
foarte bine concepută, atunci soluţia B găsită va fi doar o soluţie posibilă şi nu va fi optimă. Ea se
poate apropia însă mai mult sau mai puţin de soluţia optimă B*, în funcţie de criteriul de selecţie
implementat.
A doua variantă de implementare diferă de prima prin faptul că face o etapă iniţială de
prelucrare a mulţimii A. Practic se face o sortare a elementelor mulţimii A, conform unui anumit
criteriu. După sortare, elementele vor fi prelucrate direct în ordirea rezultată. Prezentăm în
continuare pseudocodul pentru această a doua variantă greedy.
B = multimea vida
prelucreaza(A)
for (i=0; i<n; i++)
{
x = A[i]
if (posibil(B,x))
* adauga elementul x la multimea B
}
La a doua variantă, dificultatea funcţiei alege nu a dispărut, ci s-a transferat funcţiei prelucreaza.
Dacă prelucrarea mulţimii A este bine făcută, atunci se va ajunge în mod sigur la o soluţie optimă.
Altfel se va obţine doar o soluţie posibilă, mai mult sau mai puţin apropiată de optim.
Exemple
Problema comis-voiajorului
Enunţ Se condideră n oraşe. Se cunosc distanţele dintre oricare două oraşe. Un comis-voiajor
trebuie să treacă prin toate cele n oraşe. Se cere să se determine un drum care porneşte dintr-un oraş,
trece exact o dată prin fiecare din celelalte oraşe şi apoi revine la primul oraş, astfel încât lungimea
drumului să fie minimă.
Rezolvare Pentru găsirea unei soluţii optime la această problemă este nevoie de algoritmi cu timp
de rulare foarte mare (de ordin exponenţial O(2n)). În situaţiile practice asemenea algoritmi cu timp
foarte mare de rulare nu sunt acceptabili. Ca urmare se face un compromis şi se acceptă algoritmi
care nu găsesc soluţia optimă ci doar o soluţie aproape de optim, dar au în schimb un timp de rulare
mic. Propunem în continuare o soluţie greedy la această problemă. Ideea este următoarea. Se
porneşte dintr-un oraş oarecare. Se caută drumul cel mai scurt care pleacă din oraşul respectiv către
oraşe nevizitate încă. Se parcurge acel drum şi se ajunge într-un alt oraş. Aici din nou se caută cel
mai scurt drum către oraşele nevizitate încă. Se parcurge şi acest drum, ajungându-se într-un nou
oraş. Repetând aceşti paşi se parcurg toate oraşele. La final se parcurge drumul care duce înapoi
spre primul oraş.
Să considerăm exemplul din figura 1. Avem 4 oraşe cu distanţele reprezentate în figură.
Pornim vizitarea oraşelor din oraşul 0. De aici alegem drumul cel mai scurt către oraşele nevizitate,
şi anume (0,2) de lungime 2. Ajunşi în oraşul 2, alegem din nou drumul cel mai scurt spre oraşele
nevizitate, şi anume (2,3) de lungime 1. Din oraşul 3 mai avem doar un singur oraş nevizitat, 1, aşa
că alegem drumul spre el (3,1) de lungime 1. În acest moment am parcurs toate oraşele şi ne
reîntoarcem în oraşul 0 pe drumul (1,0) de lungime 4. Drumul rezultat este 0, 2, 3, 1, 0, iar distanţa
totală de parcurs este 2 + 1 + 1 + 4 = 8.
Implementare Distanţele între oraşe le memorăm într-un tablou bidimensional D. Distanţa între
oraşele (i,j) va fi memorată în elementul di,j al matricii. În termeni Greedy, mulţimea iniţială A este
mulţimea tuturor perechilor de oraşe. Pentru reţeaua de oraşe din figura 2 mulţimea A conţine
elementele {(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)}. Mulţimea B care trebuie găsită va conţine o parte
din aceste perechi de oraşe, şi anume acele perechi care înlănţuite să formeze un drum ce trece prin
toate oraşele. Dacă avem un număr de n oraşe, atunci mulţimea B va conţine n perechi de oraşe.
În implementare nu vom lucra cu mulţimea A sub această formă explicită de perechi de
oraşe, ci vom folosi matricea distanţelor D. De asemenea drumul comis-voiajorului nu îl vom păstra
sub formă de perechi de oraşe, ci sub forma unui sir al oraşelor.
Pentru a memora drumul parcurs de comis-voiajor, folosim un tablou unidimensional drum.
În acest tablou vom memora indicii oraşelor parcuse, în ordinea parcurgerii.
Pentru a şti care oraşe au fost parcurse, facem o marcare logică a oraşelor folosind un tablou
unidimensional vizitat. Elementele din acest tablou care au valoarea 1 reprezintă oraşe vizitate.
Cod sursă În continuare este prezentat codul sursă în limbajul C care implementează algoritmul
descris mai sus.
#include <stdio.h>
int main(void)
{
FILE *fin;
int i, j;
int count, cost, min, j_min;
Fişierul cu date de intrare pentru reţeaua de oraşe din figura 2 este următorul:
4
0 4 2 7
4 0 2 1
2 2 0 1
7 1 1 0
Îmbunătăţiri Algoritmul greedy prezentat se poate îmbunătăţi pentru a furniza soluţii mai aproape
de soluţia optimă. O variantă de îmbunătăţire este să nu se pornească doar din primul oraş la
parcurgerea drumului. Se poate relua calculul având ca punct de pornire fiecare oraş pe rând şi se
poate memora minimul global astfel obţinut.
Probleme propuse
Enunţ Se consideră n oraşe. Pentru diferite perechi de oraşe (i, j), 0<i<n, 0<j<n se cunoaşte costul
conectării lor directe ci,j. Nu toate perechile de oraşe pot fi conectate; pentru perechile care nu pot fi
conectate nu se precizează costul. Se cere să se construiască o reţea prin care oricare două oraşe să
fie conectate între ele direct sau indirect şi costul total al conectării să fie minim.
Rezolvare Se poate arăta că reţeaua de conectare cerută este un arbore. Problema mai este
cunoscută şi ca problema determinării arborelui parţial de cost minim într-un graf. Pentru această
problemă există un algoritm greedy de rezolvare numit algoritmul lui Prim. În literatura de
specialitate există argumentarea matematică a faptului că acest algoritm găseşte întotdeauna soluţia
optimă de conectare a oraşelor.
Se construieşte arborele parţial minim în manieră greedy, adăugând câte un nod la fiecare
pas. La început de tot arborele parţial este vid, nu conţine nici un nod. Primul pas constă în
adăugarea unui nod arbitrar în arbore. Pe urmă, la fiecare pas se caută muchia de cost minim care
porneşte dintr-un nod deja adăugat la arbore şi ajunge într-un nod care nu este în arbore. Se adaugă
în arbore nodul în care sfârşeşte muchia găsită.
Să considerăm spre exemplu o reţea de 7 oraşe numerotate de la 0 la 6. Costurile de
conectare a oraşelor sunt redate în figura 2.
Arborele minim este redat cu linii îngroşate. El a fost construit pas cu pas, conform procedeului
descris mai sus. Iniţial arborele a fost vid. La primul pas s-a adăugat un nod arbitrar, şi anume nodul
0.
Pe urmă s-a ales muchia de cost minim care pleacă din nodul 0 către celelalte noduri.
Muchia de cost minim a fost (0,2) de cost 10. Nodul 2 a fost adăugat în arbore.
La următorul pas s-a ales muchia de cost minim care pleacă din nodurile 0 sau 2 către
celelalte noduri. Muchia aleasă a fost (2,1) de cost 9. Nodul 1 a fost adăugat în arbore.
La următorul pas s-a ales muchia de cost minim care pleacă din nodurile 0, 2 sau 1 către
nodurile încă neintroduse în arbore. Muchia aleasă a fost (1,5) de cost 3. Nodul 5 a fost adăugat în
arbore.
Următoarea muchie aleasă a fost (5,4) de cost 2. Nodul 4 a fost adăugat în arbore. Apoi a
fost aleasă muchia (4,6) de cost 2 şi nodul 6 a fost adăugat şi el în arbore.
Pe urmă a fost aleasă muchia (1,3) de cost 4 şi nodul 3 a fost introdus în arbore. În acest
moment algoritmul s-a încheiat deoarece toate oraşele au fost conectate la reţea. Costul total al
conectării a fost 10 + 9 + 3 + 2 + 2 + 4 = 30.
...
/* Numarul de orase. */
int n;
int main(void)
{
...
...
...
...
...
}
return 0;
}