Sunteți pe pagina 1din 74

ALGORITMI STRUCTURI DE DATE

1
CLASE DE ALGORITMI

Căutarea şi Sortarea sunt două dintre cele mai des întâlnite subprobleme în programare. Ele
constituie o parte esenţială din numeroasele procese de prelucrare a datelor. Operaţiile de căutare şi
sortare sunt executate frecvent de către oameni în viaţa de zi cu zi, ca de exemplu căutarea unui cuvânt în
dicţionar sau căutarea unui număr în cartea de telefon.
Căutarea este mult simplificată dacă datele în care efectuăm această operaţie sunt sortate
(ordonate, aranjate) într-o anumită ordine (cuvintele în ordine alfabetică, numerele în ordine crescătoare
sau descrescătoare).
Sortarea datelor constă în rearanjarea colecţiei de date astfel încât un câmp al elementelor
colecţiei să respecte o anumită ordine. De exemplu în cartea de telefon fiecare element (abonat) are un
câmp de nume, unul de adresă şi unul pentru numărul de telefon. Colecţia aceasta respectă ordinea
alfabetică după câmpul de nume.
Dacă datele pe care dorim să le ordonăm, adică să le sortăm, sunt în memoria internă, atunci
procesul de rearanjare a colecţiei îl vom numi sortare internă, iar dacă datele se află într-un fişier
(colecţie de date de acelaşi fel aflate pe suport extern), atunci procesul îl vom numi sortare externă.
Fiecare element al colecţiei de date se numeşte articol iar acesta la rândul său este compus din
unul sau mai multe componente. O cheie C este asociată fiecărui articol şi este de obicei unul dintre
componente. Spunem că o colecţie de n articole este ordonat crescător după cheia C dacă C(i)  C(j)
pentru 1i<jn, iar dacă C(i)  C(j) atunci şirul este ordonat descrescător.

Algoritmi de căutare

În acest subcapitol vom studia câteva tehnici elementare de căutare şi vom presupune că datele se
află în memoria internă, într-un şir de articole. Vom căuta un articol după un câmp al acestuia pe care îl
vom considera cheie de căutare. În urma procesului de căutare va rezulta poziţia elementului căutat (dacă
acesta există).
Notând cu k1, k2, ...., kn cheile corespunzătoare articolelor şi cu a cheia pe care o căutăm, problema
revine la a găsi (dacă există) poziţia p cu proprietatea a = kp.
De obicei articolele sunt păstrate în ordinea crescătoare a cheilor, deci vom presupune că
k1 < k2 < .... < kn .
Uneori este util să aflăm nu numai dacă există un articol cu cheia dorită ci şi să găsim în caz contrar locul
în care ar trebui inserat un nou articol având cheia specificată, astfel încât să se păstreze ordinea existentă.
Deci problema căutării are următoarea specificare:
Date a,n,(ki, i=1,n);
Precondiţia: nN, n1 şi k1 < k2 < .... < kn ;
Rezultate p;
Postcondiţia: (p=1 şi a  k1) sau (p=n+1 şi a > kn) sau (1<pn) şi (kp-1 < a  kp).

Pentru rezolvarea acestei probleme vom descrie mai mulţi SUBPROGRAMi.


O primă metodă este căutarea secvenţială, în care sunt examinate succesiv toate cheile.

SUBPROGRAMul CautSecv(a,n,K,p) este: {nN, n1 şi}


{k1 < k2 < .... < kn}
{Se caută p astfel ca:}
{(p=1 şi a  k1) sau (p=n+1 şi a>kn)}
{sau (1<pn) şi (kp-1 < a  kp)}
2
Fie p:=0; {Cazul "încă negasit"}
Dacă ak1 atunci p:=1 altfel
Dacă a>kn atunci p:=n+1 altfel
Pentru i:=2; n execută
Dacă (p=0) şi (aki) atunci p:=i sfdacă
sfpentru
sfdacă
sfdacă
sf-CautSecv

Se observă că prin această metodă se vor executa în cel mai nefavorabil caz n-1 comparări,
întrucât contorul i va lua toate valorile de la 2 la n. Cele n chei împart axa reală în n+1 intervale. Tot
atâtea comparări se vor efectua în n-1 din cele n+1 intervale în care se poate afla cheia căutată, deci
complexitatea medie are acelaşi ordin de mărime ca şi complexitatea în cel mai rău caz.
Evident că în multe situaţii acest algoritm face calcule inutile. Atunci când a fost deja găsită cheia
dorită este inutil a parcurge ciclul pentru celelalte valori ale lui i. Cu alte cuvinte este posibil să înlocuim
ciclul PENTRU cu un ciclu CÂTTIMP. Ajungem la un al doilea algoritm, dat în continuare.
SUBPROGRAMul CautSucc(a,n,K,p) este: {nN, n1 şi}
{k1 < k2 < .... < kn}
{Se caută p astfel ca:}
{(p=1 şi a  k1) sau (p=n+1 şi a>kn)}
{sau (1<pn) şi (kp-1 < a  kp).
Fie p:=1;
Dacă a>k1 atunci
Câttimp pn şi a>kp executş p:=p+1 sfcât
sfdacă
sf-CautSecv

O altă metodă, numită căutare binară, care este mult mai eficientă, utilizează tehnica "divide et
impera" privitor la date. Se determină în ce relaţie se află cheia articolului aflat în mijlocul colecţiei cu
cheia de căutare. În urma acestei verificări căutarea se continuă doar într-o jumătate a colecţiei. În acest
mod, prin înjumătăţiri succesive se micşorează volumul colecţiei rămase pentru căutare. Căutarea binară
se poate realiza practic prin apelul funcţiei BinarySearch(a,n,K,1,n), descrisă mai jos, folosită în
SUBPROGRAMul dat în continuare.
SUBPROGRAMul CautBin(a,n,K,p) este: {nN, n1 şi k1 < k2 < .... < kn}
{Se caută p astfel ca: (p=1 şi a  k1) sau}
{(p=n+1 şi a>kn) sau (1<pn) şi (kp-1 < a  kp)}
Dacă ak1 atunci p:=1 altfel
Dacă a>kn atunci p:=n+1 altfel
p:=BinarySearch(a,n,K,1,n)
sfdacă
sfdacă
sf-CautBin
Funcţia BinarySearch (a,n,K,St,Dr) este:
Dacă StDr-1
atunci BinarySearch:=Dr
altfel m:=(St+Dr) Div 2;
Dacă aK[m]
atunci BinarySearch:=BinarySearch(a,n,K,St,m)
altfel BinarySearch:=BinarySearch(a,n,K,m,Dr)
3
sfdacă
sfdacă
sf-BinarySearch
În funcţia BinarySearch descrisă mai sus, variabilele St şi Dr reprezintă capetele intervalului de
căutare, iar m reprezintă mijlocul acestui interval.
Se observă că funcţia BinarySearch se apelează recursiv. Se poate înlătura uşor recursivitatea, aşa
cum se poate vedea în următoarea funcţie:
Funcţia BinSeaNerec (a,n,K,St,Dr) este:
Câttimp Dr-St>1 execută
m:=(St+Dr) Div 2;
Dacă aK[m]
atunci Dr:=m
altfel St:=m
sfdacă
sfcât
BinSeaNerec:=Dr
sf-BinSeaNerec

Sortare internă

Prin sortare internă vom înţelege o rearanjare a unei colecţii aflate în memoria internă astfel
încât cheile articolelor să fie ordonate crescător (eventual descrescător).
Din punct de vedere al complexităţii algoritmilor problema revine la ordonarea cheilor. Deci
specificarea problemei de sortare internă este următoarea:
Date n,K; {K=(k1,k2,...,kn)}
Precondiţia: kiR, i=1,n
Rezultate K';
Postcondiţia: K' este o permutare a lui K, dar ordonată crescător.
Deci k1  k2  ...  kn.
O primă tehnică numită "Selecţie" se bazează pe următoarea idee: se determină poziţia
elementului cu cheie de valoare minimă (respectiv maximă), după care acesta se va interschimba cu
primul element. Acest procedeu se repetă pentru subcolecţia rămasă, până când mai rămâne doar
elementul maxim.
SUBPROGRAMul Selectie(n,K) este: {Se face o permutare a celor}
{n componente ale vectorului K astfel}
{ca k1  k2  ....  kn }
Pentru i:=1; n-1 execută
Fie ind:=i;
Pentru j:=i+1; n execută
Dacă kj < kind atunci ind:=j sfdacă
sfpentru
Dacă i<ind atunci t:=ki; ki:=kind; kind:=t sfdacă
sfpentru
sf-Selectie

Se observă că numărul de comparări este:


(n-1)+(n-2)+...+2+1=n(n-1)/2
indiferent de natura datelor.
A treia metodă care va fi prezentată, numită "BubbleSort", compară două câte două elemente
consecutive iar în cazul în care acestea nu se află în relaţia dorită, ele vor fi interschimbate. Procesul de

4
comparare se va încheia în momentul în care toate perechile de elemente consecutive sunt în relaţia de
ordine dorită.
SUBPROGRAMul BubbleSort (n,K) este:
Repetă
Fie kod:=0; {Ipoteza "este ordine"}
Pentru i:=2; n execută
Dacă ki-1 > ki atunci
t := ki-1;
ki-1 := ki;
ki:=t;
kod:=1 {N-a fost ordine!}
sfdacă
sfpentru
pânăcând kod=0 sfrep {Ordonare}
sf-BubbleSort

O metodă mai performantă de ordonare, care va fi prezentată în continuare, se numeşte


"QuickSort" şi se bazează pe tehnica "divide et impera" după cum se poate observa în continuare. Metoda
este prezentată sub forma unei proceduri care realizează ordonarea unui subşir precizat prin limita
inferioară şi limita superioară a indicilor acestuia. Apelul procedurii pentru ordonarea întregului şir este :
QuickSort(n,K,1,n), unde n reprezintă numărul de articole ale colecţiei date.
SUBPROGRAMul SortareRapidă (n,K) este:
Cheamă QuickSort(n,K,1,n)
sf-SortareRapidă

Procedura QuickSort (n,K,St,Dr) va realiza ordonarea subşirului kSt,kSt+1,..., kDr. Acest subşir va fi
rearanjat astfel încât kSt să ocupe poziţia lui finală (când şirul este ordonat). Dacă i este această poziţie,
şirul va fi rearanjat astfel încât următoarea condiţie să fie îndeplinită:
kj  ki  kl , pentru st  j < i < l dr (*)
Odată realizat acest lucru, în continuare va trebui doar să ordonăm subşirul kSt , kSt+1 , ... ,ki-1 prin
apelul recursiv al procedurii QuickSort(n,K,St,i-1) şi apoi subşirul ki+1,..., kDr prin apelul
QuickSort(i+1,Dr). Desigur ordonarea acestor două subşiruri (prin apelul recursiv al procedurii) mai este
necesară doar dacă acestea conţin cel puţin două elemente.

Procedura QuickSort este prezentată în continuare :


SUBPROGRAMul QuickSort (n,K,St,Dr) este:
Fie i:=St; j:=Dr; a:=ki;
Repetă
Câttimp kj >= a şi (i<j) execută j:=j-1 sfcât
ki:= kj;
Câttimp ki  a şi (i<j) execută i:=i+1 sfcât
kj:= ki ;
pânăcând i=j sfrep
Fie ki := a;
Dacă St < i-1 atunci Cheamă QuickSort(n,K,St,i-1) sfdacă
Dacă i+1 < Dr atunci Cheamă QuickSort(n,K,i+1,Dr) sfdacă
sf-QuickSort

Un ultim algoritm care va fi prezentat se numeşte "Merge Sort" (sortare prin interclasare) şi se
bazează pe tehnica "divide et impera". Şirul ce urmează a fi ordonat se împarte în două subşiruri care se
ordonează, după care acestea se vor interclasa obţinându-se întregul şir ordonat. Fiecare subşir se va
5
ordona tot prin despărţirea lui în două subşiruri urmată de interclasare şi aşa mai departe până când
ordonarea unui subşir se poate rezolva elementar fără a mai fi necesară despărţirea lui în alte două
subşiruri (lungimea subşirului este cel mult 2).
Algoritmul corespunzător este prezentat în secţiunea următoare sub forma unei proceduri
recursive care ordonează un subşir precizând limitele acestuia.

Interclasare

Fiind date două colecţii de date, ordonate crescător (sau descrescător) după o cheie, se cere să se
obţină o colecţie care să fie de asemenea ordonată crescător (respectiv descrescător) după aceeaşi cheie şi
care să fie formată din articolele colecţiilor date. Acest lucru se poate obţine direct (fără o sortare a
colecţiei finale) prin parcurgerea secvenţială a celor două colecţii, simultan cu generarea colecţiei cerute.
Prin compararea a două elemente din listele de intrare se va decide care element va fi adăugat în lista de
ieşire.
Deci ne interesează un algoritm de rezolvare a problemei ce are următoarea specificare:
Date m, (xi, i=1,m), n, (yi, i=1,n);
Precondiţia: {x1  x2  ...  xm} şi {y1  y2  ...  yn}
Rezultate k, (zi, i=1,k);
Postcondiţia: {k=m+n} şi {z1 z2 ... zk} şi (z1,z2,..., zk) este o permutare a valorilor (x1,
..., xm,y1,..., yn)
O soluţie posibilă ar fi depunerea componentelor vectorului X şi a componentelor vectorului Y în
vectorul Z, realizând astfel a doua parte din postcondiţie. Ordonând apoi componentele vectorului Z
obţinem soluţia dorită. Acest algoritm, deşi corect, este ineficient şi, în plus, nu este util în sortările
externe (vezi secţiunea 5.4). Este important ca la o singură trecere prin vectorii X şi Y să se obţină
vectorul Z. Acest lucru este realizat de următorul algoritm de interclasare:
SUBPROGRAMul Interclasare(m,X,n,Y,k,Z) este: {X are cele m}
{componente ordonate nedescrescător}
{La fel Y cu n componente. Cele m+n valori}
{se depun în Z, tot ordonate nedescrescător}
Fie i:=1; j:=1; k:=0;
Câttimp (i<=m) şi (j<=n) execută {Există componente}
Dacă xiyj
atunci Cheamă PUNE(i,xi,k,Z) {şi în X}
altfel Cheamă PUNE(j,yj,k,Z) {şi în Y}
sfdacă
sfcât
Câttimp (i<=m) execută {Există componente}
Cheamă PUNE(i,xi,k,Z) {numai în X}
sfcât
Câttimp (j<=n) execută {Există componente}
Cheamă PUNE(j,yj,k,Z) {numai în Y}
sfcât
sf-Interclasare

Aici s-a folosit SUBPROGRAMul PUNE(ind,val,k,Z) care pune în vectorul Z valoarea val şi
măreşte indicele ind cu 1, subalgortim dat în continuare.

SUBPROGRAMul PUNE(ind,val,k,Z) este: {Adaugă val}


k:=k+1; {în vectorul Z cu}
zk:=val; {k componente şi}
ind:=ind+1 {măreşte ind cu 1}
6
sf-PUNE

Algoritmul MergeSort de sortare bazat pe interclasare se poate vedea în continuare.


Algoritmul MergeSort este: {Sortare prin interclasare}
Citeşte n;
Pentru i:=1 ; n execută Citeşte Ki sfpentru
Cheamă SortInter (n,K);
Pentru i:=1; n execută Tipăreşte Ki sfpentru
sf-MergeSort

SUBPROGRAMul SortInter(n, C) este:


Cheamă Ordon (1,n,C);
sf-SortInter

SUBPROGRAMul Ordon (St,Dr,A) este: {Sortare prin interclasare a}


{elementelor ASt,ASt+1,...,ADr}
Dacă St < Dr atunci
Fie m:=(St+Dr) Div 2;
Cheamă Ordon (St,m,A);
Cheamă Ordon (m+1,Dr,A);
Cheamă Inter (St,m, m+1,Dr);
sfdacă
sf-Ordon

SUBPROGRAMul Inter (s1,d1, s2,d2) este: { Interclasare }


Fie A:=C; k:=s1-1;
Câttimp (s1<=d1) şi (s2<=d2) execută
Dacă (C[s1]<C[s2])
atunci Cheamă PUNE(s1,cs1 ,k,A)
altfel Cheamă PUNE(s2,cs2 ,k,A)
sfdacă
sfcât
Câttimp (s1<=d1) execută Cheamă PUNE(s1,cs1 ,k,A) sfcât
Câttimp (s2<=d2) execută Cheamă PUNE(s2,cs2 ,k,A) sfcât
C:=A
sf-Inter

Sortare externă

O problemă cu care ne confruntăm adesea este sortarea unei colecţii de date aflate pe un suport
extern, de volum relativ mare faţă de memoria internă disponibilă. În această secţiune o astfel de colecţie
de date o vom numi fişier. În acest caz nu este posibil transferul întregii colecţii în memoria internă
pentru a fi ordonată şi apoi din nou transferul pe suport extern. Dacă datele ce urmează a fi sortate ocupă
un volum de n ori mai mare decât spaţiul de memorie internă de care dispunem, atunci colecţia se va
împărţi în n subcolecţii ce vor fi transferate succesiv în memoria internă, se vor sorta pe rând şi vor fi
stocate din nou pe suportul extern sortate. Din acest moment prin operaţii de interclasare două câte două
se pot obţine colecţii de dimensiuni superioare până se obţine toată colecţia ordonată.
La aceste interclasări, pentru a efectua un număr cât mai mic de operaţii de transfer se recomandă
interclasarea colecţiilor de dimensiuni minime, apoi din datele obţinute din nou vor fi alese două colecţii
de dimensiuni minime şi aşa mai departe până se obţine o singură colecţie care va fi colecţia cerută, adică
sortată.
7
După metodele de sortare externă folosite, se descriu trei procedee de sortare externă:
– sortarea echilibrată;
– sortarea polifazică;
– sortarea în cascadă.
Evident că sortarea depinde şi de configuraţia calculatorului folosit, dar şi de suportul pe care se
află fişierul de sortat şi fişierele intermediare create.
Principial sortarea externă presupune parcurgerea a două etape importante:
a) Divizarea fişierului de sortat F, în n fişiere H1, H2, ..., Hn, cu sortarea internă a acestora;
b) Interclasarea acestor fişiere sortate pentru a ajunge la fişierul dorit G.

8
REGULI IMPORTANTE PRIVIND ALEGEREA UNUI LIMBAJ DE PROGRAMARE

Prezentăm în continuare mai multe reguli importante, majoritatea dintre ele prezente şi explicate
în secţiunile anterioare.

Defineşte complet problema.

Această indicaţie, foarte importantă în activitatea de programare, pare fără sens pentru unii cititori.
Dar nu se poate rezolva o problemă dacă nu se cunoaşte această problemă. Specificarea corectă şi
completă a problemei nu este o sarcină trivială, ci una foarte importantă şi adeseori chiar dificilă.
Programul trebuie să respecte această specificaţie, să fie construit având tot timpul în faţă această
specificaţie, să i se demonstreze corectitudinea în raport cu această specificaţie, să fie testat şi validat
ţinând seama de această specificaţie.

Gândeşte mai întâi, programează pe urmă.

Începând cu scrierea specificaţiilor problemei, trebuie pusă în prim plan gândirea. Este specificaţia
problemei corectă? Între metodele de rezolvare posibile, care ar fi cea mai potrivită scopului urmărit? În
paralel cu proiectarea algoritmului demonstrează corectitudinea lui. Verifică corectitudinea fiecărui pas
înainte de a merge mai departe.

Nu folosi variabile neiniţializate.

Este vorba de prezenţa unei variabile într-o expresie fără ca în prealabil această variabilă să fi
primit valoare. Este o eroare foarte frecventă a programatorilor începători (dar nu numai a lor). Destule
compilatoare permit folosirea variabilelor neiniţializate, neverificând dacă o variabilă a fost iniţializată
înaintea folosirii ei. Alte compilatoare iniţializează automat variabilele numerice cu valoarea zero. Cu
toate acestea nu e bine să ne bazăm pe o asemenea iniţializare ci să atribuim singuri valorile iniţiale
corespunzătoare variabilelor. Programul realizat trebuie să fie portabil, să nu se bazeze pe specificul unui
anumit compilator.

Verifică valoarea variabilei imediat după obţinerea acesteia.

Dacă o variabilă întreagă trebuie să ia valori într-un subdomeniu c1..c2 verifică respectarea acestei
proprietăţi. Orice încălcare a ei indică o eroare care trebuie înlăturată. Valoarea variabilei poate fi
calculată sau introdusă de utilizator. În primul caz, verificarea trebuie făcută după calcul, în al doilea caz
se recomandă ca verificarea să urmeze imediat după citirea valorii respectivei variabile.

Cunoaşte şi foloseşte metodele de programare.

Este vorba de programarea Top-Down, Rafinarea în paşi succesivi, Divide et impera [Gries85]),
Bottom-up şi mixtă, programarea modulară, programarea structurată şi celelalte metode prezentate în
acest curs, sau alte metode ce vor fi asimilate ulterior.
Aceste metode încurajează reutilizarea, reducând costul realizării programelor. De asemenea,
folosirea unor componente existente (deci testate) măreşte gradul de fiabilitate a produselor soft realizate
şi scurtează perioada de realizare a acestora. Evident, dacă o parte din SUBPROGRAMii necesari
programului sunt deja scrişi şi verificaţi, viteza de lucru va creşte prin folosirea lor. Foloseşte deci
bibliotecile de componente reutilizabile existente şi construieşte singur astfel de biblioteci, care să
înglobeze experienţa proprie.
O bună programare modulară elimină legăturile între două module prin variabile globale. Se
recomandă ca fiecare modul să realizeze o activitate bine definită şi independentă de alt modul.
9
Comunicarea între două module trebuie să se realizeze numai prin mecanismul parametrilor formali-
actuali.

Amână pe mai târziu detaliile nesemnificative.

Această regulă stabileşte priorităţile de realizare a componentelor unui program; în primul rând se
acordă atenţie aspectelor esenţiale, începând cu modulul principal. În fiecare fază dă atenţie lucrurilor
importante. De exemplu, este inutil să se piardă timp cu scrierea unor părţi de program pentru tipărirea
rezultatelor şi a constata ulterior că rezultatele nu sunt cele dorite, sau nu sunt corecte.
Nu uita însă că pentru beneficiar "Detaliile nesemnificative sunt semnificative". Beneficiarii ţin
foarte mult la forma rezultatelor şi, adeseori, judecă programatorii după această formă. E păcat de munca
depusă dacă tipărirea rezultatelor lasă o impresie proastă asupra beneficiarului.

Evită artificiile.

Prin folosirea artificiilor în programare, a prescurtărilor şi simplificărilor se pierde adesea din


claritatea programului şi, mult mai grav, uneori se ajunge chiar la introducerea unor erori. În plus se poate
pierde portabilitatea programului.
Există însă situaţii în care prin anumite artificii se câştigă eficienţă în execuţie sau se face
economie de memorie. Dacă acest fapt este important atunci artificiile sunt binevenite, în caz contrar nu
se recomandă folosirea lor.

Foloseşte constante simbolice.

Folosirea intensivă a constantelor simbolice este recomandată oriunde în textul sursă trebuie scris
un număr (la declararea tablourilor, la precizarea limitelor de variaţie a unor variabile, etc.). Prin
utilizarea acestor constante se măreşte gradul de generalitate a textului scris, iar în situaţia în care
valoarea unei constante trebuie schimbată, modificarea este mult mai uşoară (doar la locul definiţiei
constantei) şi nu duce la erori. Ea implică numai definiţia constantei, nu modificarea valorii concrete în
toate instrucţiunile programului.

Verifică corectitudinea algoritmului şi programului în fiecare etapă a elaborării lor.

Detectarea şi eliminarea unei erori imediat după comiterea ei duce la creşterea vitezei de realizare
a produsului, evitându-se activităţi inutile de depanare. Se recomandă demonstrarea corectitudinii fiecărui
algoritm folosit, întrucât erorile semnalate în timpul testării sunt adeseori greu de descoperit şi, câteodată,
imposibil de eliminat altfel decât prin rescrierea modulului sau programului respectiv. Urmează testarea
fiecărui subprogram imediat după ce a fost scris (codificat). Acest lucru se potriveşte codificării bottom-
up şi sugerează o abordare sistematică a activităţii de codificare. Dacă pentru proiectare se pot folosi
oricare dintre metodele indicate, în codificare (şi testarea aferentă codificării), abordarea de jos în sus este
esenţială. Sugerăm ca această testare să se facă independent de programul în care se va folosi
subprogramul testat. Este adevărat că activitatea de testare necesită un anumit timp, dar ea este utilă cel
puţin din trei puncte de vedere:
– scoate în evidenţă erorile provocate de proiectarea algoritmului sau codificarea
neadecvată a acestuia;
– facilitează detectarea erorilor, deoarece dimensiunea problemei este mai mică; în fapt nu
se pierde timp cu scrierea unui program de test, ci se câştigă timp, deoarece la fiecare nivel
de detaliere se vor folosi numai componente testate deja; ceea ce rămâne de testat la
nivelul respectiv este gestiunea corectă a apelurilor respectivelor componente;
– obligă implementatorul să gândească încă o utilizare (cel puţin) a respectivului
subprogram, independentă de cea pentru care a fost iniţial conceput.
10
Foloseşte denumiri sugestive pentru identificatorii utilizaţi în program.

Fiecare identificator (nume de variabilă, de tip de date, de constante, de subprograme) îşi are rolul
şi semnificaţia lui într-un program. E bine ca denumirea să reflecte această semnificaţie, mărind astfel
claritatea textului programului.
Unii programatori exagerează însă, folosind identificatori lungi, obţinuţi prin concatenarea mai
multor cuvinte. E clar că denumirea alesă redă semnificaţia variabilei, dar claritatea textului scade,
lungimea programului creşte şi citirea lui devine greoaie.

Cunoaşte şi respectă semnificaţia fiecărei variabile.

Fiecare variabilă are o semnificaţie. În demonstrarea corectitudinii algoritmului această


semnificaţie se reflectă, de cele mai multe ori, printr-un predicat invariant. O greşeală frecventă făcută de
unii programatori constă în folosirea unei variabile în mai multe scopuri.

Foloseşte variabile auxiliare numai acolo unde este strict necesar.

Fiecare variabilă trebuie să aibă o semnificaţie proprie, iar în demonstrarea corectitudinii


programului, acesteia i se ataşează un invariant, care trebuie verificat.
Folosirea necontrolată a mai multor variabile auxiliare, ruperea unor expresii chiar lungi în
subexpresii cu diferite denumiri, pot duce la reducerea clarităţii programului.

Prin scriere redă cât mai fidel structura programului.

Importanţa indentării şi spaţierii pentru claritatea programului au fost arătate anterior. Fiecare
programator trebuie să aibă propriile reguli de scriere, care să scoată cât mai bine în evidenţă structura
programului şi funcţiile fiecărei părţi a acestuia.

Nu uita să testezi programul chiar dacă ai demonstrat corectitudinea lui.

Sunt cunoscute demonstraţii greşite pentru unele teoreme celebre din matematică. Şi o
demonstraţie a corectitudinii unui program poate fi greşită. Dar, chiar dacă demonstrarea corectitudinii
algoritmului este validă, programul poate conţine greşeli de codificare, de introducere (tastare) sau pot fi
alte cauze care generează erori.

Nu recalcula limitele şi nu modifica variabila de ciclare în interiorul unei structuri repetitive dată
prin propoziţia Pseudocod PENTRU.

O astfel de practică poate duce la erori greu de detectat şi încalcă regulile programării structurate.
Atunci când este necesară schimbarea variabilei de ciclare sau a limitelor se recomandă folosirea uneia
din structurile repetitive REPETĂ sau CÂTTIMP.

Nu ieşi forţat din corpul unei structuri repetitive redată prin propoziţia Pseudocod PENTRU.

Instrucţiunea Pseudocod PENTRU corespunde unui număr cunoscut de execuţii ale corpului
ciclului. În situaţia când corpul conţine şi testarea condiţiei de continuare a ciclării, recomandăm a se
folosi structurile REPETĂ sau CÂTTIMP şi nu PENTRU.

Elaborează documentaţia programului în paralel cu realizarea lui.


11
Aşa cum s-a arătat în mai multe locuri din acest material, pe durata de viaţă a unui program se iau
mai multe decizii. E bine ca aceste decizii să rămână consemnate împreună cu rezultatul final al fiecărei
faze din viaţa programului (specificarea problemei, proiectarea algoritmilor, programul propriu-zis, datele
de test folosite). Vor rezulta documentaţii de analiză, proiectare, implementare şi exploatare. Primele trei
sunt necesare la întreţinerea aplicaţiei, trebuind a fi actualizate ori de câte ori se produc modificări, iar
ultima este necesară celor care exploatează aplicaţia. Pe lângă acestea, un program bun va trebui să
posede şi o componentă de asistenţă on-line (funcţie help), care contribuie la asigurarea a ceea ce am
numit interfaţă prietenoasă.

Foloseşte comentariile.

Rolul comentariilor a fost explicat în secţiunea 4.4. Este foarte greu să descifrăm un program lipsit
de comentarii, chiar dacă este vorba de propriu; program scris în urmă cu câteva luni sau ani de zile.
Orice program sau modul trebuie să fie însoţit de comentarii explicative dacă dorim să-l refolosim şi nu
trebuie să scriem programe care să nu poată fi refolosite. Minimum de comentarii într-un modul trebuie să
conţină specificarea acestui modul şi semnificaţia fiecărei variabile.

12
METODA BACKTRACKING

La dispoziţia celor care rezolvă probleme cu ajutorul calculatorului există mai multe metode.
Dintre acestea cel mai des utilizate sunt:
– metoda Greedy;
– metoda Divide et impera;
– metoda Branch and Bound;
– metoda Backtracking;
Metoda backtracking se aplică problemelor în care soluţia poate fi reprezentată sub forma unui
vector – x = (x1, x2, x3, …xk,… xn) € S, unde S este mulţimea soluţiilor problemei şi S = S1 x S2 x… x
Sn, şi Si sunt mulţimi finite având s elemente si xi € si , (¥)i = 1..n.
Pentru fiecare problemă se dau relaţii între componentele vectorului x, care sunt numite condiţii
interne; soluţiile posibile care satisfac condiţiile interne se numesc soluţii rezultat. Metoda de generare a
tuturor soluţiilor posibile si apoi de determinare a soluţiilor rezultat prin verificarea îndeplinirii condiţiilor
interne necesită foarte mult timp.
Metoda backtracking evită această generare şi este mai eficientă. Elementele vectorului x,
primesc pe rând valori în ordinea crescătoare a indicilor, x[k] va primi o valoare numai daca au fost
atribuite valori elementelor x1.. x[k-1]. La atribuirea valorii lui x[k] se verifica îndeplinirea unor condiţii
de continuare referitoare la x1…x[k-1]. Daca aceste condiţii nu sunt îndeplinite, la pasul k, acest lucru
înseamnă ca orice valori i-am atribui lui x[k+1], x[k+1], .. x[n] nu se va ajunge la o soluţie rezultat.
Metoda backtracking construieşte un vector soluţie în mod progresiv începând cu prima
componentă a vectorului şi mergând spre ultima cu eventuale reveniri asupra atribuirilor anterioare.
Metoda se aplica astfel :
1) se alege prima valoare sin S1 si I se atribuie lui x1 ;
2) se presupun generate elementele x1…x[k-1], cu valori din S1..S[k-1]; pentru generarea lui x[k] se
alege primul element din S[k] disponibil si pentru valoarea aleasa se testează îndeplinirea
condiţiilor de continuare.
Pot apărea următoarele situaţii :
a) x[k] îndeplineşte condiţiile de continuare. Daca s-a ajuns la soluţia finală (k = n) atunci se
afişează soluţia obţinută. Daca nu s-a ajuns la soluţia finală se trece la generarea
elementului următor – x [k-1];
b) x[k] nu îndeplineşte condiţiile de continuare. Se încearcă următoarea valoare disponibila
din S[k]. Daca nu se găseşte nici o valoare în S[k] care să îndeplinească condiţiile de
continuare, se revine la elementul x[k-1] şi se reia algoritmul pentru o nouă valoare a
acestuia. Algoritmul se încheie când au fost luate in considerare toate elementele lui S1.
Problemele rezolvate prin această metodă necesită timp mare de execuţie, de aceea este indicat sa
se folosească metoda numai daca nu avem alt algoritm de rezolvare.
Dacă mulţimile S1,S2,…Sn au acelaşi număr k de elemente, timpul necesar de execuţie al
algoritmului este k la n. Dacă mulţimile S1, S2.. Sn nu au acelaşi număr de elemente, atunci se notează cu
„m” minimul cardinalelor mulţimilor S1…Sn si cu „M”, maximul. Timpul de execuţie este situat în
intervalul [m la n .. M la n]. Metoda backtracking are complexitatea exponenţială, in cele mai multe
cazuri fiind ineficientă. Ea insa nu poate fi înlocuită cu alte variante de rezolvare mai rapide în situaţia în
care se cere determinarea tuturor soluţiilor unei probleme.
Generarea permutărilor. Se citeşte un număr natural n. Să se genereze toate permutările
mulţimii {1, 2, 3, …,n}.
Generarea permutărilor se va face ţinând cont că orice permutare va fi alcătuită din elemente
distincte ale mulţimii A. Din acest motiv, la generarea unei permutări, vom urmări ca numerele să fie
distincte.

13
Prezentăm algoritmul corespunzător cazului n=3:

1 2 3
1 2 2 2 2
1 1 1 1 1 1

1 2 3
3 3 3 3 1
1 1 1 1 2 2

1 2 3 1
1 1 1 2 3 3
2 2 2 2 2 2

 se încarcă în stivă pe nivelul 1 valoarea 1;


 încărcarea valorii 1 pe nivelul al 2-lea nu este posibilă, întrucât această valoare se
găseşte şi pe nivelul 1 al stivei;
 încărcarea valorii 2 pe nivelul al 2-lea este posibilă, deoarece această valoare nu mai
este întâlnită;
 valoarea 1 din nivelul al 3-lea se regăseşte pe nivelul 1;
 valoarea 2 din nivelul al 3-lea se regăseşte pe nivelul al 2-lea;
 valoarea 3 pe nivelul al 3-lea nu e întâlnită pe nivelurile anterioare; întrucât nivelul 3
este completat corect. Tipărim: 1 2 3
……
Algoritmul continuă până când stiva devine vidă.
Programul sursa este urmatorul:

Private Sub CommandButton14_Click()


cit_n "n = ", n
back_perm
End Sub

Sub back_perm()
Dim k As Integer
k = 1
While k > 0
Do
succesor am_suc, st, k
If am_suc = True Then
valid1 ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_r
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
14
End Sub

Sub valid1(ev As Boolean, st As stiva, k As Integer)


ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) Then
ev = False
End If
Next
End Sub

Sub tipar_r()
Dim i As Integer, b As String
b = " "
For i = 1 To n
b = b + Str$(st.ss(i)) + ","
Next
MsgBox b
End Sub

Sub succesor(am_suc As Boolean, st As stiva, k As Integer)


If st.ss(k) < n Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

Stiva este acea formă de organizare a datelor (structură de date) cu proprietatea că operaţiile de
introducere şi scoatere a datelor se fac în vârful ei.
Stivele se pot simula utilizând vectori.
Fie ST(i) un vector. ST(1), ST(2), ..., ST(n) pot reţine numai litere sau numai cifre. O variabilă K
indică în permanentă vârful stivei.
Exemplificăm, în continuare, modul de lucru cu stiva:

A
În stiva iniţial vidă se introduce litera A, vârful stivei va fi la nivelul 1 (k-1);

B
introducem în stivă litera B, deci k va lua valoarea 2;
A

scoatem din stivă pe B (A nu poate fi scos);


A
scoatem din stivă pe A; stiva rămâne vidă

În mod practic la scoaterea unei variabile din stivă, scade cu 1 valoarea variabilei ce indică vârful
stivei, iar atunci când scriem ceva în stivă, o eventuală valoare reziduală se pierde:
Pe un anumit nivel se retine, de regulă, o singură informaţie (literă sau cifră), însă este posibil; aşa
cum va rezulta din exemplele, prezentate în lucrare, să avem mai multe informaţii, caz în care avem de a
face cu stive duble, triple, etc.
Întreaga teorie a recursivităţii se bazează pe structura de tip stivă.

15
Prezentarea tehnicii Backtracking
Această tehnică se foloseşte în rezolvarea problemelor care îndeplinesc simultan următoarele
condiţii:
– soluţia lor poate fi pusă sub forma unui vector S=x1,x2, ...,xn, cu x1 € A1, x2 € A2 …, xn
€ An
– mulţimile A1, A2 , …., An sunt mulţimi finite, iar elementele lor se consideră că se află
într-o relaţie de ordine bine stabilită;
– nu se dispune de o altă metodă de rezolvare, mai rapidă
– x1 x2 …, xn pot fi la rândul lor vectori;
– A1, A2 …, An pot coincide.

La întâlnirea unei astfel de probleme, dacă nu cunoaştem această tehnică, suntem tentaţi să
generăm toate elementele produsului cartezian A1,A2 …,An si fiecare element să fie testat dacă este
soluţie. Rezolvând problema în acest mod, timpul de execuţie este atât de mare, încât poate fi considerat
infinit, algoritmul neavând nici o valoare practică.
De exemplu, dacă dorim să generăm toate permutările unei mulţimi finite A, nu are rost să
generăm produsul cartezian AxAx.....xA, pentru ca apoi, să testăm, pentru fiecare element al acestuia,
dacă este sau nu permutare (nu are rost să generăm 1.1,1.......1, pentru ca apoi să constatăm că nu am
obţinut o permutare, când de la a doua cifră 1 ne puteam da seama că cifrele nu sunt distincte).
Tehnica Backtracking are la bază un principiu extrem de simplu:
– se construieşte soluţia pas cu pas: x1, x2 …,xn
– dacă se constată că, pentru o valoare aleasă, nu avem cum să ajungem la soluţie, se
renunţă la acea valoare şi se reia căutarea din punctul în care am rămas.
Concret:
– se alege primul element x, ce aparţine lui A;
– presupunând generate elementele x1,x2 …,xk , aparţinând mulţimilor A1, A2 …,Ak, se
alege (dacă există) xk+1, primul element disponibil din mulţimea Ak+1, apar două
posibilităţi:
1) Nu s-a găsit un astfel de element, caz în care caz în care se reia căutarea considerând
generate elementele x1,x2 …,xk+1 , iar aceasta se reia de la următorul element al mulţimii
Ak rămas netestat;
2) A fost găsit, caz în care se testează dacă acesta îndeplineşte anumite condiţii de
continuare apărând astfel două posibilităţi:
 îndeplineşte, caz în care se testează dacă s-a ajuns la soluţie si apar din nou două
posibilităţi:
- s-a ajuns la soluţie, se tipăreşte soluţia si se reia algoritmul considerând generate
elementele x1,x2 …,xk, (se caută în continuare, un alt element al mulţimii Ak, rămas
netestat);
- nu s-a ajuns la soluţie, caz în care se reia algoritmul considerând generate elementele
x1,x2 …,xk , si se caută un prim element xk+2 € Ak.
 nu le îndeplineşte caz în care se reia algoritmul considerând generate elementele x 1,x2
…, xk , iar elementul xk-1 se caută între elementele mulţimii A, rămase netestate.
Algoritmii se termină atunci când nu există nici un element x1 € A1 netestat.
Observaţie: tehnica Backtracking are ca rezultat obţinerea tuturor soluţiilor problemei. În cazul în care
se cere o sigură soluţie se poate forţa oprirea, atunci când a fost găsită.
Am arătat că orice soluţie se generează sub formă de vector. Vom considera că generarea soluţiilor
se face intr-o stivă. Astfel, x1 € A1, se va găsi pe primul nivel al stivei, x2 € A2 se va găsi pe al doilea nivel
al stivei,... xk € Ak se va găsi pe nivelul k al stivei. În acest fel, stiva (notată ST) va arăta astfel:

16
...
xk

x2
x1
ST

Nivelul k+1 al stivei trebuie iniţializat (pentru a alege, în ordine, elementele mulţimii k+1 ).
Iniţializarea trebuie făcută cu o valoare aflată (în relaţia de ordine considerată, pentru mulţimea A k+1 )
înaintea tuturor valorilor posibile din mulţime. De exemplu, pentru generarea permutărilor mulţimii
{1,2.....n}, orice nivel al stivei va lua valori de la 1 la n. Iniţializarea unui nivel (oarecare) se face cu
valoarea 0. Procedura de iniţializare o vom numi INIT şi va avea doi parametri: k (nivelul care trebuie
iniţializat si ST (stiva)).
Găsirea următorului element al mulţimii Ak (element care a fost netestat) se face cu ajutorul
procedurii SUCCESOR (AS,ST,K). Parametrul AS (am succesor) este o variabilă booleană. În situaţia în
care am găsit elementul, acesta este pus în stivă şi AS ia valoarea TRUE, contrar (nu a rămas un element
netestat) AS ia valoarea FALSE..
Odată ales un element, trebuie văzut dacă acesta îndeplineşte condiţiile de continuare (altfel spus,
dacă elementul este valid). Acest test se face cu ajutorul procedurii VALID (EV,ST,K).
Testul dacă s-a ajuns sau nu la soluţia finală se face cu ajutorul funcţiei SOLUTIE(k) iar o soluţie
se tipăreşte cu ajutorul procedurii TIPAR. Prezentăm în continuare rutina Backtracking:

k:=1; CALL init(1,st);


while k>0
do
CALL succesor (as, st, k) ;
if as then CALLvalid(ev,st,k) then
loop until (not as) or (as and ev) ;
if as then
if solutie(k) then
CALL tipar
else
k:=k+l;
CALL init ( k, st );
end;
else
k:=k-1
wend

Observaţie: Problemele rezolvate prin această metodă necesită un timp îndelungat. Din acest motiv, este
bine să utilizăm metoda numai atunci când nu avem la dispoziţie un alt algoritm mai eficient. Cu toate că
există probleme pentru care nu se pot elabora alţi algoritmi mai eficienţi, tehnica backtracking trebuie
aplicată numai în ultimă instanţă.
Fiind dată o tablă de şah, de dimensiune n, xn, se cer toate soluţiile de aranjare a n dame, astfel
încât să nu se afle două dame pe aceeaşi linie, coloană sau diagonală (dame să nu se atace reciproc).

17
Exemplu: Presupunând că dispunem de o tablă de dimensiune 4x4, o soluţie ar fi următoarea:
D
D
D
D

Observăm că o damă trebuie să fie plasată singură pe linie. Plasăm prima damă pe linia 1, coloana 1.
D

A doua damă nu poate fi aşezată decât în coloana 3.


D
D

Observăm că a treia damă nu poate fi plasată în linia 3. Încercăm atunci plasarea celei de-a doua dame în
coloana 4.
D
D

A treia damă nu poate fi plasată decât în coloana 2.

D
D
D

În această situaţie dama a patra nu mai poate fi aşezată.


Încercând să avansăm cu dama a treia, observăm că nu este posibil să o plasăm nici în coloana 3,
nici în coloana 4, deci o vom scoate de pe tablă. Dama a doua nu mai poate avansa, deci şi ea este scoasă
de pe tablă. Avansăm cu prima damă în coloana 2.

18
A doua damă nu poate fi aşezată decât în coloana 4.
D
D

Dama a treia se aşează în prima coloană.


D
D
D

Acum este posibil să plasăm a patra damă în coloana 3 si astfel am obţinut o soluţie a problemei.

D
D
D
D

Algoritmul continuă în acest mod până când trebuie scoasă de pe tablă prima damă.
Pentru reprezentarea unei soluţii putem folosi un vector cu n componente (având în vedere că pe
fiecare linie se găseşte o singură damă).
Exemplu pentru soluţia găsită avem vectorul ST ce poate fi asimilat unei stive.
Două dame se găsesc pe aceeaşi diagonală dacă si numai dacă este îndeplinită condiţia: |st(i)-
st(j)|=|i-j| ( diferenţa, în modul, între linii si coloane este aceeaşi).

În general ST(i)=k semnifică faptul că pe linia i dama ocupă poziţia k.

3 ST(4)

1 ST(3)

4 ST(2)

2 ST(1)

Exemplu: în tabla 4 x4 avem situaţia:


D
st(1)= 1 i = 1
D st(3)= 3 j = 3
D |st(1) - st(3)| = |1 – 3| = 2
D |i – j| = |1 – 3| = 2

sau situaţia

19
D st(1) = 3 i = 1
D st(3) = 1 j = 3
|st(i) - st(j)| = |3 – 1| = 2
D
|i – j| = |1 – 3| = 2
D

Întrucât doua dame nu se pot găsi în aceeaşi coloană, rezultă că o soluţie este sub formă de
permutare. O primă idee ne conduce la generarea tuturor permutărilor si la extragerea soluţiilor pentru
problema ca două dame să nu fie plasate în aceeaşi diagonală. A proceda astfel, înseamnă că lucrăm
conform strategiei backtracking. Aceasta presupune ca imediat ce am găsit două dame care se atacă, să
reluăm căutarea.
Iată algoritmul, conform strategiei generate de backtracking:
– În prima poziţie a stivei se încarcă valoarea 1, cu semnificaţia că în linia unu se aşează
prima damă în coloană.
– Linia 2 se încearcă aşezarea damei în coloana 1, acest lucru nefiind posibil întrucât
avem doua dame pe aceeaşi coloană.
– În linia 2 se încearcă aşezarea damei în coloana 2 , însă acest lucru nu este posibil,
pentru că damele se găsesc pe aceiaşi diagonală (|st(1)-st(2)|=|1-2|);
– Aşezarea damei 2 în coloana 3 este posibilă.
– Nu se poate plasa dama 3 în coloana 1, întrucât în liniile 1-3 damele ocupa acelaşi
coloană.
– Şi această încercare eşuează întrucât damele de pe 2 şi 3 sunt pe aceeaşi diagonală.
– Damele de pe 2-3 se găsesc pe aceeaşi coloană.
– Damele de pe 2-3 se găsesc pe aceeaşi diagonală.
– Am coborât în stivă mutând dama de pe linia 2 şi coloana 3 în coloana 4.
Algoritmul se încheie atunci când stiva este vidă. Semnificaţia procedurilor utilizate este
următoarea:
INIT - nivelul k al stivei este iniţializat cu 0;
SUCCESOR - măreşte cu 1 valoarea aflată pe nivelul k al stivei în situaţia în care aceasta
este mai mică decât n şi atribuie variabilei EV valoarea TRUE, în caz contrar, atribuie
variabilei EV valoarea FALSE;
VALID - validează valoarea pusă pe nivelul k al stivei, verificând dacă nu avem două
dame pe aceeaşi linie (st(k)=st(i)), sau dacă nu avem două dame pe aceeaşi diagonală
(st(k)-st(i)=|k-i|)caz în care variabilei EV i se atribuie FALSE; în caz contrar, variabilei EV
i se atribuie TRUE;
SOLUTIE - verifică dacă stiva a fost completată până la nivelul n inclusiv;
TIPAR - tipăreşte o soluţie.
Subprogramele prezentate in limbajul Visual Basic sunt descrise mai jos:

Global n As Integer, am_suc As Boolean, ev As Boolean


Type stiva
ss(100) As Integer
End Type
Global st As stiva

Sub init(k As Integer, st As stiva)


st.ss(k) = 0
End Sub

Sub succesor(am_suc As Boolean, st As stiva, k As Integer)


If st.ss(k) < n Then
am_suc = True

20
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

Sub valid(ev As Boolean, st As stiva, k As Integer)


ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) Or (Abs(st.ss(i) - st.ss(k)) = Abs(k -
i)) Then
ev = False
End If
Next
End Sub

Function solutie(k As Integer) As Integer


If k = n Then
solutie = True
Else
solutie = False
End If
End Function

Sub tipar()
Dim i As Integer, b As String
b = " "
For i = 1 To n
b = b + "(" + Str$(i) + "," + Str$(st.ss(i)) + "),"
Next
MsgBox b
End Sub

Sub back()
Dim k As Integer
k = 1
While k > 0
Do
succesor am_suc, st, k
If am_suc = True Then
valid ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

Sub Button2_Click()
21
n = InputBox("n=", ib_title)
back
End Sub

Produsul cartezian a n mulţimi. Se dau mulţimile de mai jos şi se cere produsul cartezian al lor.
A1 = {1, 2, 3, …, k1}
A2 = {1, 2, 3, …, k2}
………………………
An = {1, 2, 3, …, kn}
Exemplu: A1 = {1, 2}
A2 = {1, 2, 3}
A3 = {1, 2, 3}
A1  A2  A3 = {(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 2, 1), (1, 2, 2), (1, 2, 3), (1, 3, 1), (1, 3, 2), (1, 3, 3),
(2, 1, 1), (2, 1, 2), (2, 1, 3), (2, 2, 1), (2, 2, 2), (2, 2, 3), (2, 3, 1), (2, 3, 2), (2, 3, 3)}.
Pentru rezolvare, se folosesc stiva ST şi un vector A ce reţine numerele k1, k2, …kn. Utilizăm
metoda backtracking, uşor modificată din următoarele motive:
a) Orice element aflat la nivelul k al stivei este valid, motiv pentru care procedura valid nu
face altceva decât să atribuie variabilei ev valoarea TRUE.
b) Limita superioară pe nivelul k al stivei este dată de A(k).
Modul de concepere a algoritmului rezultă din cele ce urmează:

1 2 3 1

1 1 1 2 2

1 1 1 1 1 1

2 3 1 2 3

2 2 3 3 3 3

1 1 1 1 1 1

Observaţii:
Algoritmul prezentat aici este de tip backtracking? Întrebarea are sens pentru că este absent mecanismul
de întoarcere. Vom admite că şi aceasta este backtracking, dar „degenerat”.

Private Sub CommandButton16_Click()


Dim a As vector
cit_n "n=", n
cit_date "a", n, a
tipar " multimile sunt : ", n, a
back_prod_cart
End Sub

Sub cit_n(mes As String, nnn As Integer)


Do
nnn = InputBox(mes, y)
Loop Until n > 0 And n < 100
End Sub
22
Sub cit_date(mes As String, n As Integer, a As vector)
For i = 1 To n
a.v(i) = InputBox(mes + "(" + Str$(i) + ")=", y)
Next
End Sub

Sub tipar(mes As String, n As Integer, a As vector)


sir = ""
For i = 1 To n
sir = sir + Str$(a.v(i)) + ","
Next
MsgBox mes + " " + sir
End Sub

Sub back_prod_cart()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor_prod am_suc, st, k
If am_suc = True Then
valid_prod ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_r
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

Sub valid_prod(ev As Boolean, st As stiva, k As Integer)


ev = True
End Sub
Function solutie(k As Integer) As Boolean
If k = n Then
solutie = True
Else
solutie = False
End If
End Function

Sub succesor_prod(am_suc As Boolean, st As stiva, k As Integer)


If st.ss(k) < a.v(k) Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
23
End If
End Sub
Sub init(k As Integer, st As stiva)
st.ss(k) = 0
End Sub

Generarea aranjamentelor. Se citesc n şi p. Să se genereze toate aranjamentele de n luate câte p.


Din analiza problemei rezultă următoarele:
– stiva are înălţimea p;
– fiecare nivel ia valori între 1 şi n;
– elementele plasate pe diverse niveluri trebuie să fie distincte.
Algoritmul este asemănător cu cel de la permutări, cu deosebirea că aici stipa are înălţimea p.

Private Sub CommandButton17_Click()


cit_n "n = ", n
cit_n "p = ", p
back_aranj
End Sub

Sub back_aranj()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor am_suc, st, k
If am_suc = True Then
valid1 ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie1(k) Then
tipar_rr
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

Sub valid1(ev As Boolean, st As stiva, k As Integer)


ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) Then
ev = False
End If
Next
End Sub

Sub tipar_rr()
Dim i As Integer, b As String
b = " "
For i = 1 To p
24
b = b + Str$(st.ss(i)) + ","
Next
MsgBox b
End Sub

Function solutie1(k As Integer) As Boolean


If k = p Then
solutie1 = True
Else
solutie1 = False
End If
End Function

Sub succesor(am_suc As Boolean, st As stiva, k As Integer)


If st.ss(k) < n Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub
Sub init(k As Integer, st As stiva)
st.ss(k) = 0
End Sub

Generarea combinărilor. Se citesc n şi p numere naturale, np. Se cere să se genereze toate


submulţimile cu p elemente ale mulţimii {1, 2, 3, …, n}.
Pentru rezolvarea problemei trebuie ţinut cont de următoarele:
– stiva are înălţimea p;
– elementele aflate pe niveluri diferite ale stivei trebuie să fie distincte;
– pentru a evita repetiţia elementele se aşează în ordine crescătoare: pe nivelul k se va afla
o valoare mai mare decât pe nivelul k-1 şi mai mică sau egală cu n-p+k.

Private Sub CommandButton18_Click()


cit_n "n = ", n
cit_n "p = ", p
back_comb
End Sub

Sub back_comb()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor_c am_suc, st, k
If am_suc = True Then
valid_c ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie1(k) Then
tipar_rr
Else
k = k + 1
init k, st
25
End If
Else
k = k - 1
End If
Wend
End Sub

Sub succesor_c(am_suc As Boolean, st As stiva, k As Integer)


If st.ss(k) < n - p + k Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

Sub valid_c(ev As Boolean, st As stiva, k As Integer)


Dim i As Integer
ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) Then
ev = False
End If
Next
If k > 1 Then
If st.ss(k) < st.ss(k - 1) Then
ev = False
End If
End If
End Sub

Function solutie1(k As Integer) As Boolean


If k = p Then
solutie1 = True
Else
solutie1 = False
End If
End Function

Sub tipar_col()
Dim i As Integer, b As String
b = " "
For i = 1 To n
b = b + "Tara = " + Str$(i) + "; culoarea " + Str$(st.ss(i)) + " "
Next
MsgBox b
End Sub

Problema comis-voiajorului. Un comis voiajor trebuie să viziteze un număr n de oraşe. Iniţial, el


se află într-unul dintre ele, notat 1. Comis – voiajorul doreşte să nu treacă de două ori prin acelaşi oraş, iar
la întoarcere să revină în oraşul 1. Cunoscând legăturile existente între oraşe, se cere să se tipărească toate
drumurile posibile pe care le poate efectua comis – voiajorul.

26
Exemplu: În figura alăturată sunt simbolizate cele 6 oraşe, precum şi drumurile existente între ele.

2 3

1 4

6 5

Comis - voiajorul are următoarele posibilităţi de parcurgere:


1, 2, 3, 4, 5, 6, 1;
1, 2, 5, 4, 3, 6, 1;
1, 6, 3, 4, 5, 2, 1;
1, 6, 5, 4, 3, 2, 1;
Legăturile existente între oraşe sunt date în matricea An,n. Elementele matricei A pot fi 0 sau 1
(matricea este binară).
1, dacă există drum între oraşele i şi j;
A(i,j) =
0 , altfel
Se observă că A(i,j) = A(j,i), oricare ar fi i,j {1, 2, 3, …, n} – matricea este simetrică.
Pentru rezolvarea problemei folosim stiva st. la baza stivei (nivelul 1) se încarcă numărul 1.
Prezentăm în continuare modul de rezolvare a problemei.

2
De la oraşul 1 la oraşul 2 există drum, deci se va urca în stivă;
1

2
2 Oraşul 2 se mai găseşte în stivă, deci nu este acceptat;
1

3
De la oraşul 2 la oraşul 3 se găseşte drum; prin oraşul 3 nu s-a mai trecut, deci
2
oraşul 3 este acceptat.
1

Algoritmul continuă în acest mod până se ajunge din nou la nivelul 1, caz în care algoritmul se
încheie.
Un succesor, între 2 şi n, aflat pe nivelul k al stivei, este considerat valid dacă sunt îndeplinite
următoarele condiţii:
– nu s-a mai trecut prin oraşul simbolizat de succesor, deci acesta nu se regăseşte în stivă;
– există drum între oraşul aflat la nivelul k-1 şi cel aflat la nivelul k;
27
– dacă succesorul se găseşte la nivelul n, să existe drum de la el la oraşul 1.
Observaţii:
1. Problemele rezolvate prin această metodă necesită un timp îndelungat de execuţie. Din
acest motiv este bine să utilizăm metoda atunci numai atunci când nu mai avem la
dispoziţie un alt algoritm mai eficient
2. Menţionăm că nu există probleme pentru care nu se cunosc algoritmi eficienţi de
rezolvare, deci backtracking este indicată.
3. Rezolvarea iterativă încalcă principiul stivei atunci când verificăm condiţiile de
continuare, sau atunci când tipărim soluţia găsită, pentru că accesăm orice nivel al stivei.
Consider că o structură trebuie folosită ca atare atunci când este strict necesar. De exemplu,
chiar şi segmentul de stivă al calculatorului poate fi accesat oriunde. Asta nu înseamnă că
acolo nu se utilizează din plin „mecanismul” stivei.

Sub back_comis()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor_col am_suc, st, k
If am_suc = True Then
valid_col ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_col
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

Sub succesor_comis(am_suc As Boolean, st As stiva, k As Integer)


If st.ss(k) < n Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub
Sub valid_comis(ev As Boolean, st As stiva, k As Integer)
ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) And (mat.m(i, k) = 1) Then
ev = False
End If
Next
End Sub

Sub tipar_comis()
28
Dim i As Integer, b As String
b = " "
For i = 1 To n
b = b + "Tara = " + Str$(i) + "; vizitat " + Str$(st.ss(i)) + " "
Next
MsgBox b
End Sub

PROBLEMA COLORĂRII HĂRŢILOR

Enunţ:
Fiind dată o hartă cu n ţări, se cer toate soluţiile de colorare a hărţii, utilizând cel mult patru culori,
astfel încât două ţări de frontieră comună să fie colorate diferit. Este demonstrat faptul că sunt suficiente
numai patru culori pentru ca orice hartă să poată fi colorată.
Rezolvare:
Pentru exemplificare, vom considera următoarea hartă unde ţările sunt numerotate cu cifre
cuprinse între 1 şi 5:

3
2
5

Figura 9.1 Harta ţărilor


pentrproblema
O soluţie a acestei probleme este următoarea:
ţara 1 – culoarea 1
ţara 2 – culoarea 2
ţara 3 – culoarea 1
ţara 4 – culoarea 3
ţara 5 – culoarea 4
Harta este furnizată programului cu ajutorul unei matrice An,n
1, dacă ţara i se învecinează cu ţara j;
A(i,j) =
0 , altfel
Matricea A este simetrică. Pentru rezolvarea problemei se utilizează stiva st, unde nivelul k al
stivei simbolizează ţara k, iar st[k] culoarea ataşată ţării k. Stiva are înălţimea n şi pe fiecare nivel ia
valori între 1 şi 4.

Sub back_col()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor_col am_suc, st, k
If am_suc = True Then
valid_col ev, st, k
29
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_col
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

Sub succesor_col(am_suc As Boolean, st As stiva, k As Integer)


If st.ss(k) < 4 Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

Sub valid_col(ev As Boolean, st As stiva, k As Integer)


ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) And (mat.m(i, k) = 1) Then
ev = False
End If
Next
End Sub

Sub tipar_col()
Dim i As Integer, b As String
b = " "
For i = 1 To n
b = b + "Tara = " + Str$(i) + "; culoarea " + Str$(st.ss(i)) + " "
Next
MsgBox b
End Sub

Sub tipar_rr()
Dim i As Integer, b As String
b = " "
For i = 1 To p
b = b + Str$(st.ss(i)) + ","
Next
MsgBox b
End Sub

30
METODA DIVIDE ET IMPERA

Metoda de programare DIVIDE ET IMPERA consta in impartirea problemei initiale de


dimensiuni [n] in doua sau mai multe probleme de dimensiuni reduse. In general se executa impartirea in
doua subprobleme de dimensiuni aproximativ egale si anume [n/2]. Impartirea in subprobleme are loc
pana cand dimensiunea acestora devine suficient de mica pentru a fi rezolvate in mod direct(cazul de
baza). Dupa rezolvarea celor doua subprobleme se executa faza de combinare a rezultatelor in vederea
rezolvarii intregii probleme.
Metoda DIVIDE ET IMPERA se poate aplica in rezolvarea unei probleme care indeplineste
urmatoarele conditii:
 se poate descompune in (doua sau mai multe) suprobleme;
 aceste suprobleme sunt independente una fata de alta (o subproblema nu se rezolva pe
baza alteia si nu se foloseste rezultate celeilalte);
 aceste subprobleme sunt similare cu problema initiala;
 la randul lor subproblemele se pot descompune (daca este necesar) in alte subprobleme
mai simple;
 aceste subprobleme simple se pot solutiona imediat prin algoritmul simplificat.
Deoarece putine probleme indeplinesc conditiile de mai sus ,aplicarea metodei este destul de rara.
Dupa cum sugereaza si numele "desparte si stapaneste "etapele rezolvarii unei probleme (numita
problema initiala) in DIVIDE ET IMPERA sunt :
 descompunerea problemei initiale in subprobleme independente, smilare problemei de
baza, de dimensiuni mai mici;
 descompunerea treptata a subproblemelor in alte subprobleme din ce in ce mai simple,
pana cand se pot rezolva imediata ,prin algoritmul simplificat;
 rezolvarea subproblemelor simple;
 combinarea solutiilor gasite pentru construirea solutiilor subproblemelor de dimensiuni
din ce in ce mai mari;
 combinarea ultimelor solutii determina obtinerea solutiei problemei initiale
Metoda DIVIDE ET IMPERA admite o implementare recursiva, deorece subproblemele sunt
similare problemei initiale, dar de dimensiuni mai mici.
Principiul fundamental al recursivitatii este autoapelarea unui subprogram cand acesta este activ;
ceea ce se intampla la un nivel, se intampla la orice nivel, avand grija sa asiguram conditia de terminare
ale apelurilor repetate. Asemanator se intampla si in cazul metodei DIVITE ET IMPERA; la un anumit
nivel sunt doua posibilitati:
 s-a ajuns la o (sub)problema simpla ce admite o rezolvare imediata caz in care se
rezolva (sub)problema si se revine din apel (la subproblema anterioara, de dimensiuni mai
mari);
 s-a ajuns la o (sub)problema care nu admite o rezolvare imediata, caz in care o
descompunem in doua sau mai multe subprobleme si pentru fiecare din ele se continua
apelurile recursive (ale procedurii sau functiei).
In etapa finala a metodei DIVIDE ET IMPERA se produce combinarea subproblemelor
(rezolvate deja) prin secventele de revenire din apelurile recursive.
Etapele metodei DIVIDE ET IMPERA (prezentate anterior) se pot reprezenta prin urmatorul
subprogram general (procedura sau functie )recursiv exprimat in limbaj natural:
Subprogram DIVIMP (PROB);
Daca PROBLEMA PROB este simpla
Atunci se rezolva si se obtine solutia SOL
Altfel pentru i=1,k executa DIVIMP(PROB) si se obtine SOL1;
Se combina solutiile SOL 1,... ,SOL K si se obtine SOL;
31
Sfarsit_subprogram;
Deci, subprogramul DIVIMP se apeleaza pentru problema initiala PROB; aceasta admite
descompunerea in K subprobleme simple; pentru acestea se reapeleaza recursiv subprogramul; in final se
combina solutiile acestor K subprobleme.
De obicei problema initiala se descompune in doua subprobleme mai simple; in acest caz etapele
generale ale metodei DIVIDE ET IMPERA se pot reprezenta concret, in limbaj pseudocod, printr-o
procedura recursiva astfel:
Procedura DIVIMP(li,ls,sol);
Daca ((ls-li)<=eps)
Atunci REZOLVA (li,ls,sol);
Altfel
DIVIDE (li,m,ls);
DIVIMP(li,msol1);
DIVIMP(m,ls,sol2);
COMBINA(sol1,sol2,sol);
Sfarsit_procedura;

Procedura DIVIMP se apeleaza pentru problema initiala care are dimensiunea intre limita
inferioara (li) si limita inferioara(ls); daca (sub)problema este simpla (ls-li<=eps), atunci procedura
REZOLVA ii afla solutia imediat si se produce intoarcerea din apelul recursiv; daca (sub)problema este
(inca) complexa, atunci procedura DIVIDE o imparte in doua subprobleme, alegand pozitia m intre
limitele li si ls; pentru fiecare din cele doua subprobleme se reapeleaza recursiv procedura DIVIMP; in
final, la intoarcerile din apeluri se produce combinarea celor doua soluitii sol1 si sol2 prin apelul
procedurii COMBINA.

PROBLEMA TURNURILOR DIN HANOI


Prezentarea algoritmului rezolvarii
Fie trei tije verticale notate A,B,C. Pe tija A se gasesc asezate n discuri de diametre diferite, in
ordinea crescatoare a diametrelor, privind de sus in jos. Initial, tijele B si C sunt goale. Sa se afiseze toate
mutarile prin care discurile de pe tija A se muta pe tija B, in aceeasi ordine, folosind ca tija de manevra C
si resspectand urmatoarele reguli:
– la fiecare pas se muta un singur disc;
– un disc se poate aseza numai peste un disc cu diametrul mai mare.
Rezolvarea acestei probleme se bazeaza pe urmatoarele considerente logice:
– daca n=1, atunci mutarea este immediata AB (mut discul de pe A pe B);
– daca n=2, atunci sirul mutarilor este: AC,AB,CB;
– daca n>2 procedam astfel :
- mut (n-1) discuri AC;
- mut un disc AB;
- mut cele (n-1) discuri CB.
Observam ca problema initiala se descompune in trei subprobleme mai simple, similare problemei
initiale: mut (n-1)discuri AC, mut ultimul disc pe B, mut cele (n-1)discuri C-->B. Dimensiunile acestor
subprobleme sunt: n-1,1,n-1.
Aceste subprobleme sunt independente, deoarece tijele initial (pe care sunt dispuse discurile),
tijele finale si tijele intermediare sunt diferite. Notam H(n,A,B,C)=sirul mutarilor a n discuri de pe A pe
B, folosind C.

PENTRU
n=1 AB
n>1 H(n,A,B,C)= H(n-1,A,C,B),AB, H(n-1,C,B,A)

32
Sortare rapida (quicksort)
Un tablou V se completeaza cu n elemente numere reale .Sa se ordoneze crescator folosind
metoda de sortare rapida .
Solutia problemei se bazeaza pe urmatoarele etape implementate in programul principal:
 se apeleaza procedura “quick” cu limita inferioara li=1 si limita superioara ls=n;
 functia”poz” realizeaza mutarea elementului v[i] exact pe pozitia ce o va ocupa acesta
in vectorul final ordonat ; functia”poz” intoarce (in k ) pozitia ocupata de acest element;
 in acest fel , vectorul V se imparte in doua parti: li …k-1 si k+1…ls;
 pentru fiecare din aceste parti se reapeleaza procedura ”quick”, cu limitele modificate
corespunzator;
 in acest fel, primul element din fiecare parte va fi pozitionat exact pe pozitia finala ce o
va ocupa in vectorul final ordonat (functia”poz”);
 fiecare din cele doua parti va fi, astfel, inpartita in alte doua parti; procesul continua
pana cand limitele partilor ajung sa se suprapuna ,ceea ce indica ca toate elementele
vectorului au fost mutate exact pe pozitiile ce le vor ocupa in vectorul final ;deci vectorul
este ordonat ;
 in acest moment se produc intoarcerile din apelurile recursive si programul isi termina
executia.
Observaţii:
– daca elementul se afla in stanga, atunci se compara cu elementele din dreapta lui si se
sar (j:=j-1) elementele mai mari decat el;
– daca elementul se afla in dreapta, atunci se compara cu elemente din stanga lui si se sar
(i:=i+1) elementele mai mici decat el.
Sortare prin interclasare(mergesort)
Tabloul unidimensional V se completeaza cu n numere reale. Sa se ordoneze crescator folosind
sortare prin interclasare.
Sortarea prin interclasare se bazeaza pe urmatoarea logica:
– vectorul V se imparte, prin injumatatiri succesive,in vectori din ce in ce mai mici;
– cand se ating vectorii de maxim doua elemente, fiecare dintre acestia se ordoneaza
printr-o simpla comparare a elementelor;
– cate doi astfel de mini-vectori ordonati se interclaseaza succesiv pana se ajunge iar la
vectorul V.
Observaţii:
– mecanismul general de tip Divide et Impera se gaseste implementat in procedura “divi”;
– astfel de abordare a problemei sortarii unii vector conduce la economie de timp de
calcul, deoarece operatia de interclasare a doi vectori deja ordonati este foarte rapida, iar
ordonarea independenta celor doua jumatati (mini-vectori) consuma in total aproximativ a
doua parte din timpul care ar fi necesar ordonarii vectorului luat ca intreg .

CONCLUZII LA TEHNICA DIVIDE ET IMPERA

Sortare prin insertie binara


Sa se ordoneze crescator un tablou unidimensional V de n numere reale, folosind sortarea prin
insertie binara.
Pentru fiecare element v[i] se procedeaza in patru pasi:
1. se considera ordonate elementele v[1],v[2],….,v[i-1];
2. se cauta pozitia k pe care urmeaza s-o ocupe v[i] intre elementele v[1],v[2],…,v[i-1]
(procedura “poz” prin cautare binara);
3. se deplaseaza spre dreapta elementele din pozitiile k,k+1,…,n (procedura “deplasare”);
4. insereaza elementul v[i] in pozitia k (procedura”deplasare”);
33
Se obtine o succesiune de k+1 elemente ordonate crescator.

Analiza a complexitatii timp pentru algoritmii Divide et Impera


Algoritmii de tip Divide et Impera au buna comportare in timp, daca se indeplinesc urmatoarele
conditii:
– dimensiunile subprogramelor (in care se imparte problema initiala) sunt aproximativ
egale (“principiul balansarii”);
– lipsesc fazele de combinare a solutiilor subproblemelor (cautare binara).

34
METODA GREEDY

Algoritmi greedy

Pusi in fata unei probleme pentru care trebuie sa elaboram un algoritm, de multe ori “nu stim cum
sa incepem”. Ca si in orice alta activitate, exista cateva principii generale care ne pot ajuta in aceasta
situatie. Ne propunem sa prezentam in urmatoarele capitole tehnicile fundamentale de elaborare a
algoritmilor. Cateva din aceste metode sunt atat de generale, incat le folosim frecvent, chiar daca numai
intuitiv, ca reguli elementare in gandire.

Tehnica greedy

Algoritmii greedy (greedy = lacom) sunt in general simpli si sunt folositi la probleme de
optimizare, cum ar fi: sa se gaseasca cea mai buna ordine de executare a unor lucrari pe calculator, sa se
gaseasca cel mai scurt drum intr-un graf etc. In cele mai multe situatii de acest fel avem:
 multime de candidati (lucrari de executat, varfuri ale grafului etc)
 o functie care verifica daca o anumita multime de candidati constituie o solutie posibila
 o functie care verifica daca o multime de candidati este fezabila, adica daca este posibil
sa completam aceasta multime astfel incat sa obtinem o solutie posibila, nu neaparat
optima, a problemei
 o functie de selectie care indica la orice moment care este cel mai promitator dintre
candidatii inca nefolositi
 o functie obiectiv care da valoarea unei solutii (timpul necesar executarii tuturor
lucrarilor intr-o anumita ordine, lungimea drumului pe care l-am gasit etc); aceasta este
functia pe care urmarim sa o optimizam (minimizam/maximizam)
Pentru a rezolva problema noastra de optimizare, cautam o solutie posibila care sa optimizeze
valoarea functiei obiectiv. Un algoritm greedy construieste solutia pas cu pas. Initial, multimea
candidatilor selectati este vida. La fiecare pas, incercam sa adaugam acestei multimi cel mai promitator
candidat, conform functiei de selectie. Daca, dupa o astfel de adaugare, multimea de candidati selectati nu
mai este fezabila, eliminam ultimul candidat adaugat; acesta nu va mai fi niciodata considerat. Daca, dupa
adaugare, multimea de candidati selectati este fezabila, ultimul candidat adaugat va ramane de acum
incolo in ea. De fiecare data cand largim multimea candidatilor selectati, verificam daca aceasta multime
nu constituie o solutie posibila a problemei noastre. Daca algoritmul greedy functioneaza corect, prima
solutie gasita va fi totodata o solutie optima a problemei. Solutia optima nu este in mod necesar unica: se
poate ca functia obiectiv sa aiba aceeasi valoare optima pentru mai multe solutii posibile. Descrierea
formala a unui algoritm greedy general este:

function greedy(C)
{C este multimea candidatilor}
S← ø {S este multimea in care construim solutia}
while not solutie(S) and C ≠ ø do
x ← un element din C care maximizeaza/minimizeaza select(x)
C ← C \ {x}
if fezabil(S  {x}) then S ← S  {x}
if solutie(S) then return S
else return “nu există soluţie”

35
Este de inteles acum de ce un astfel de algoritm se numeste “lacom” (am putea sa-i spunem si
“nechibzuit”). La fiecare pas, procedura alege cel mai bun candidat la momentul respectiv, fara sa-i pese
de viitor si fara sa se razgandeasca. Daca un candidat este inclus in solutie, el ramane acolo; daca un
candidat este exclus din solutie, el nu va mai fi niciodata reconsiderat. Asemenea unui intreprinzator
rudimentar care urmareste castigul imediat in dauna celui de perspectiva, un algoritm greedy actioneaza
simplist. Totusi, ca si in afaceri, o astfel de metoda poate da rezultate foarte bune tocmai datorita
simplitatii ei.
Functia select este de obicei derivata din functia obiectiv; uneori aceste doua functii sunt chiar
identice.
Un exemplu simplu de algoritm greedy este algoritmul folosit pentru rezolvarea urmatoarei
probleme. Sa presupunem ca dorim sa dam restul unui client, folosind un numar cat mai mic de monezi.
In acest caz, elementele problemei sunt:
 candidatii: multimea initiala de monezi de 1, 5, si 25 unitati, in care presupunem ca din
fiecare tip de moneda avem o cantitate nelimitata
 solutie posibila: valoarea totala a unei astfel de multimi de monezi selectate trebuie sa
fie exact valoarea pe care trebuie sa o dam ca rest
 multime fezabila: valoarea totala a unei astfel de multimi de monezi selectate nu este
mai mare decat valoarea pe care trebuie sa o dam ca rest
 functia de selectie: se alege cea mai mare moneda din multimea de candidati ramasa
 functia obiectiv: numarul de monezi folosite in solutie; se doreste minimizarea acestui
numar
Se poate demonstra ca algoritmul greedy va gasi in acest caz mereu solutia optima (restul cu un
numar minim de monezi). Pe de alta parte, presupunand ca exista si monezi de 12 unitati sau ca unele din
tipurile de monezi lipsesc din multimea initiala de candidati, se pot gasi contraexemple pentru care
algoritmul nu gaseste solutia optima, sau nu gaseste nici o solutie cu toate ca exista solutie.
Evident, solutia optima se poate gasi incercand toate combinarile posibile de monezi. Acest mod
de lucru necesita insa foarte mult timp.
Un algoritm greedy nu duce deci intotdeauna la solutia optima, sau la o solutie. Este doar un
principiu general, urmand ca pentru fiecare caz in parte sa determinam daca obtinem sau nu solutia
optima.

Minimizarea timpului mediu de asteptare

O singura statie de servire (procesor, pompa de benzina etc) trebuie sa satisfaca cererile a n clienti.
Timpul de servire necesar fiecarui client este cunoscut in prealabil: pentru clientul i este necesar un timp
ti, 1 ≤ i ≤ n. Dorim sa minimizam timpul total de asteptare
(timpul de asteptare pentru clientul i)
ceea ce este acelasi lucru cu a minimiza timpul mediu de asteptare, care este T/n. De exemplu, daca avem
trei clienti cu t1 = 5, t2 = 10, t3 = 3, sunt posibile sase ordini de servire. In primul caz, clientul 1 este servit
primul, clientul 2 asteapta pana este servit clientul 1 si apoi este servit, clientul 3 asteapta pana sunt serviti
clientii 1, 2 si apoi este servit. Timpul total de asteptare a celor trei clienti este 38.

Ordinea T
1 2 3 5+(5+10)+(5+10+3) = 38
1 3 2 5+(5+3)+(5+3+10) = 31
2 1 3 10+(10+5)+(10+5+3) = 43
2 3 1 10+(10+3)+(10+3+5) = 41
3 1 2 3+(3+5)+(3+5+10) = 29 ← optim
3 2 1 3+(3+10)+(3+10+5) = 34
36
Algoritmul greedy este foarte simplu: la fiecare pas se selecteaza clientul cu timpul minim de
servire din multimea de clienti ramasa. Vom demonstra ca acest algoritm este optim.
Fie I = (i1 i2 ... in) o permutare oarecare a intregilor {1, 2, ..., n}. Daca servirea are loc in ordinea I,
avem

Presupunem acum ca I este astfel incat putem gasi doi intregi a < b cu

Interschimbam pe ia cu ib in I; cu alte cuvinte, clientul care a fost servit al b-lea va fi servit acum
al a-lea si invers. Obtinem o noua ordine de servire J, care este de preferat deoarece

Prin metoda greedy obtinem deci intotdeauna planificarea optima a clientilor.


Problema poate fi generalizata pentru un sistem cu mai multe statii de servire.

Interclasarea optima a sirurilor ordonate


Sa presupunem ca avem doua siruri S1 si S2 ordonate crescator si ca dorim sa obtinem prin
interclasarea lor sirul ordonat crescator care contine elementele din cele doua siruri. Daca interclasarea
are loc prin deplasarea elementelor din cele doua siruri in noul sir rezultat, atunci numarul deplasarilor
este #S1 + #S2.
Generalizand, sa consideram acum n siruri S1, S2, ..., Sn, fiecare sir Si, 1 ≤ i ≤ n, fiind format din qi
elemente ordonate crescator (vom numi qi lungimea lui Si). Ne propunem sa obtinem sirul S ordonat
crescator, continand exact elementele din cele n siruri. Vom realiza acest lucru prin interclasari succesive
de cate doua siruri. Problema consta in determinarea ordinii optime in care trebuie efectuate aceste
interclasari, astfel incat numarul total al deplasarilor sa fie cat mai mic. Exemplul de mai jos ne arata ca
problema astfel formulata nu este banala, adica nu este indiferent in ce ordine se fac interclasarile.
Fie sirurile S1, S2, S3 de lungimi q1 = 30, q2 = 20, q3 = 10. Daca interclasam pe S1 cu S2, iar
rezultatul il interclasam cu S3, numarul total al deplasarilor este (30+20)+(50+10)=110. Daca il
interclasam pe S3 cu S2, iar rezultatul il interclasam cu S1, numarul total al deplasarilor este
(10+20)+(30+30)=90.
Atasam fiecarei strategii de interclasare cate un arbore binar in care valoarea fiecarui varf este data
de lungimea sirului pe care il reprezinta. Daca sirurile S1, S2, ..., S6 au lungimile q1 = 30, q2 = 10, q3 = 20,
q4 = 30, q5 = 50, q6 = 10, doua astfel de strategii de interclasare sunt reprezentate prin arborii din Figura
11.1.

Figura 1 Reprezentarea strategiilor de interclasare.

Observam ca fiecare arbore are 6 varfuri terminale, corespunzand celor 6 siruri initiale si 5 varfuri
neterminale, corespunzand celor 5 interclasari care definesc strategia respectiva. Numerotam varfurile in
37
felul urmator: varful terminal i, 1 ≤ i ≤ 6, va corespunde sirului Si, iar varfurile neterminale se
numeroteaza de la 7 la 11 in ordinea obtinerii interclasarilor respective (Figura 2).

Figura 2 Numerotarea varfurilor arborilor din Figura 1

Strategia greedy apare in Figura 11.1b si consta in a interclasa mereu cele mai scurte doua siruri
disponibile la momentul respectiv.
Interclasand sirurile S1, S2, ..., Sn, de lungimi q1, q2, ..., qn, obtinem pentru fiecare strategie cate un
arbore binar cu n varfuri terminale, numerotate de la 1 la n, si n–1 varfuri neterminale, numerotate de la
n+1 la 2n–1. Definim, pentru un arbore oarecare A de acest tip, lungimea externa ponderata:

unde ai este adancimea varfului i. Se observa ca numarul total de deplasari de elemente pentru strategia
corespunzatoare lui A este chiar L(A). Solutia optima a problemei noastre este atunci arborele (strategia)
pentru care lungimea externa ponderata este minima.

Proprietatea 1 Prin metoda greedy se obtine intotdeauna interclasarea optima a n siruri ordonate, deci
strategia cu arborele de lungime externa ponderata minima.

Demonstratie: Demonstram prin inductie. Pentru n = 1, proprietatea este verificata. Presupunem ca


proprietatea este adevarata pentru n–1 siruri. Fie A arborele strategiei greedy de interclasare a n siruri de
lungime q1 ≤ q2 ≤ ... qn. Fie B un arbore cu lungimea externa ponderata minima, corespunzator unei
strategii optime de interclasare a celor n siruri. In arborele A apare subarborele

reprezentand prima interclasare facuta conform strategiei greedy. In arborele B, fie un varf neterminal de
adancime maxima. Cei doi fii ai acestui varf sunt atunci doua varfuri terminale qj si qk. Fie B' arborele
obtinut din B schimband intre ele varfurile q1 si qj, respectiv q2 si qk. Evident, L(B') ≤ L(B). Deoarece B
are lungimea externa ponderata minima, rezulta ca L(B') = L(B). Eliminand din B' varfurile q1 si q2,
obtinem un arbore B" cu n–1 varfuri terminale q1+q2, q3, ..., qn. Arborele B' are lungimea externa
ponderata minima si L(B') = L(B") + q1+q2. Rezulta ca si B" are lungimea externa ponderata minima.
Atunci, conform ipotezei inductiei, avem L(B") = L(A'), unde A' este arborele strategiei greedy de
interclasare a sirurilor de lungime q1+q2, q3, ..., qn. Cum A se obtine din A' atasand la varful q1+q2 fiii q1 si
q2, iar B' se obtine in acelasi mod din B", rezulta ca L(A) = L(B') = L(B). Proprietatea este deci adevarata
pentru orice n.
38
La scrierea algoritmului care genereaza arborele strategiei greedy de interclasare vom folosi un
min-heap. Fiecare element al min-heap-ului este o pereche (q, i) unde i este numarul unui varf din
arborele strategiei de interclasare, iar q este lungimea sirului pe care il reprezinta. Proprietatea de min-
heap se refera la valoarea lui q.
Algoritmul interopt va construi arborele strategiei greedy. Un varf i al arborelui va fi memorat in
trei locatii diferite continand:
LU[i] = lungimea sirului reprezentat de varf
ST[i] = numarul fiului stang
DR[i] = numarul fiului drept

procedure interopt(Q[1 .. n])


{construieste arborele strategiei greedy de interclasare
a sirurilor de lungimi Q[i] = qi, 1 ≤ i ≤ n}
H ← min-heap vid
for i ← 1 to n do
(Q[i], i) => H {insereaza in min-heap}
LU[i] ← Q[i]; ST[i] ← 0; DR[i] ← 0
for i ← n+1 to 2n–1 do
(s, j) <= H {extrage radacina lui H}
(r, k) <= H {extrage radacina lui H}
ST[i] ← j; DR[i] ← k; LU[i] ← s+r
(LU[i], i) => H {insereaza in min-heap}

In cazul cel mai nefavorabil, operatiile de inserare in min-heap si de extragere din min-heap
necesita un timp in ordinul lui log n. Restul operatiilor necesita un timp constant. Timpul total pentru
interopt este deci in O(n log n).

Coduri Huffman
O alta aplicatie a strategiei greedy si a arborilor binari cu lungime externa ponderata minima este
obtinerea unei codificari cat mai compacte a unui text.
Un principiu general de codificare a unui sir de caractere este urmatorul: se masoara frecventa de
aparitie a diferitelor caractere dintr-un esantion de text si se atribuie cele mai scurte coduri, celor mai
frecvente caractere, si cele mai lungi coduri, celor mai putin frecvente caractere. Acest principiu sta, de
exemplu, la baza codului Morse. Pentru situatia in care codificarea este binara, exista o metoda eleganta
pentru a obtine codul respectiv. Aceasta metoda, descoperita de Huffman (1952) foloseste o strategie
greedy si se numeste codificarea Huffman. O vom descrie pe baza unui exemplu.
Fie un text compus din urmatoarele litere (in paranteze figureaza frecventele lor de aparitie):
S (10), I (29), P (4), O (9), T (5)
Conform metodei greedy, construim un arbore binar fuzionand cele doua litere cu frecventele cele
mai mici. Valoarea fiecarui varf este data de frecventa pe care o reprezinta.

Etichetam muchia stanga cu 1 si muchia dreapta cu 0. Rearanjam tabelul de frecvente:

39
Multimea {P, T} semnifica evenimentul reuniune a celor doua evenimente independente
corespunzatoare aparitiei literelor P si T. Continuam procesul, obtinand arborele

In final, ajungem la arborele din Figura 3, in care fiecare varf terminal corespunde unei litere din
text.
Pentru a obtine codificarea binara a literei P, nu avem decat sa scriem secventa de 0-uri si 1-uri in
ordinea aparitiei lor pe drumul de la radacina catre varful corespunzator lui P: 1011. Procedam similar si
pentru restul literelor:
S (11), I (0), P (1011), O (100), T (1010)
Pentru un text format din n litere care apar cu frecventele f1, f2, ..., fn, un arbore de codificare este
un arbore binar cu varfurile terminale avand valorile f1, f2, ..., fn, prin care se obtine o codificare binara a
textului. Un arbore de codificare nu trebuie in mod necesar sa fie construit dupa metoda greedy a lui
Huffman, alegerea varfurilor care sunt fuzionate la fiecare pas putandu-se face dupa diverse criterii.
Lungimea externa ponderata a unui arbore de codificare este:

Figura 3 Arborele de codificare Huffman.

unde ai este adincimea varfului terminal corespunzator literei i. Se observa ca lungimea externa ponderata
este egala cu numarul total de caractere din codificarea textului considerat. Codificarea cea mai compacta
a unui text corespunde deci arborelui de codificare de lungime externa ponderata minima. Se poate
demonstra ca arborele de codificare Huffman minimizeaza lungimea externa ponderata pentru toti arborii
de codificare cu varfurile terminale avand valorile f1, f2, ..., fn. Prin strategia greedy se obtine deci
intotdeauna codificarea binara cea mai compacta a unui text.
Arborii de codificare pe care i-am considerat in acesta sectiune corespund unei codificari de tip
special: codificarea unei litere nu este prefixul codificarii nici unei alte litere. O astfel de codificare este
de tip prefix. Codul Morse nu face parte din aceasta categorie. Codificarea cea mai compacta a unui sir de
caractere poate fi intotdeauna obtinuta printr-un cod de tip prefix. Deci, concentrandu-ne atentia asupra
acestei categorii de coduri, nu am pierdut nimic din generalitate.

Arbori parţiali de cost minim

Fie G = <V, M> un graf neorientat conex, unde V este multimea varfurilor si M este multimea
muchiilor. Fiecare muchie are un cost nenegativ (sau o lungime nenegativa). Problema este sa gasim o
submultime A  M, astfel incat toate varfurile din V sa ramina conectate atunci cand sunt folosite doar
40
muchii din A, iar suma lungimilor muchiilor din A sa fie cat mai mica. Cautam deci o submultime A de
cost total minim. Aceasta problema se mai numeste si problema conectarii oraselor cu cost minim, avand
numeroase aplicatii.
Graful partial <V, A> este un arbore si este numit arborele partial de cost minim al grafului G
(minimal spanning tree). Un graf poate avea mai multi arbori partiali de cost minim si acest lucru se poate
verifica pe un exemplu.
Vom prezenta doi algoritmi greedy care determina arborele partial de cost minim al unui graf. In
terminologia metodei greedy, vom spune ca o multime de muchii este o solutie, daca constituie un arbore
partial al grafului G, si este fezabila, daca nu contine cicluri. O multime fezabila de muchii este
promitatoare, daca poate fi completata pentru a forma solutia optima. O muchie atinge o multime data de
varfuri, daca exact un capat al muchiei este in multime. Urmatoarea proprietate va fi folosita pentru a
demonstra corectitudinea celor doi algoritmi.

Proprietatea 2 Fie G = <V, M> un graf neorientat conex in care fiecare muchie are un cost nenegativ. Fie
W  V o submultime stricta a varfurilor lui G si fie A  M o multime promitatoare de muchii, astfel
incat nici o muchie din A nu atinge W. Fie m muchia de cost minim care atinge W. Atunci, A  {m} este
promitatoare.

Demonstratie: Fie B un arbore partial de cost minim al lui G, astfel incat A  B (adica, muchiile din A
sunt continute in arborele B). Un astfel de B trebuie sa existe, deoarece A este promitatoare. Daca m  B,
nu mai ramane nimic de demonstrat. Presupunem ca m  B. Adaugandu-l pe m la B, obtinem exact un
ciclu. In acest ciclu, deoarece m atinge W, trebuie sa mai existe cel putin o muchie m' care atinge si ea pe
W (altfel, ciclul nu se inchide). Eliminandu-l pe m', ciclul dispare si obtinem un nou arbore partial B' al lui
G. Costul lui m este mai mic sau egal cu costul lui m', deci costul total al lui B' este mai mic sau egal cu
costul total al lui B. De aceea, B' este si el un arbore partial de cost minim al lui G, care include pe m.
Observam ca A  B' deoarece muchia m', care atinge W, nu poate fi in A. Deci, A  {m} este
promitatoare.

Multimea initiala a candidatilor este M. Cei doi algoritmi greedy aleg muchiile una cate una intr-o
anumita ordine, aceasta ordine fiind specifica fiecarui algoritm.

Algoritmul lui Kruskal


Arborele partial de cost minim poate fi construit muchie, cu muchie, dupa urmatoarea metoda a lui
Kruskal (1956): se alege intai muchia de cost minim, iar apoi se adauga repetat muchia de cost minim
nealeasa anterior si care nu formeaza cu precedentele un ciclu. Alegem astfel #V–1 muchii. Este usor de
dedus ca obtinem in final un arbore. Este insa acesta chiar arborele partial de cost minim cautat?
Inainte de a raspunde la intrebare, sa consideram, de exemplu, graful din Figura 11.4a. Ordonam
crescator (in functie de cost) muchiile grafului: {1, 2}, {2, 3}, {4, 5}, {6, 7}, {1, 4}, {2, 5}, {4, 7}, {3, 5},
{2, 4}, {3, 6}, {5, 7}, {5, 6} si apoi aplicam algoritmul. Structura componentelor conexe este ilustrata,
pentru fiecare pas, in Tabelul 11.1.

41
Figura 4 Un graf si arborele sau partial de cost minim.

Pasul Muchia considerata Componentele conexe ale


subgrafului <V, A>
initializare — {1}, {2}, {3}, {4}, {5}, {6}, {7}
1 {1, 2} {1, 2}, {3}, {4}, {5}, {6}, {7}
2 {2, 3} {1, 2, 3}, {4}, {5}, {6}, {7}
3 {4, 5} {1, 2, 3}, {4, 5}, {6}, {7}
4 {6, 7} {1, 2, 3}, {4, 5}, {6, 7}
5 {1, 4} {1, 2, 3, 4, 5}, {6, 7}
6 {2, 5} respinsa (formeaza ciclu)
7 {4, 7} {1, 2, 3, 4, 5, 6, 7}

Tabelul 1 Algoritmul lui Kruskal aplicat grafului din Figura 4a.

Multimea A este initial vida si se completeaza pe parcurs cu muchii acceptate (care nu formeaza
un ciclu cu muchiile deja existente in A). In final, multimea A va contine muchiile {1, 2}, {2, 3}, {4, 5},
{6, 7}, {1, 4}, {4, 7}. La fiecare pas, graful partial <V, A> formeaza o padure de componente conexe,
obtinuta din padurea precedenta unind doua componente. Fiecare componenta conexa este la randul ei un
arbore partial de cost minim pentru varfurile pe care le conecteaza. Initial, fiecare varf formeaza o
componenta conexa. La sfarsit, vom avea o singura componenta conexa, care este arborele partial de cost
minim cautat (Figura 11.4b).
Ceea ce am observat in acest caz particular este valabil si pentru cazul general, din Proprietatea
11.2 rezultand:

Proprietatea 3 In algoritmul lui Kruskal, la fiecare pas, graful partial <V, A> formeaza o padure de
componente conexe, in care fiecare componenta conexa este la randul ei un arbore partial de cost minim
pentru varfurile pe care le conecteaza. In final, se obtine arborele partial de cost minim al grafului G.
Pentru a implementa algoritmul, trebuie sa putem manipula submultimile formate din varfurile
componentelor conexe. Folosim pentru aceasta o structura de multimi disjuncte si procedurile de tip find
si merge. In acest caz, este preferabil sa reprezentam graful ca o lista de muchii cu costul asociat lor,
astfel incat sa putem ordona aceasta lista in functie de cost. Iata algoritmul:

function Kruskal(G = <V, M>)


{initializare}
sorteaza M crescator in functie de cost
n ← #V
A←ø {va contine muchiile arborelui partial de cost minim}
42
initializeaza n multimi disjuncte continand
fiecare cate un element din V

{bucla greedy}
repeat
{u, v} ← muchia de cost minim care
inca nu a fost considerate
ucomp ← find(u)
vcomp ← find(v)
if ucomp ≠ vcomp then merge(ucomp, vcomp)
A ← A  {{u, v}}
until #A = n-1
return A

Pentru un graf cu n varfuri si m muchii, presupunand ca se folosesc procedurile find3 si merge3,


numarul de operatii pentru cazul cel mai nefavorabil este in:
 O(m log m) pentru a sorta muchiile. Deoarece m ≤ n(n–1)/2, rezulta
O(m log m)  O(m log n). Mai mult, graful fiind conex, din n-1 ≤ m rezulta si
O(m log n)  O(m log m), deci O(m log m) = O(m log n).
 O(n) pentru a initializa cele n multimi disjuncte.
 Cele cel mult 2m operatii find3 si n–1 operatii merge3 necesita un timp in O((2m+n-
1) lg* n). Deoarece O(lg* n)  O(log n) si n-1 ≤ m, acest timp este si in O(m log n).
 O(m) pentru restul operatiilor.
Deci, pentru cazul cel mai nefavorabil, algoritmul lui Kruskal necesita un timp in O(m log n).
O alta varianta este sa pastram muchiile intr-un min-heap. Obtinem astfel un nou algoritm, in care
initializarea se face intr-un timp in O(m), iar fiecare din cele n–1 extrageri ale unei muchii minime se face
intr-un timp in O(log m) = O(log n). Pentru cazul cel mai nefavorabil, ordinul timpului ramane acelasi cu
cel al vechiului algoritm. Avantajul folosirii min-heap-ului apare atunci cand arborele partial de cost
minim este gasit destul de repede si un numar considerabil de muchii raman netestate. In astfel de situatii,
algoritmul vechi pierde timp, sortand in mod inutil si aceste muchii.

Algoritmul lui Prim


Cel de-al doilea algoritm greedy pentru determinarea arborelui partial de cost minim al unui graf
se datoreaza lui Prim (1957). In acest algoritm, la fiecare pas, multimea A de muchii alese impreuna cu
multimea U a varfurilor pe care le conecteaza formeaza un arbore partial de cost minim pentru subgraful
<U, A> al lui G. Initial, multimea U a varfurilor acestui arbore contine un singur varf oarecare din V, care
va fi radacina, iar multimea A a muchiilor este vida. La fiecare pas, se alege o muchie de cost minim, care
se adauga la arborele precedent, dand nastere unui nou arbore partial de cost minim (deci, exact una dintre
extremitatile acestei muchii este un varf in arborele precedent). Arborele partial de cost minim creste
“natural”, cu cate o ramura, pina cand va atinge toate varfurile din V, adica pina cand U = V. Functionarea
algoritmului, pentru exemplul din Figura 11.4a, este ilustrata in Tabelul 11.2. La sfarsit, A va contine
aceleasi muchii ca si in cazul algoritmului lui Kruskal. Faptul ca algoritmul functioneaza intotdeauna
corect este exprimat de urmatoarea proprietate, pe care o puteti demonstra folosind Proprietatea 11.2.

Pasul Muchia considerata U


initializare — {1}
1 {2, 1} {1, 2}
2 {3, 2} {1, 2, 3}
3 {4, 1} {1, 2, 3, 4}
4 {5, 4} {1, 2, 3, 4, 5}
43
5 {7, 4} {1, 2, 3, 4, 5, 6}
6 {6, 7} {1, 2, 3, 4, 5, 6, 7}

Tabelul 2 Algoritmul lui Prim aplicat grafului din Figura 11.4a.

Proprietatea 11.4 In algoritmul lui Prim, la fiecare pas, <U, A> formeaza un arbore partial de cost minim
pentru subgraful <U, A> al lui G. In final, se obtine arborele partial de cost minim al grafului G.

Descrierea formala a algoritmului este data in continuare.


function Prim-formal(G = <V, M>)
{initializare}
A ←ø {va contine muchiile arborelui partial de cost minim}
U ← {un varf oarecare din V}
{bucla greedy}
while U ≠ V do
gaseste {u, v} de cost minim astfel ca u  V \ U si v  U
A ← A  {{u, v}}
U ← U  {u}
return A

Pentru a obtine o implementare simpla, presupunem ca: varfurile din V sunt numerotate de la 1 la
n, V = {1, 2, ..., n}; matricea simetrica C da costul fiecarei muchii, cu C[i, j] = +  , daca muchia {i, j} nu
exista. Folosim doua tablouri paralele. Pentru fiecare i  V \ U, vecin[i] contine varful din U, care este
conectat de i printr-o muchie de cost minim; mincost[i] da acest cost. Pentru i  U, punem mincost[i] = –
1. Multimea U, in mod arbitrar initializata cu {1}, nu este reprezentata explicit. Elementele vecin[1] si
mincost[1] nu se folosesc.

function Prim(C[1 .. n, 1 .. n])


{initializare; numai varful 1 este in U}
A←ø
for i ← 2 to n do vecin[i] ← 1
mincost[i] ← C[i, 1]
{bucla greedy}
repeat n–1 times
min ← + 
for j ← 2 to n do
if 0 < mincost[ j] < min then min ← mincost[ j]
k←j
A ← A  {{k, vecin[k]}}
mincost[k] ← –1 {adauga varful k la U}
for j ← 2 to n do
if C[k, j] < mincost[ j] then mincost[ j] ← C[k, j]
vecin[ j] ← k
return A

Bucla principala se executa de n–1 ori si, la fiecare iteratie, buclele for din interior necesita un
timp in O(n). Algoritmul Prim necesita, deci, un timp in O(n2). Am vazut ca timpul pentru algoritmul lui
Kruskal este in O(m log n), unde m = #M. Pentru un graf dens (adica, cu foarte multe muchii), se deduce
44
ca m se apropie de n(n–1)/2. In acest caz, algoritmul Kruskal necesita un timp in O(n2 log n) si algoritmul
Prim este probabil mai bun. Pentru un graf rar (adica, cu un numar foarte mic de muchii), m se apropie de
n si algoritmul Kruskal necesita un timp in O(n log n), fiind probabil mai eficient decat algoritmul Prim.

5 Cele mai scurte drumuri care pleaca din acelasi punct

Fie G = <V, M> un graf orientat, unde V este multimea varfurilor si M este multimea muchiilor.
Fiecare muchie are o lungime nenegativa. Unul din varfuri este desemnat ca varf sursa. Problema este sa
determinam lungimea celui mai scurt drum de la sursa catre fiecare varf din graf.
Vom folosi un algoritm greedy, datorat lui Dijkstra (1959). Notam cu C multimea varfurilor
disponibile (candidatii) si cu S multimea varfurilor deja selectate. In fiecare moment, S contine acele
varfuri a caror distanta minima de la sursa este deja cunoscuta, in timp ce multimea C contine toate
celelalte varfuri. La inceput, S contine doar varful sursa, iar in final S contine toate varfurile grafului. La
fiecare pas, adaugam in S acel varf din C a carui distanta de la sursa este cea mai mica.
Spunem ca un drum de la sursa catre un alt varf este special, daca toate varfurile intermediare de-a
lungul drumului apartin lui S. Algoritmul lui Dijkstra lucreaza in felul urmator. La fiecare pas al
algoritmului, un tablou D contine lungimea celui mai scurt drum special catre fiecare varf al grafului.
Dupa ce adaugam un nou varf v la S, cel mai scurt drum special catre v va fi, de asemenea, cel mai scurt
dintre toate drumurile catre v. Cand algoritmul se termina, toate varfurile din graf sunt in S, deci toate
drumurile de la sursa catre celelalte varfuri sunt speciale si valorile din D reprezinta solutia problemei.
Presupunem, pentru simplificare, ca varfurile sunt numerotate, V = {1, 2, ..., n}, varful 1 fiind
sursa, si ca matricea L da lungimea fiecarei muchii, cu L[i, j] = +  , daca muchia (i, j) nu exista. Solutia
se va construi in tabloul D[2 .. n]. Algoritmul este:

function Dijkstra(L[1 .. n, 1 .. n])


{initializare}
C ← {2, 3, ..., n} {S = V \C exista doar implicit}
for i ← 2 to n do D[i] ← L[1, i]
{bucla greedy}
repeat n–2 times
v ← varful din C care minimizeaza D[v]
C ← C \ {v} {si, implicit, S ← S  {v}}
for fiecare w  C do
D[w] ← min(D[w], D[v]+L[v, w])
return D

Pentru graful din Figura 11.5, pasii algoritmului sunt prezentati in Tabelul 11.3.

Figura 5 Un graf orientat.

Pasul v C D
45
Initializare — {2, 3, 4, 5} [50, 30, 100, 10]
1 5 {2, 3, 4} [50, 30, 20, 10]
2 4 {2, 3} [40, 30, 20, 10]
3 3 {2} [35, 30, 20, 10]

Tabelul 3 Algoritmul lui Dijkstra aplicat grafului din Figura 11.5.

Observam ca D nu se schimba daca mai efectuam o iteratie pentru a-l scoate si pe {2} din C. De
aceea, bucla greedy se repeta de doar n-2 ori.
Se poate demonstra urmatoarea proprietate:

Proprietatea 5. In algoritmul lui Dijkstra, daca un varf i


i) este in S, atunci D[i] da lungimea celui mai scurt drum de la sursa catre i;
ii) nu este in S, atunci D[i] da lungimea celui mai scurt drum special de la sursa catre i.
La terminarea algoritmului, toate varfurile grafului, cu exceptia unuia, sunt in S. Din proprietatea
precedenta, rezulta ca algoritmul lui Dijkstra functioneaza corect.
Daca dorim sa aflam nu numai lungimea celor mai scurte drumuri, dar si pe unde trec ele, este
suficient sa adaugam un tablou P[2 .. n], unde P[v] contine numarul nodului care il precede pe v in cel
mai scurt drum. Pentru a gasi drumul complet, nu avem decat sa urmarim, in tabloul P, varfurile prin care
trece acest drum, de la destinatie la sursa. Modificarile in algoritm sunt simple:
– initializeaza P[i] cu 1, pentru 2 ≤ i ≤ n
– continutul buclei for cea mai interioara se inlocuieste cu
if D[w] > D[v] + L[v, w] then D[w] ← D[v] + L[v, w]
P[w] ← v
– bucla repeat se executa de n -1 ori
Sa presupunem ca aplicam algoritmul Dijkstra asupra unui graf cu n varfuri si m muchii.
Initializarea necesita un timp in O(n). Alegerea lui v din bucla repeat presupune parcurgerea tuturor
varfurilor continute in C la iteratia respectiva, deci a n -1, n -2, ..., 2 varfuri, ceea ce necesita in total un
timp in O(n2). Bucla for interioara efectueaza n-2, n-3, ..., 1 iteratii, totalul fiind tot in O(n2). Rezulta ca
algoritmul Dijkstra necesita un timp in O(n2).
Incercam sa imbunatatim acest algoritm. Vom reprezenta graful nu sub forma matricii de
adiacenta L, ci sub forma a n liste de adiacenta, continand pentru fiecare varf lungimea muchiilor care
pleaca din el. Bucla for interioara devine astfel mai rapida, deoarece putem sa consideram doar varfurile
w adiacente lui v. Aceasta nu poate duce la modificarea ordinului timpului total al algoritmului, daca nu
reusim sa scadem si ordinul timpului necesar pentru alegerea lui v din bucla repeat. De aceea, vom tine
varfurile v din C intr-un min-heap, in care fiecare element este de forma (v, D[v]), proprietatea de min-
heap referindu-se la valoarea lui D[v]. Numim algoritmul astfel obtinut Dijkstra-modificat. Sa il analizam
in cele ce urmeaza.
Initializarea min-heap-ului necesita un timp in O(n). Instructiunea “C ← C \ {v}” consta in
extragerea radacinii min-heap-ului si necesita un timp in O(log n). Pentru cele n–2 extrageri este nevoie
de un timp in O(n log n).
Pentru a testa daca “D[w] > D[v]+L[v, w]”, bucla for interioara consta acum in inspectarea
fiecarui varf w din C adiacent lui v. Fiecare varf v din C este introdus in S exact o data si cu acest prilej
sunt testate exact muchiile adiacente lui; rezulta ca numarul total de astfel de testari este de cel mult m.
Daca testul este adevarat, trebuie sa il modificam pe D[w] si sa operam un percolate cu w in min-heap,
ceea ce necesita din nou un timp in O(log n). Timpul total pentru operatiile percolate este deci in
O(m log n).
In concluzie, algoritmul Dijkstra-modificat necesita un timp in O(max(n, m) log n). Daca graful
este conex, atunci m ≥ n si timpul este in O(m log n). Pentru un graf rar este preferabil sa folosim
algoritmul Dijkstra-modificat, iar pentru un graf dens algoritmul Dijkstra este mai eficient.

46
Este usor de observat ca, intr-un graf G neorientat conex, muchiile celor mai scurte drumuri de la
un varf i la celelalte varfuri formeaza un arbore partial al celor mai scurte drumuri pentru G. Desigur,
acest arbore depinde de alegerea radacinii i si el difera, in general, de arborele partial de cost minim al lui
G.
Problema gasirii celor mai scurte drumuri care pleaca din acelasi punct se poate pune si in cazul
unui graf neorientat.

Euristica greedy

Pentru anumite probleme, se poate accepta utilizarea unor algoritmi despre care nu se stie daca
furnizeaza solutia optima, dar care furnizeaza rezultate “acceptabile”, sunt mai usor de implementat si
mai eficienti decat algoritmii care dau solutia optima. Un astfel de algoritm se numeste euristic.
Una din ideile frecvent utilizate in elaborarea algoritmilor euristici consta in descompunerea
procesului de cautare a solutiei optime in mai multe subprocese succesive, fiecare din aceste subprocese
constand dintr-o optimizare. O astfel de strategie nu poate conduce intotdeauna la o solutie optima,
deoarece alegerea unei solutii optime la o anumita etapa poate impiedica atingerea in final a unei solutii
optime a intregii probleme; cu alte cuvinte, optimizarea locala nu implica, in general, optimizarea globala.
Regasim, de fapt, principiul care sta la baza metodei greedy. Un algoritm greedy, despre care nu se poate
demonstra ca furnizeaza solutia optima, este un algoritm euristic.
Vom da doua exemple de utilizare a algoritmilor greedy euristici.

Colorarea unui graf

Fie G = <V, M> un graf neorientat, ale carui varfuri trebuie colorate astfel incat oricare doua
varfuri adiacente sa fie colorate diferit. Problema este de a obtine o colorare cu un numar minim de culori.
Folosim urmatorul algoritm greedy: alegem o culoare si un varf arbitrar de pornire, apoi consideram
varfurile ramase, incercand sa le coloram, fara a schimba culoarea. Cand nici un varf nu mai poate fi
colorat, schimbam culoarea si varful de start, repetand procedeul.

Figura 6 Un graf care va fi colorat.

Daca in graful din Figura 11.6 pornim cu varful 1 si il coloram in rosu, mai putem colora tot in
rosu varfurile 3 si 4. Apoi, schimbam culoarea si pornim cu varful 2, colorandu-l in albastru. Mai putem
colora cu albastru si varful 5. Deci, ne-au fost suficiente doua culori. Daca coloram varfurile in ordinea 1,
5, 2, 3, 4, atunci se obtine o colorare cu trei culori.
Rezulta ca, prin metoda greedy, nu obtinem decat o solutie euristica, care nu este in mod necesar
solutia optima a problemei. De ce suntem atunci interesati intr-o astfel de rezolvare? Toti algoritmii
cunoscuti, care rezolva optim aceasta problema, sunt exponentiali, deci, practic, nu pot fi folositi pentru
cazuri mari. Algoritmul greedy euristic propus furnizeaza doar o solutie “acceptabila”, dar este simplu si
eficient.
Un caz particular al problemei colorarii unui graf corespunde celebrei probleme a colorarii
hartilor: o harta oarecare trebuie colorata cu un numar minim de culori, astfel incat doua tari cu frontiera
comuna sa fie colorate diferit. Daca fiecarui varf ii corespunde o tara, iar doua varfuri adiacente
reprezinta tari cu frontiera comuna, atunci hartii ii corespunde un graf planar, adica un graf care poate fi
47
desenat in plan fara ca doua muchii sa se intersecteze. Celebritatea problemei consta in faptul ca, in toate
exemplele intalnite, colorarea s-a putut face cu cel mult 4 culori. Aceasta in timp ce, teoretic, se putea
demonstra ca pentru o harta oarecare este nevoie de cel mult 5 culori.
Problema colorarii unui graf poate fi interpretata si in contextul planificarii unor activitati. De
exemplu, sa presupunem ca dorim sa executam simultan o multime de activitati, in cadrul unor sali de
clasa. In acest caz, varfurile grafului reprezinta activitati, iar muchiile unesc activitatile incompatibile.
Numarul minim de culori necesare pentru a colora graful corespunde numarului minim de sali necesare.

Problema comis-voiajorului

Se cunosc distantele dintre mai multe orase. Un comis-voiajor pleaca dintr-un oras si doreste sa se
intoarca in acelasi oras, dupa ce a vizitat fiecare din celelalte orase exact o data. Problema este de a
minimiza lungimea drumului parcurs. Si pentru aceasta problema, toti algoritmii care gasesc solutia
optima sunt exponentiali.
Problema poate fi reprezentata printr-un graf neorientat, in care oricare doua varfuri diferite ale
grafului sunt unite intre ele printr-o muchie, de lungime nenegativa. Cautam un ciclu de lungime minima,
care sa se inchida in varful initial si care sa treaca prin toate varfurile grafului.
Conform strategiei greedy, vom construi ciclul pas cu pas, adaugand la fiecare iteratie cea mai
scurta muchie disponibila cu urmatoarele proprietati:
 nu formeaza un ciclu cu muchiile deja selectate (exceptand pentru ultima muchie aleasa,
care completeaza ciclul)
 nu exista inca doua muchii deja selectate, astfel incat cele trei muchii sa fie incidente in
acelasi varf

La: 2 3 4 5 6

De la:
1 3 10 11 7 25
2 6 12 8 26
3 9 4 20
4 5 15
5 18
Tabelul 11.4 Matricea distantelor pentru problema comis-voiajorului.

De exemplu, pentru sase orase a caror matrice a distantelor este data in Tabelul 11.4, muchiile se
aleg in ordinea: {1, 2}, {3, 5}, {4, 5}, {2, 3}, {4, 6}, {1, 6} si se obtine ciclul (1, 2, 3, 5, 4, 6, 1) de
lungime 58. Algoritmul greedy nu a gasit ciclul optim, deoarece ciclul (1, 2, 3, 6, 4, 5, 1) are lungimea 56.

48
STUDII DE CAZ – APLICAŢII

49
1. Să se determine toate numerele perechile de numere gemene pana la o anumita valoare n. Două
numere sunt gemene dacă sunt ambele prime şi diferenţa dintre cel mai mare şi cel mai mic este 2.
Private Sub CommandButton1_Click()
Dim rad As Integer, n As Integer, p As Integer, i As Integer, j
As Integer
cit_n "n = ", n
For i = 3 To n
p = 1
rad = Int(Sqr(i + 2))
For j = 2 To Int(rad)
If i Mod j = 0 Or (i + 2) Mod j = 0 Then
prim = 0
j = Int(rad)
End If
Next
If p Then
MsgBox "(" + Str$(i) + "," + Str$(i + 2) + ")" + Chr(13)
End If
Next

End Sub

2. Să se citească o valoare naturala n cu valori cuprinse intre 1 şi 100.


Sub cit_n(mes As String, nnn As Integer)
Do
nnn = InputBox(mes, y)
Loop Until n > 0 And n < 100
End Sub

3. Citirea unui vector cu n componente


Sub cit_date(mes As String, n As Integer, a As vector)
For i = 1 To n
50
a.v(i) = InputBox(mes + "(" + Str$(i) + ")=", y)
Next
End Sub

4. Tipărirea unui tablou cu n componente


Sub tipar(mes As String, n As Integer, a As vector)
sir = ""
For i = 1 To n
sir = sir + Str$(a.v(i)) + ","
Next
MsgBox mes + " " + sir
End Sub

5. Generarea permutărilor utilizănd metoda backtracking


Private Sub CommandButton14_Click()
cit_n "n = ", n
back_perm
End Sub

6. Generarea produsului cartezian a n mulţimi utilizând metoda backtracking


Private Sub CommandButton16_Click()
Dim a As vector
cit_n "n=", n
cit_date "a", n, a
tipar " multimile sunt : ", n, a
back_prod_cart
End Sub

7. Generarea permutărilor utilizănd metoda backtracking


Private Sub CommandButton17_Click()
cit_n "n = ", n
cit_n "p = ", p
back_aranj
End Sub

8. “Problema celor n dame” utilizănd metoda backtracking


Private Sub CommandButton15_Click()
cit_n "n = ", n
back
End Sub

9. Generarea combinărilor (de n luate câte m) utilizănd metoda backtracking


Private Sub CommandButton18_Click()
cit_n "n = ", n
cit_n "p = ", p
back_comb
End Sub

10. Generarea partiţiilor unei mulţimi utilizănd metoda backtracking


Private Sub CommandButton19_Click()
51
cit_n "n=", n
back_partitii
End Sub

11. Căutarea binară utilizând metoda “Divide et Impera” pentru sortarea unui şir de numere
Private Sub CommandButton2_Click()
Dim n As Integer, x As Integer, a As vector
cit_n "n = ", n
cit_date "a", n, a
tipar "sirul dat este : ", n, a
divimp 1, n, a
'MsgBox "Sirul a sortat este"
tipar "Sirul a sortat este", n, a
x = InputBox(" x = ", y)
st = 1
dr = n
l = True
While st <= dr And l = True
pp = (st + dr) / 2
If a.v(pp) = x Then
l = False
MsgBox "numarul x = " + Str$(x) + " se afla printre elementele
vectorului a"
End If
If a.v(pp) < x Then
st = pp + 1
Else
dr = p - 1
End If
Wend
If l = True Then
MsgBox "numarul x = " + Str$(x) + " nu se fala in sir "
End If
End Sub

12. Realizarea unei subrutine pentru sortarea rapidă “Quicksort”


Sub sort(p As Integer, q As Integer, a As vector)
Dim m As Integer
If a.v(p) > a.v(q) Then
m = a.v(p)
a.v(p) = a.v(q)
a.v(q) = m
End If
End Sub

13. Sortarea “Merge-Sort” utilizând metoda “Divide et impera”


Sub interc(p As Integer, q As Integer, m As Integer, a As vector)
Dim b As vector, i, j, k As Integer
i = p
j = m + 1
k = 1
While (i <= m) And (j <= q)
If a.v(i) <= a.v(j) Then
b.v(k) = a.v(i)
52
i = i + 1
k = k + 1
Else
b.v(k) = a.v(j)
j = j + 1
k = k + 1
End If
Wend
If i <= m Then
For j = i To m
b.v(k) = a.v(j)
k = k + 1
Next
Else
For i = j To q
b.v(k) = a.v(i)
k = k + 1
Next
End If
k = 1
For i = p To q
a.v(i) = b.v(k)
k = k + 1
Next
End Sub

14. Sortarea rapidă utilizând metoda “Divide et impera”


Sub divimp(p As Integer, q As Integer, a As vector)
Dim m As Integer
If (q - p) <= 1 Then
sort p, q, a
Else
m = Int((p + q) / 2)
divimp p, m, a
divimp m + 1, q, a
interc p, q, m, a
End If
End Sub

15. Problema colorării hărţilor utilizând metoda backtracking


Private Sub CommandButton20_Click()
Dim mat As matrice
cit_n " n = ", n
cit_mat "a", n, n, mat
tipar_mat "a", n, n, mat
For i = 1 To n
For j = 1 To n
mat.m(j, i) = mat.m(i, j)
Next
Next
back_col
End Sub

53
16. Interclasarea a 2 şiruri ordonate crescător
Private Sub CommandButton3_Click()
Dim n As Integer, x As Integer, a As vector, m As Integer, b As
vector, k As Integer, c As vector
cit_n "n = ", n
cit_date "a", n, a
tipar "sirul dat este : ", n, a
divimp 1, n, a
'MsgBox "Sirul a sortat este"
tipar "Sirul a sortat este", n, a
cit_n "m = ", m
cit_date "a", m, b
tipar "sirul dat este : ", m, b
divimp 1, m, b
'MsgBox "Sirul a sortat este"
tipar "Sirul b sortat este", m, b
i = 1
j = 1
k = 0
While i <= n And j <= m
If a.v(i) < b.v(j) Then
k = k + 1
c.v(k) = a.v(i)
i = i + 1
Else
If a.v(i) = b.v(j) Then
k = k + 1
c.v(k) = a.v(i)
i = i + 1
j = j + 1
Else
k = k + 1
c.v(k) = b.v(j)
j = j + 1
End If
End If
Wend
If i <= n Then
For l = i To n
k = k + 1
c.v(k) = a.v(l)
Next
End If
If j <= m Then
For l = j To m
k = k + 1
c.v(k) = b.v(l)
Next
End If
tipar "A U B = ", k, c
End Sub

17. Sortarea Shell-Sort utilizând metoda Greedy


Private Sub CommandButton4_Click()
Dim n As Integer, k As Integer, a As vector
54
cit_n "n = ", n
cit_date "a", n, a
tipar "sirul dat este : ", n, a
k = n
Do
k = k / 2
Do
b = 1
For i = 1 To n - k
If a.v(i) > a.v(i + k) Then
x = a.v(i)
a.v(i) = a.v(i + k)
a.v(i + k) = x
b = 0
End If
Next
Loop Until Not (b = 0)
Loop Until Not (k <> 1)
'MsgBox "Sirul a sortat este"
tipar "Sirul a sortat este", n, a
End Sub

18. Citirea si scrierea unei matrici pe ecran


Private Sub CommandButton5_Click()
Dim n As Integer, m As Integer, a As matrice, b As matrice, p As
Integer, c As matrice
cit_n "n = ", n
cit_n "m = ", m
cit_mat "a", n, m, a
tipar_mat "a", n, m, a
End Sub

19. Citirea unei matrici de pe dispozitivul de intrare


Sub cit_mat(mes As String, n As Integer, m As Integer, a As
matrice)
For i = 1 To n
For j = 1 To m
a.m(i, j) = InputBox(mes + "(" + Str$(i) + "," + Str$(j) +
")=", y)
Next
Next
End Sub

20. Scrierea unei matrici pe ecran


Sub tipar_mat(mes As String, n As Integer, m As Integer, a As
matrice)
sir = mes + Chr(10)
For i = 1 To n
For j = 1 To m
sir = sir + Str$(a.m(i, j)) + " "
Next
sir = sir + Chr(10)
Next
MsgBox sir
55
End Sub

21. Produsul a două matrici


Private Sub CommandButton6_Click()
Dim n As Integer, m As Integer, a As matrice, b As matrice, p As
Integer, c As matrice
cit_n "n = ", n
cit_n "m = ", m
cit_mat "a", n, m, a
tipar_mat "a", n, m, a
cit_n "p = ", p
'cit_n "m = ", m
cit_mat "b", m, p, b
tipar_mat "m", m, p, b
prod_mat n, m, p, a, b, c
tipar_mat "axb=", n, p, c
End Sub

Sub prod_mat(n As Integer, m As Integer, p As Integer, a As


matrice, b As matrice, c As matrice)
For i = 1 To n
For j = 1 To p
c.m(i, j) = 0
For k = 1 To m
c.m(i, j) = c.m(i, j) + a.m(i, k) * b.m(k, j)
Next
Next
Next
End Sub

22. Programul principal pentru adunarea a două matrici


Private Sub CommandButton7_Click()
Dim n As Integer, m As Integer, a As matrice, b As matrice, p As
Integer, c As matrice
cit_n "n = ", n
cit_n "m = ", m
cit_mat "a", n, m, a
tipar_mat "a", n, m, a
'cit_n "p = ", p
'cit_n "m = ", m
cit_mat "b", n, m, b
tipar_mat "b", n, m, b
ad_mat n, m, a, b, c
tipar_mat "a+b=", n, m, c
End Sub

23. Subrutina pentru adunarea a două matrici


Sub ad_mat(n As Integer, m As Integer, a As matrice, b As matrice,
c As matrice)
For i = 1 To n
For j = 1 To m
c.m(i, j) = a.m(i, j) + b.m(i, j)
Next
Next
56
End Sub

24. Programul principal pentru scăderea a două matrici


Private Sub CommandButton8_Click()
Dim n As Integer, m As Integer, a As matrice, b As matrice, p As
Integer, c As matrice
cit_n "n = ", n
cit_n "m = ", m
cit_mat "a", n, m, a
tipar_mat "a", n, m, a
'cit_n "p = ", p
'cit_n "m = ", m
cit_mat "b", n, m, b
tipar_mat "b", n, m, b
scad_mat n, m, a, b, c
tipar_mat "a-b=", n, m, c
End Sub

25. Subrutina pentru adunarea a două matrici


Sub scad_mat(n As Integer, m As Integer, a As matrice, b As
matrice, c As matrice)
For i = 1 To n
For j = 1 To m
c.m(i, j) = a.m(i, j) - b.m(i, j)
Next
Next
End Sub

26. Programul principal pentru ridicarea unei matrici la o putere p


Private Sub CommandButton9_Click()
Dim n As Integer, m As Integer, a As matrice, b As matrice, p As
Integer, c As matrice, k As Integer
'Sub scad_mat(n As Integer, m As Integer, a As matrice, b As
matrice, c As matrice)
'const t as vector ={0,31,28,31,30,31,30,31,30,31,30,31,30}
cit_n "n = ", n
'cit_n "m = ", m
cit_mat "a", n, n, a
tipar_mat "a", n, n, a
cit_n "putere = ", k
'cit_n "m = ", m
'cit_mat "b", n, m, b
'tipar_mat "b", n, m, b
putere_mat n, a, k, c
tipar_mat "a^p=", n, n, c
End Sub

27. Subprogramul pentru ridicarea unei matrici la o putere p


Sub putere_mat(n As Integer, a As matrice, k As Integer, c As
matrice)
Dim b As matrice, c1 As matrice
For i = 1 To n
For j = 1 To n
57
c.m(i, j) = 0
c1.m(i, j) = 0
Next
Next
For i = 1 To n
c.m(i, i) = 1
c1.m(i, i) = 1
Next
'Next
While k > 0
If k Mod 2 = 1 Then
prod_mat n, n, n, c1, a, c
End If
For i = 1 To n
For j = 1 To n
c1.m(i, j) = c.m(i, j)
'c1.m(i, j) = 0
Next
Next
prod_mat n, n, n, a, a, b
k = Int(k / 2)
For i = 1 To n
For j = 1 To n
a.m(i, j) = b.m(i, j)
'c1.m(i, j) = 0
Next
Next
Wend
For i = 1 To n
For j = 1 To n
c.m(i, j) = c1.m(i, j)
'c1.m(i, j) = 0
Next
Next
End Sub

28. Subrutina de iniţializare a stivei pentru metoda backtracking


Sub init(k As Integer, st As stiva)
st.ss(k) = 0
End Sub

29. Subrutina successor pentru “problema celor n dame”


Sub succesor(am_suc As Boolean, st As stiva, k As Integer)
If st.ss(k) < n Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

30. Subrutina successor pentru generarea combinărilor


Sub succesor_c(am_suc As Boolean, st As stiva, k As Integer)
If st.ss(k) < n - p + k Then
58
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

31. Subrutina succesor pentru problema “produsului cartezian a n mulţimi” utilizând metoda
backtracking
Sub succesor_prod(am_suc As Boolean, st As stiva, k As Integer)
If st.ss(k) < a.v(k) Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

32. Subrutina successor pentru colorarea hărţilor


Sub succesor_col(am_suc As Boolean, st As stiva, k As Integer)
If st.ss(k) < 4 Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

33. Subrutina valid pentru “problema celor n dame”


Sub valid(ev As Boolean, st As stiva, k As Integer)
ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) Or (Abs(st.ss(i) - st.ss(k)) = Abs(k -
i)) Then
ev = False
End If
Next
End Sub

34. Subrutina valid pentru colorarea hărţilor


Sub valid_col(ev As Boolean, st As stiva, k As Integer)
ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) And (mat.m(i, k) = 1) Then
ev = False
End If
Next
End Sub

Sub valid_c(ev As Boolean, st As stiva, k As Integer)


Dim i As Integer
ev = True
For i = 1 To k - 1
59
If (st.ss(i) = st.ss(k)) Then
ev = False
End If
Next
If k > 1 Then
If st.ss(k) < st.ss(k - 1) Then
ev = False
End If
End If
End Sub

35. Subrutina valid pentru “produs cartezian a n mulţimi”


Sub valid_prod(ev As Boolean, st As stiva, k As Integer)
ev = True
End Sub

36. Subrutina soluţie pentru generarea permutărilor


Function solutie(k As Integer) As Boolean
If k = n Then
solutie = True
Else
solutie = False
End If
End Function

37. Subrutina soluţie pentru generarea aranjamentelor sau combinărilor


Function solutie1(k As Integer) As Boolean
If k = p Then
solutie1 = True
Else
solutie1 = False
End If
End Function

38. Subrutina tipărire pentru “problema celor n dame”


Sub tiparr()
Dim i As Integer, b As String
b = " "
For i = 1 To n
b = b + "(" + Str$(i) + "," + Str$(st.ss(i)) + "),"
Next
MsgBox b
End Sub

39. Subrutina tipărire pentru “colorarea hărţilor”


Sub tipar_col()
Dim i As Integer, b As String
b = " "
For i = 1 To n
b = b + "Tara = " + Str$(i) + "; culoarea " + Str$(st.ss(i)) + " "
Next
60
MsgBox b
End Sub

40. Subrutina back pentru “problema celor n dame”


Sub back()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor am_suc, st, k
If am_suc = True Then
valid ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tiparr
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

41. Programul principal pentru “problema celor n dame”


Sub Button2_Click()
n = InputBox("n=", ib_title)
back
End Sub

42. Subrutina back pentru “generarea permutărilor”


Sub back_perm()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor am_suc, st, k
If am_suc = True Then
valid1 ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_r
Else
k = k + 1
init k, st
End If
Else
61
k = k - 1
End If
Wend
End Sub

43. Subrutina back pentru “generarea aranjamentelor”


Sub back_aranj()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor am_suc, st, k
If am_suc = True Then
valid1 ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie1(k) Then
tipar_rr
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

44. Subrutina valid pentru metoda backtracking


Sub valid1(ev As Boolean, st As stiva, k As Integer)
ev = True
For i = 1 To k - 1
If (st.ss(i) = st.ss(k)) Then
ev = False
End If
Next
End Sub

45. Subrutina tipar pentru metoda backtracking


Sub tipar_r()
Dim i As Integer, b As String
b = " "
For i = 1 To n
b = b + Str$(st.ss(i)) + ","
Next
MsgBox b
End Sub

46. Subrutina tipar pentru metoda backtracking


Sub tipar_rr()
Dim i As Integer, b As String
62
b = " "
For i = 1 To p
b = b + Str$(st.ss(i)) + ","
Next
MsgBox b
End Sub

47. Subrutina back pentru “generarea combinărilor”


Sub back_comb()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor_c am_suc, st, k
If am_suc = True Then
valid_c ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie1(k) Then
tipar_rr
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

48. Subrutina back pentru “generarea produsului cartezian a n multimi”


Sub back_prod_cart()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor_prod am_suc, st, k
If am_suc = True Then
valid_prod ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_r
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
63
End Sub

49. Subrutina back pentru “generarea partiţiilor unei mulţimi”


Sub back_partitii()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor_part am_suc, st, k
If am_suc = True Then
valid_prod ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_part
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub

50. Subrutina tiparire pentru problema “generare partiţii” a unei mulţimi


Sub tipar_part()
Dim i As Integer, max As Integer, j As Integer, sir As String
sir = ""
max = st.ss(1)
For i = 2 To n
If max < st.ss(i) Then
max = st.ss(i)
End If
Next
sir = " PARTITII "
For j = 1 To max
For i = 1 To n
If st.ss(i) = j Then
sir = sir + Str$(i) + " "
End If
Next
sir = sir + Chr(10)
Next
MsgBox sir
End Sub

51. Subrutina succesor pentru problema “generare partiţii” a unei mulţimi


Sub succesor_part(am_suc As Boolean, st As stiva, k As Integer)
Dim i As Integer, max As Integer
If k = 1 Then
max = 1
64
Else
max = st.ss(1)
For i = 2 To k - 1
If max < st.ss(i) Then
max = st.ss(i)
End If
Next
End If
If st.ss(k) < max + 1 And st.ss(k) < k Then
am_suc = True
st.ss(k) = st.ss(k) + 1
Else
am_suc = False
End If
End Sub

52. Subrutina back pentru “colorarea hărţilor”


Sub back_col()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor_col am_suc, st, k
If am_suc = True Then
valid_col ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_col
Else
k = k + 1
init k, st
End If
Else
k = k - 1
End If
Wend
End Sub
Public s As String

53. Funcţia pentru a verifica dacă un număr natural n este prim sau nu
Function prim(n As Integer) As Boolean
b = True
For i = 2 To Int(Sqr(n))
If n Mod i = 0 Then
b = False
i = Int(Sqr(n))
End If
Next
prim = b
End Function

54. Programul principal pentru inversarea unui număr natural n


65
Sub buton1_Click()
Dim n As Integer, ninv As Integer, n1 As Integer, sir As String
Do
n = InputBox(" n = ", y)
Loop Until n > 0
n1 = n
ninv = 0
sir = ""
While n <> 0
sir = sir + LTrim(RTrim(Str$(n Mod 10)))
ninv = ninv * 10 + n Mod 10
n = Int(n / 10)
Wend
MsgBox " numarul initial este : " + Str$(n1) + " numarul inversat
este: " + sir
End Sub

55. Algoritmul lui Euclid pentru calcularea CMMDC a două numere naturale pozitive
Private Sub Buton10_Click()
Dim a As Integer, b As Integer, c As Integer
Do
a = InputBox("a = ", y)
b = InputBox("b = ", y)
a1 = a
b1 = b
Loop Until a > 0 And b > 0 And a > b
c = euclid2(a, b)
If c = 1 Then
MsgBox " nr. sunt prime intre ele (" + Str$(a1) + "," + Str$(b1)
+ ")"
Else
MsgBox "Cmmdc (" + Str$(a1) + "," + Str$(b1) + ")=" +
Str$(euclid2(a, b))
End If
End Sub

56. Sortarea unui sir cu n componente utilizând metoda bulelor


Private Sub Buton11_Click()
Dim n As Integer, a As vector
cit_n "n = ", n
cit_date "a", n, a
tipar "vectorul initial a este ", n, a
bule n, a
tipar "vectorul a sortat este : ", n, a
End Sub

57. Subrutina pentru sortarea prin metoda bulelor


Sub bule(n As Integer, a As vector)
Do
k = 0
For i = 1 To n - 1
If a.v(i) > a.v(i + 1) Then
x = a.v(i)
66
a.v(i) = a.v(i + 1)
a.v(i + 1) = x
k = 1
End If
Next
Loop Until k = 0
End Sub

58. Sortarea unui sir cu n componente utilizând metoda selecţiei directe


Private Sub Buton12_Click()
Dim n As Integer, a As vector
cit_n "n = ", n
cit_date "a", n, a
tipar "vectorul initial a este ", n, a
selectie n, a
tipar "vectorul a sortat este : ", n, a
End Sub

59. Subrutina pentru sortarea prin metoda selecţiei directe


Sub selectie(n As Integer, a As vector)
For i = 1 To n - 1
min = a.v(i)
k = i
For j = i + 1 To n
If min > a.v(j) Then
min = a.v(j)
k = j
End If
Next
If k <> i Then
x = a.v(i)
a.v(i) = a.v(k)
a.v(k) = x
End If
Next
End Sub

60. Sortarea unui sir cu n componente utilizând metoda prin numărare


Private Sub Buton14_Click()
Dim n As Integer, a As vector
cit_n "n = ", n
cit_date "a", n, a
tipar "vectorul initial a este ", n, a
numarare n, a
tipar "vectorul a sortat este : ", n, a
End Sub

61. Suma cifrelor unui număr natural dat n


Sub buton2_Click()
Dim n As Integer, s As Long, n1 As Integer
Do
n = InputBox(" n = ", y)
Loop Until n > 0
67
n1 = n
s = 0
While n <> 0
s = s + n Mod 10
n = Int(n / 10)
Wend
MsgBox " suma cifrelor numarului n = " + Str$(n1) + " este : " +
Str$(s)
End Sub

62. Verificarea unui numar natural n daca este prim sau nu


Sub buton3_Click()
Dim n As Integer, s As Long, n1 As Integer
Do
n = InputBox(" n = ", y)
Loop Until n > 0
n1 = n
b = True
For i = 2 To Int(Sqr(n))
If n Mod i = 0 Then
b = False
i = Int(Sqr(n))
End If
Next
If b = True Then
MsgBox "numarul n = " + Str$(n) + " este prim"
Else
MsgBox "numarul n = " + Str$(n) + " nu este prim"
End If
End Sub

63. Determinarea numerelor prime mai mici sau egale cu n utilizând metoda directă
Sub buton4_Click()
Dim n As Integer, s As Long, n1 As Integer, i As Integer
Do
n = InputBox(" n = ", y)
Loop Until n > 0
n1 = n
If n = 2 Then
MsgBox "numerele prime sunt : 2"
Else
sir = "2,"
i = 3
While i <= n
If prim(i) = True Then
sir = sir + Str$(i) + ","
End If
i = i + 2
Wend
End If
MsgBox "numere prime sunt : " + sir
End Sub

64. Ciurul lui Eratostene

68
Sub buton5_Click()
Dim n As Integer, a As vector, sir As String
Do
n = InputBox(" n = ", y)
Loop Until n > 0
For i = 1 To n
a.v(i) = i
Next
For i = 2 To Int(Sqr(n))
If a.v(i) <> 0 Then
j = 2 * i
While j <= n
j = j + i
a.v(j) = 0
Wend
End If
Next
sir = ""
For i = 2 To n
If a.v(i) <> 0 Then
sir = sir + Str$(i) + ","
End If
Next
MsgBox "Numerele prime sunt : " + sir
End Sub

65. Descompunerea unui numar in factori primi


Sub buton6_Click()
Dim n As Integer, a As vector, sir As String, n1 As Integer
Do
n = InputBox(" n = ", y)
Loop Until n > 0
i = 2
n1 = n
l = 0
sir = ""
Do
fm = 0
While n Mod i = 0
fm = fm + 1
l = 1
n = Int(n / i)
Wend
If fm <> 0 Then
sir = sir + Str$(i) + "^" + Str$(fm) + "*"
End If
i = i + 1
Loop Until n = 1
If l = 0 Then
sir = Str$(n) + "^1"
End If
MsgBox Str$(n1) + "=" + sir
End Sub

66. Scrierea unui număr ca suma a două cuburi

69
Sub buton7_Click()
Dim n As Integer, a As vector, sir As String, n1 As Integer
Do
n = InputBox(" n = ", y)
Loop Until n > 0
n1 = n
For n = 1 To n1
Max = Int(n / 2)
nr = 0
For i = 1 To Max
For j = i To Max
If i * i * i + j * j * j = n Then
If nr = 0 Then
i1 = i
j1 = j
Else
i2 = i
j2 = j
End If
nr = nr + 1
End If
Next
Next
If nr > 1 Then
MsgBox Str$(n) + "=" + Str$(i1) + "^" + Str$(j1) + "+" +
Str$(i2) + "^" + Str$(j2)
End If
Next
End Sub

67. CMMDC a două numere utilizând recursivitatea


Sub buton8_Click()
Dim a As Integer, b As Integer, c As Integer
Do
a = InputBox("a = ", y)
b = InputBox("b = ", y)
a1 = a
b1 = b
Loop Until a > 0 And b > 0 And a > b
c = euclid(a, b)
If c = 1 Then
MsgBox " nr. sunt prime intre ele (" + Str$(a1) + "," + Str$(b1)
+ ")"
Else
MsgBox "Cmmdc (" + Str$(a1) + "," + Str$(b1) + ")=" +
Str$(euclid(a, b))
End If
End Sub

68. Funcţia euclid


Function euclid(a As Integer, b As Integer) As Integer
Dim r As Integer
Do
r = a Mod b
MsgBox r
70
a = b
b = r
Loop Until Not (r = 0 And r = 1)
If r = 1 Then
euclid = 1
Else
euclid = a
End If
End Function

69. CMMDC a două numere utilizând scăderi repetate


Private Sub Buton9_Click()
Dim a As Integer, b As Integer, c As Integer
Do
a = InputBox("a = ", y)
b = InputBox("b = ", y)
a1 = a
b1 = b
Loop Until a > 0 And b > 0 And a > b
c = euclid1(a, b)
If c = 1 Then
MsgBox " nr. sunt prime intre ele (" + Str$(a1) + "," + Str$(b1)
+ ")"
Else
MsgBox "Cmmdc (" + Str$(a1) + "," + Str$(b1) + ")=" +
Str$(euclid1(a, b))
End If
End Sub

70. Funcţia Euclid utilizând scăderi repetate


Function euclid1(a As Integer, b As Integer) As Integer
If a > b Then
euclid1 = euclid1(a - b, b)
Else
If a < b Then
euclid1 = euclid1(a, b - a)
Else
euclid1 = a
End If
End If
End Function

71. Funcţia Euclid utilizând scăderi repetate


Function euclid2(a As Integer, b As Integer) As Integer
If b = 0 Then
euclid2 = a
Else
euclid2 = euclid2(b, a Mod b)
End If
End Function

72. x ^ y utilizând un număr minim de înmulţiri

71
Sub Button15_Click()
Dim x As Integer, y As Integer, z As Integer, t As String, bb As
vector
Dim xx As Integer
Do
x = InputBox("a=", ib_title)
y = InputBox("b=", ib_title)
Loop Until (x > 0) And (y > 0) And (x >= y)
baza1 x, y, bb, xx
t = ""
MsgBox "n = " + Str$(xx)
For z = xx To 1 Step -1
t = t + Str$(bb.v(z))
Next
MsgBox t
End Sub

73. Verifică dacă un număr natural este palindrome sau nu


Sub Button16_Click()
Dim n As Long, m As Long
Do
n = InputBox("n=", ib_title)
Loop Until (n > 0)
m = n
If palindrom(n) = True Then
MsgBox "n=" + Str$(m) + " este plaindrom"
Else
MsgBox "n=" + Str$(m) + " nu este plaindrom"
End If
End Sub

74. Baza la exponent


Sub Button17_Click()
Dim x As Double, y As Byte, z As Double, t As Byte
Do
x = InputBox("baza=", ib_title)
y = InputBox("exponent=", ib_title)
Loop Until (x > 0) And (y > 0)
z = putere(x, y, t)
MsgBox Str$(z) + " " + Str$(t - 1)
End Sub

75. Quicksort
Sub Button18_Click()
Dim n As Integer, a As vector
cit_n "n = ", n
cit_date "a", n, a
'MsgBox "Sirul a este"
tipar "Sirul a este", n, a
divimp 1, n, a
'MsgBox "Sirul a sortat este"
tipar "Sirul a sortat este", n, a
End Sub

72
76. Minimul dintr-un şir de numere utilizând divide et impera
Sub Button19_Click()
Dim n As Integer, a As vector
cit_n "n=", n
cit_date "a", n, a
'MsgBox "Sirul a este"
tipar "sirul dat este ", n, a
MsgBox "minimul in Sirul a este" + Str$(minim(1, n))
End Sub

77. Turnurile din Hanoi


Sub Button20_Click()
Dim n As Integer, a As sir, b As sir, c As sir
d = ""
a.s = "A"
b.s = "B"
c.s = "C"
n = InputBox("n=", ib_title)
hanoi n, a, b, c
MsgBox d
End Sub

78. Subrutina Hanoi


Sub hanoi(n As Integer, a As sir, b As sir, c As sir)
If n = 1 Then
d = d + "(" + a.s + "->" + b.s + "),"
Else
hanoi n - 1, a, c, b
d = d + "(" + a.s + "->" + b.s + "),"
hanoi n - 1, c, b, a
End If
End Sub

79. Subrutina back pentru permutări


Sub back_perm()
Dim k As Integer
k = 1
init k, st
While k > 0
Do
succesor am_suc, st, k
If am_suc = True Then
valid1 ev, st, k
End If
Loop Until (Not am_suc) Or (am_suc And ev)
If am_suc Then
If solutie(k) Then
tipar_r
Else
k = k + 1
init k, st
End If

73
Else
k = k - 1
End If
Wend
End Sub

80. Calculul sumei 1-1,1-1-1,………….,1-1-1-1-1-1-1…….-1


Private Sub Buttton3_Click()
Dim n As Integer, ss As String
cit_n "n = ", n
ss = ""
i = 0
j = 1
While (i < n)
ss = ss + " 1"
i = i + 1
k = 1
While k <= j And i < n
ss = ss + " -1"
i = i + 1
k = k + 1
Wend
j = j + 1
Wend
MsgBox ss
End Sub

74

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