Sunteți pe pagina 1din 9

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

Aplicabilitatea algoritmilor de tip Greedy

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 BA 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 BA 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.

Modul de lucru al algoritmilor de tip Greedy

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

Figura 1: Reţea de oraşe pentru problema comis-voiajorului

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>

/* Numarul maxim de orase. */


#define N_MAX 30

/* Constanta care se foloseste ca valoare


de initializare la cautarea minimului. */
#define MINIM 10000
/* Numarul de orase. */
int n;

/* Matricea distantelor dintre orase. */


int d[N_MAX][N_MAX];

/* Drumul comis voiajorului. Contine


indicii oraselor in ordinea in care
sunt ele parcurse. */
int drum[N_MAX];

/* Vector care memoreaza care orase au


fost vizitate. vizitat[k] va fi 1 daca
orasul k a fost vizitat, 0 altfel. */
int vizitat[N_MAX];

/* Functie care alege urmatorul element care


sa fie prelucrat din multimea oraselor.
Primeste ca parametru ultimul oras care
a fost vizitat, si returneaza urmatorul
oras care sa fie vizitat precum si lungimea
drumului catre acesta. */
void alege(int ultimul, int *min, int *j_min)
{
int j;

/* Cautam drumul minim de la ultimul


oras pana la orasele neparcurse inca. */
*min = MINIM;
*j_min = -1;
for (j=0; j<n; j++)
if (!vizitat[j])
{
if (d[ultimul][j] < *min)
{
*min = d[ultimul][j];
*j_min = j;
}
}
}

int main(void)
{
FILE *fin;
int i, j;
int count, cost, min, j_min;

/* Deschidem fisierul pentru citire in mod text. */


fin = fopen("comis.in", "rt");
if (!fin)
{
printf("Eroare: nu pot deschide fisierul.\n");
return -1;
}

/* Citim datele din fisier. */


fscanf(fin, "%d", &n);
for (i=0; i<n; i++)
for (j=0; j<n; j++)
fscanf(fin, "%d", &(d[i][j]));

/* Afisam pe ecran datele preluate din fisier. */


printf("Avem %d orase.\n", n);
printf("Distantele dintre orase sunt:\n");
for (i=0; i<n; i++)
{
for (j=0; j<n; j++)
printf("%d ", d[i][j]);
printf("\n");
}
printf("\n");

/* Initial nici un oras nu este vizitat. */


for (i=0; i<n; i++)
vizitat[i] = 0;

/* Primul oras vizitat este cel cu numarul "0".


Costul total este zero deocamdata. */
drum[0] = 0;
vizitat[0] = 1;
count = 1;
cost = 0;

/* Parcurgem restul de n-1 orase. */


for (i=0; i<n-1; i++)
{
/* Alegem urmatorul oras care sa fie vizitat. */
alege(drum[count-1], &min, &j_min);

/* Parcurgem drumul minim gasit si vizitam


un nou oras. */
printf("Am ales drumul (%d, %d) de cost %d.\n",
drum[count-1], j_min, min);
drum[count] = j_min;
vizitat[j_min] = 1;
count++;
cost += min;
}

/* Parcurgem drumul de la ultimul oras vizitat


catre primul oras si actualizam costul
total. */
cost += d[drum[n-1]][0];

/* Afisam drumul parcurs. */


printf("\nDrumul are costul %d si este:\n", cost);
for (i=0; i<n; i++)
printf("%d ", drum[i]);
printf("0\n");
return 0;
}

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

Conectarea oraşelor cu cost minim

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.

Figura 2: Costuri de conectare a oraşelor pentru problema


conectării oraşelor cu cost minim

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.

Implementare Matricea costurilor, C, se reţine într-un tablou bidimensional. Pentru perechile de


oraşe între care nu se poate face legătură se va trece în matricea costurilor valoarea 0. În termeni
Greedy, mulţimea noastră iniţială de elemente A este muţimea tuturor perechilor de oraşe între care
se poate stabili legătură directă. Adică A={(i, j) | c i,j>0}. Pentru graful din figura 1, mulţimea A va
conţine elementele {(0,1), (0,2), (1,2), (1,3), (1,5), (2,3), (4,5), (4,6), (5,6)}.
Submulţimea B pe care o căutăm va conţine o parte din perechile aflate în mulţimea A. Se
poate demonstra că soluţia optimă B* conţine n-1 perechi atunci cănd numărul de oraşe este n
(presupunem că graful este conex, adică se poate construi o reţea care să conecteze toate oraşele).
Pentru construirea mulţimii B, vom selecta oraşele rând pe rând pentru a le adăuga la reţea.
Vom spune că un oraş este selectat atunci când el a fost conectat la reţeaua de oraşe printr-o muchie
care face parte din mulţimea B.
În implementare nu vom lucra cu mulţimea A sub forma explicită de perechi, ci vom folosi
matricea costurilor C. Vom eticheta liniile şi coloanele matricei costurilor după cum urmează.
Atunci cănd un oraş oi este selectat, linia i din matrice se marchează, iar coloana i din matrice se
şterge.
Pentru a alege următorul element din mulţimea A care să fie prelucrat, căutăm cel mai mic
cost din matricea costurilor din liniile marcate şi coloanele care nu sunt şterse. Să zicem că cel mai
mic cost a fost găsit ca fiind elementul ci_min,j_min. Atunci următorul element din mulţimea A care va fi
prelucrat este perechea (i_min,j_min).
Ştergerea coloanelor din matricea costurilor nu va însemna o ştergere fizică, ci doar una
logică. Vom folosi doi vectori prin care vom memora care linii sunt marcate şi care coloane sunt
şterse.
O schema de cod sursă pentru rezolvarea acestei probleme arată astfel:

...

/* Numarul maxim de noduri din graf. */


#define N_MAX 30

/* Numarul de orase. */
int n;

/* Matricea costurilor de conectare a oraselor. */


int c[N_MAX][N_MAX];

/* Vector care indica liniile marcate din matricea


costurilor. marcat[k] va fi 1 pentru liniile
marcate si 0 pentru liniile nemarcate. */
int marcat[N_MAX];

/* Vector care indica coloanele sterse din matricea


costurilor. sters[k] va fi 1 pentru coloanele
sterse si 0 pentru coloanele nesterse. */
int sters[N_MAX];

/* Functie care alege urmatorul element care sa


fie prelucrat din multimea A, adica o pereche
de orase intre care sa se construiasca drum.
Se parcurg liniile marcate si coloanele
nesterse din matricea costurilor si se
alege costul minim.
Se returneaza costul minim gasit, si linia si
coloana unde apare el. */
void alege(int* min, int *i_min, int* j_min)
{
...
}

int main(void)
{
...

/* Aici memoram muchiile alese pentru a face parte din arbore.


O muchie este memorata de perechea (arbore[i][0], arbore[i][1]). */
int arbore[N_MAX][2];

/* Numarul de muchii introduse in arbore. */


int count = 0;

...

/* Citim din fisier numarul de noduri din graf


si matricea costurilor. */
...

/* Initial nici un nod nu este nici marcat nici


sters. Pentru asta initializam toate elementele
vectorilor “marcat” si “sters” cu zero. */
...

/* Pornim de la nodul "0", motiv pentru care


marcam linia 0 si stergem coloana 0. */
marcat[0] = 1;
sters[0] = 1;

...

/* Cat timp mai avem noduri neparcurse. */


while (!gata)
{
/* Alege urmatoarea pereche de noduri care sa fie
prelucrata. Nodul “i_min” va fi un nod deja parcurs,
iar nodul “j_min” va fi un nod inca neparcurs. */
alege(&min, &i_min, &j_min);

...

/* Marcam linia noului nod si stergem


coloana lui. */
marcat[j_min] = 1;
sters[j_min] = 1;

/* Adaugam muchia la arbore. */


arbore[count][0] = i_min;
arbore[count][1] = j_min;
count++;

...
}

/* Afisam arborele partial minim pe care l-am gasit. */


...

return 0;
}

Fisierul cu date de intrare pentru graful din figura 2 este următorul:


7
0 20 10 0 0 0 0
20 0 9 4 0 3 0
10 9 0 10 0 0 0
0 4 10 0 0 0 0
0 0 0 0 0 2 2
0 3 0 0 2 0 3
0 0 0 0 2 3 0

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