Sunteți pe pagina 1din 82

CAPITOLUL 1

INTRODUCERE

Studiul algoritmilor are la bază proiectarea foarte bună a structurilor de date şi a


metodelor care vor fi folosite pentru rezolvarea problemelor. Proiectarea algoritmilor este
strâns legată de matematică, dar în acest caz trebuie să ne concentrăm mai mult asupra
metodelor utilizate şi mai puţin asupra demonstrării matematice a acestora.
În continuare, vom utiliza pentru descrierea algoritmilor un limbaj universal, denumit
pseudocod. Pornind de la această descriere, utilizând structuri de date şi instrucţiuni specifice
unui limbaj, în acest caz limbajul Visual Basic, se vor implementa aceşti algoritmi într-un
mediu de programare. Bineânţeles că aceşti algoritmi pot fi implementaţi în orice alt limbaj de
programare, cum ar fi: limbajul de asamblare I80X86, limbajul C, limbajul Java etc.
Înţelegerea algoritmilor nu înseamnă în nici un caz învăţarea unor metode particulare
pentru câteva tipuri de probleme. Acest curs va prezenta nu numai detalii referitoare la
anumiţi algoritmi clasici, ci şi stiluri şi şabloane care pot fi folosite în situaţii complet noi. De
asemenea, în acest curs se vor prezenta metode care ajută la facerea distincţiei între
problemele care pot fi soluţionate şi cele care nu pot fi.
Pentru început vom vedea ce înseamnă a fi un bun programator. Timoty Budd
(profesor la Oregon State University) dă următoarea definiţie: “Un bun programator trebuie
să fie înzestrat cu tehnică, experienţă, capacitate de abstractizare, logică, inteligenţă,
creativitate şi talent”.
Să vedem ce înseamnă fiecare termen din această definiţie:
1. Tehnica – este desigur o calitate ce poate fi, şi este, dobîndită doar prin aplicarea
asiduă (conform proverbului: “exerciţiul îl face pe maestru”) în activitatea
concretă de programare a tehnicilor de programare învăţate şi asimilate de către
programator în timpul formării sale profesionale. Tehnica de programare înseamnă
atât studiu într-o facultate de profil cât şi studiu individual.
2. Experienţa – este perechea geamănă a calităţii de mai sus, fără însă a se exclude
una pe cealaltă. Nu vom mai repeta cum şi în ce condiţii poate fi ea obţinută ci
vom deduce următoarea consecinţa imediată : nici un programator începător nu
poate fi numit bun programator întrucît el nu a avut cînd (adică timpul necesar )
să dobîndească ambele calităţi. Este binecunoscut faptul că o rubrică importantă
ce se cere completată la angajare sau la schimbarea locului de muncă este
experienţa de programare în ani. Se consideră în general că experienţa apare abia
după minimum doi ani de programare. Acest fapt nu trebuie privit ca o descurajare
pentru cei mai tineri programatori ci mai degrabă ca pe un motiv de ambiţionare şi
ca o invitaţie la rapidă autoperfecţionare.
3. Abstractizarea – este o trăsătură a intelectului uman şi constituie un dat al oricărui
om normal, dar din păcate este o însuşire prea puţin dezvoltată şi prea puţin
folosită de oamenii obişnuiţi. Ea constă din capacitatea de a extrage din context, de
a vedea dincolo de suprafaţa imediată şi de a putea sesiza structura – scheletul ce
susţine întreaga reţea de detalii ale unei probleme generale. Pentru a fi un bun
programator această calitate trebuie să fie net amplificată faţă de “normal” întrucît
stă la baza oricărui proces de analiză şi modelare a problemelor, cît şi la baza
procesului de proiectare a soluţiilor generale. Absenţa sau mai exact atrofierea
acestei capacităţi se constată practic la studenţi prin incapacitatea de a înţelege sau
de a asimila explicaţii, demonstraţii sau modele abstracte (simplu spus, o acută şi
permanentă “lipsă de chef” atunci cînd sînt atinse anumite subiecte ce nu mai au
contact direct cu realitatea concretă, imediată – adică subiecte abstracte ). Metoda
pentru a recăpăta sau a amplifica această capacitate este de a face cît mai des uz de
ea, adică de a o exersa mereu (conform zicalei “funcţia creează organul”) într-un
domeniu particular, susţinut de o motivaţie personală puternică. Altfel spus,
capacitatea noastră de abstractizare se va amplifica dacă vom încerca găsirea de
soluţii la problemele dintr-unul din domeniile noastre preferate, pentru că
rezolvarea acestora va fi automotivată, făcută “cu chef” şi va prezenta o doză
sporită de atractivitate.
4. Logica – este o altă calitate intrinsecă a oricărui intelect sănătos. Ea este absolut
necesară atît pentru a putea folosi mecanismele mentale de deducţie şi inducţie
logică, cît şi pentru a putea înţelege uşor, dar în acelaşi timp corect, cursul – firul
roşu al unei demonstraţii sau al unui raţionament întins pe mai multe pagini.
Asemenea tuturor calităţilor intrinseci existente în stare potenţială, antrenarea şi
amplificarea acesteia se face prin exerciţiu repetat, prin folosirea ei în mod curent.
Din păcate, doar prin rezolvarea de integrame nu se ajunge la amplificarea
logicii…
5. Inteligenţa – este una din cele mai de preţ calităţi intrinseci ale intelectului uman.
În cîteva cuvinte, fără a avea pretenţia de a da prin acestea o definiţie, prin
inteligenţă înţelegem capacitatea de a face (de a stabili) conexiuni sau legături noi
şi folositoare (din latinescul inter-legere) între idei, cunoştinţe sau informaţii
“aparent fără legătură”. Faţă de logică, pe care o considerăm ca fiind o calitate
bazală, inteligenţa este o calitate ce se “întinde pe verticala” intelectului şi are în
plus trăsătura de a fi mult mai dinamică şi mai mobilă (chiar fulgerător de rapidă)
în acţiune. Pentru cultivarea, amplificarea şi cizelarea acestei calităţi este nevoie de
“punerea ei la lucru” cît mai des şi pe durate tot mai mari de timp. Insatisfacţia
obţinerii unor rezultate rapide sau chiar imediate este un obstacol ce poate fi
depăşit relativ uşor prin antrenarea inteligenţei pe un “teren” cunoscut şi accesibil,
adică în domeniul preferat de interes. În acest fel există siguranţa de a fi susţinut
de atracţia sporită pentru acel domeniu particular ceea ce va conduce prin efort
perseverent (dar susţinut de această dată cu pasiune!) la apariţia rezultatelor
aşteptate şi, implicit, a satisfacţiei.
6. Creativitatea – este o calitate intrinsecă nu numai intelectului uman ci însăşi vieţii
în general. Ea constă, în ultimă instanţă, în capacitatea de a face (de a produce)
ceva cu adevărat nou şi original. De aceea am putea afirma că toate organismele
vii, prin capacitatea lor de a se opune entropiei, creează mereu ordine din
dezordine aducînd în acest fel ceva nou, neaşteptat. Ceea ce se aşteaptă însă de la
un bun programator nu este doar acest tip de creativitate (gen: adaptare
inconştientă şi instinctivă) ci o creativitate conştientă, responsabilă, reflectată în
adaptarea soluţiilor existente sau chiar inventarea altora noi.
7. Talentul – este singura calitate ce nu poate fi cultivată şi amplificată. În
accepţiunea sa obişnuită, prin talent se înţelege o sumă de înzestrări native sau o
predispoziţie personală pentru un anumit domeniu. Existenţa talentului este
percepută de cel în cauză ca uşurinţă – abilitate - dexteritate de a învăţa, asimila
şi aplica toate cunoştinţele domeniului respectiv, abilitate ce este simţită de cel
"talentat" ca un fel de “ceva în plus” în comparaţie cu capacităţile celor din jur.
Din păcate, în accepţiunea comună se crede că talentul este calitatea suficientă care
permite oricui atingerea cu siguranţă a calificativului bun programator, concepţie
care este infirmată de orice programator cu experienţă. Asta nu înseamnă că lipsa
talentului în programare este permisă pentru atingerea acestui nivel, însă efortul,
tenacitatea şi răbdarea existente în “cantităţi” mult sporite într-o asemenea situaţie
de ne-înzestrare cu talent vor permite o apropiere sigură de acest calificativ. Din

2
păcate, lipsa talentului va apărea la început sub forma unei insatisfacţii interioare şi
ca o impresie acută că lipsesc rezultatele. Reamintim că însăşi cuvîntul facultate
are la origine sensul de capacitate, potenţialitate, înzestrare. Deci, normal ar fi ca
alegerea unui student pentru frecventarea cursurilor unei Facultăţi să fi fost făcută
ţinînd cont de aptitudinile şi abilităţile celui în cauză, descoperite în prelabil, adică
să se dovedească talentat pentru domeniul ales. Acest lucru este cu atît mai
important în cazul optării pentru învăţarea programării, cunoscută fiind ca o
specializare complexă şi solicitantă.

Reluînd în sinteză ideile prezentate, putem spune că:


• Pentru a fi un bun programator trebuie să fie prezente următorele şapte calităţi într-o
formă activă, dinamică: tehnică, experienţă, capacitate de abstractizare, logică,
inteligenţă, creativitate şi talent.
• Dintre toate cele şapte calităţi necesare programării de înaltă calitate, numai una –
talentul - nu este inerentă unui intelect sănătos. De altfel, prezenţa talentului nu este
absolut necesară pentru a deveni programator, dar în timp ce absenţa lui îngreunează
apropierea de calificativul bun programator, prezenţa lui şi amplificarea celorlalte
calităţi este o garanţie a succesului, ce va fi cu siguranţă obţinut, însă nu fără efort,
răbdare şi perseverenţă !
• Toate celelalte şase calităţi excluzînd talentul, prezente fiind într-o formă potenţială,
trebuiesc doar cultivate şi amplificate.
• “Cheia secretă“ ce conduce cu siguranţă la declanşarea procesului de dinamizare şi
amplificare a fiecăreia din cele şase calităţi inerente este de a avea mereu o motivaţie
puternică (de a învăţa “cu chef” sau “cu tragere de inimă”!). Acest fapt este posibil
dacă se ţine cont de necesitatea adaptării efortului la domeniul preferat al celui în
cauză. La modul concret, este necesar ca toate aplicaţiile, problemele, exerciţiile,
întrebările, curiozităţile, inovaţiile, descoperirile, “săpăturile” etc să fie făcute sau să
fie alese, la început, din domeniul preferat, chiar dacă acesta nu are la prima vedere
legătură cu programarea. Scopul ce se atinge cu siguranţă în acest mod în această
primă fază este acela de a pune “la lucru” inteligenţa, creativitatea, logica etc, ceea ce
va conduce cu siguranţă la trezirea şi amplificarea rapidă a acestor calităţi. Acest fapt
va permite apoi trecerea la o a doua fază în care, pe baza acumulărilor calitative
obţinute, se poate trece la programarea propriu-zise “înarmat cu forţe proaspete”.

Încheiem răspunzînd într-o singură frază întrebării din titlu Ce şanse am să devin un bun
programator ? :
dacă mă simt înzestrat cu talent pentru programare (adică nu mă simt inconfortabil
la acest subiect) atunci, mobilizîndu-mi voinţa (motivaţia) şi amplificîndu-mi capacitatea
de abstractizare, logica, inteligenţa şi creativitatea (ce există în mine într-o formă
potenţială), prin practică de programare voi acumula în timp tehnica şi experienţa
necesare pentru a deveni cu siguranţă un bun programator , însă nu fără efort, răbdare
şi perseverenţă.

3
Probleme de logică

Oferim în cele ce urmează o selecţie de probleme ce nu necesită cunoştinţe de


matematică avansate (doar nivelul preuniversitar) dar care pun la încercare capacitatea de
judecată, inspiraţia şi creativitatea gîndirii. Rezolvarea acestor probleme constituie un bun
antrenament pentru creşterea capacităţii de gîndire creativă precum şi a fluidităţii gîndirii.
Credem că nu degeaba aceste două trăsături sînt considerate cele mai importante semne ale
tinereţii minţii.
Problemele, selectate din multiple surse, nu au putut fi grupate în ordinea dificultăţii
mai ales datorită diversităţii şi varietăţii lor. Ele au fost doar separate în cîteva categorii a
căror nume vrea să sugereze un anumit mod de gîndire pe care l-am folosit şi noi în rezolvarea
lor. Criteriul principal pe baza căruia s-a făcut această selecţie a fost următorul: fiecare
problemă cere în rezolvarea ei un minimum de inventivitate şi creativitate. Majoritatea
problemelor te pun "faţă în faţă cu imposibilul", aşa că rezolvarea fiecărei probleme necesită
depăşirea unor "limitări ale gîndirii" plus un minimum de originalitate în gîndire. Tocmai de
aceea, pentru rezolvarea lor este nevoie de efort, putere de concentrare şi perseverenţă. Zis
într-un singur cuvînt: este necesar şi un strop de pasiune.
Considerăm că eforturile consecvente ale celor care vor rezolva aceste probleme vor fi
din plin răsplătite prin plăcerea "minţii biruitoare" şi prin amplificarea calităţilor următoare:
capacitate sporită de efort intelectual, putere de concentrare mărită şi prospeţime în gîndire.
Vă dorim mult succes !

Probleme de perspicacitate

1. Ştiind că un ou costă 1000 lei plus o jumătate de ou, cît costă un ou ?

2. Ce număr lipseşte alături de


ultima figură:
3 4 2 ?

3. Fumat Lui Popescu nici prin gînd nu-i trecea să folosească toate mijloacele pe care le
avea la îndemînă ca să lupte împotriva adversarilor tendinţei contra neintroducerii mişcării
anti-fumat. Care este poziţia lui Popescu: este pentru sau contra fumatului ?

4. Trei cutii. În trei cutii identice sînt închise trei perechi de fructe: fie o pereche de mere,
fie o pereche de pere, fie o pereche formată dintr-un măr şi o pară. Pe cele trei cutii sînt
lipite trei etichete: "două mere", "două pere" şi, respectiv, "un măr şi o pară". Ştiind că
nici una din etichete nu corespunde cu conţinutul cuitei închise pe care se află, să se afle
care este numărul minim de extrageri a cîte un fruct pentru a se stabili conţinutul fiecărei
cutii.

5. Întrerupătoarele. Pe peretele alăturat uşei încuiate de la intrarea unei încăperi, se află trei
întrerupătoare ce corespund cu cele trei becuri de pe plafonul încăperii în care nu putem
intra. Acţionînd oricare din întrerupătoare, dunga de lumină care apare pe sub uşă ne
asigură că niciunul din cele trei becuri nu este ars. Cum putem afla, intrând o singură dată
în încăpere, care întrerupător corespunde cu care bec ?

4
6. Cîntarul defect. Avînd la dispoziţie un cîntar gradat defect care greşeşte constant cu
aceeaşi valoare (cantitate necunoscută de grame), putem să cîntărim ceva determinîndu-i
corect greutatea ?

7. Împăturirea celor 8 pătrate. Împăturiţi iniţial în opt o foaie dreptunghiulară după care
desfaceţi-o şi însemnaţi fiecare din cele opt zone dreptunghiulare obţinute (marcate de
pliurile de îndoire) cu o cifră de la 1 la 8. Puteţi împături foaia astfel obţinută reducînd-o
de opt ori (la un singur dreptunghi sau pătrat) astfel încît trecînd cu un ac prin cele opt
pliuri suprapuse acesta să le perforeze exact în ordinea 1, 2, 3, …, 8 ? Încercaţi aceste
două configuraţii:

1 8 7 4
2 3 6 5

1 8 2 7
4 5 3 6

8. Problemă pentru cei puternici. Încercaţi să împăturiţi de 8 ori, pur şi simplu, o coală de
hîrtie (de fiecare dată linia de îndoire este "în cruce" peste cea dinainte). Este posibil ?

9. Piese defecte Într-un atelier există 10 lădiţe ce conţin fiecare piese cu greutatea de 100
grame, cu excepţia uneia din lădiţe ce conţine piese defecte având greutatea de 90 grame.
Puteţi preciza care este lădiţa cu pricina, folosind un cîntar doar pentru o singură dată ?

5
Probleme cu chibrituri

1. Realizati doua triunghiuri cu numai patru bete de chibrit.

2. Folosind doar sase bete de chibrit realizati in plan patru triunghiuri echilaterale egale.

3. Camarute. Realizati o constructie in care sa apara zece "camere" cu ajutorul a sapte chibrituri.Acestea nu pot
avea capete libere.

4. Triunghiuri. Folosind sase bete de chibrit, realizati o constructie care sa cuprinda patru triunghiuri echilaterale,
toate avand lungimea unui bat .

5. O intrebare. Poate fi construit un patrat din douazeci si sapte de chibrituri?

6. Doar 6 chibrituri. Realizati o constructie in care sa apara douasprezece triunghiuri dreptungice cu ajutorul a ...
sase chibrituri.

7. Cateva figuri geometrice simple. Utilizand noua chibrituri, realizati o constructie in care sa existe trei patrate
si doua triunghiuri echilaterale, toate cu laturile egale cu lungimea unui chibrit.

8. Demonstratie. Aratati ca folosind zece bete de chibrit, asezate doar in pozitie verticala sau orizontala se poate
construi un unic poligon cu aria de cinci unitati.

9. Un poligon. Folosind douasprezece bete de chibrit, desenati un poligon avand aria egala cu patru unitati.

10. Problema se complica. Impartiti optsprezece bete de chibrit in doua grupuri : unul de cinci si celalalt de
treisprezece.Realizati astfel, doua poligoane, unul cu aria de cinci ori mai mare decat a celuilalt.

11. 1=2 ? Deplasati un bat astfel incat sa se obtina o egalitate adevarata :

12. Numarul maxim. Cate constructii diferite se pot obtine prin folosirea a numai batru bete de chibrit? (Doua
figuri sunt diferite daca nu se poate obtine una din cealalta prin deformare, oglindire sau rasturnare)

13. Un hectar. Lungimea unui bat de chibrit este de aproximativ 4,5 cm, aria unui patrat format din patru bete
fiind deci de aproximativ 20 cm 2 . Care este numarul minim de bate necesare pentru a obtine un hectar?

14. Patrate. Daca admitem patratul cu aria nula, observam ca are perimetrul egal ca valoare numerica cu aria
sa.Exista si alte asemenea patrate?De cate chibrituri avem nevoie pentru a le construi?

15. Fractii. Cu ajutorul a sase bete de chibrit a fost realizata o fractie subunitara. Deplasati un singur bat
pentru a obtine o fractie cu valoarea 1.

16. Un cub. Intr-un punct de intalnire a trei bete de chibrit putem avea zero, unul, doua sau trei capete cu fosfor,
deci patru posibilitati. Din douasprezece bete realizati un cub in care pe fiecare fata sa se gaseasca toate cele
patru combinatii.

17. "Intalnirea". Realizati o constructie in care sa apara exact o data fiecare combinatie posibila de puncte de
intalnire a unul, doua si trei chirituri, distingand intre capatul cu fosfor si cel fara. (Punctul de intalnire pentru un
chibrit este un capat liber)

18. 4-5 patrate. Plecand de la urmatoarea figura,adaugati patru bete pentru a obtine o constructie cu numai patru,
respectiv cinci patrate.

6
19. Deplasari. In figura apar trei patrate realizate cu jutorul a douasprezece chibrituri.
Deplasati trei bete pentru a obtine sapte patrate sau patru chibrituri pentru a obtine patru
patrate.

20. Hexagonul. In figura apare un hexagon format cu ajutorul a sase chibrituri. Formati doua
romburi prin deplasarea a doua bete si adaugarea unui al treilea chibrit.

21. Spirala. Deplasati numai patru bete de chibrit din spirala pentru a obtine trei patrate.

22. Numarul minim. Care este cel mai mic numar care poate fi scris cu ajutorul a trei bete de chibrit?

23. Fereastra. Sa ne imaginam ca figura reprezinta o fereastra cu patru ochiuri de geam. Construiti o
alta fereastra, cu laturile formate tot din doua chibrituri, care sa aiba opt ochiuri de geam de aceeasi
forma, toate cu latura egala cu un bat de chibrit.

24. Impartiri. In figura apare un patrat 5 X 5 si patru chibrituri in interiorul acestuia. Folosind inca cincisprezece
bete de chibrit, impartiti suprafata patatului in cinci zone de forme diferite, dar de arii egale.

7
25. Posibilitati multiple. Aflati toate posibilitatile de orientare a trei chibrituri care se intalnesc
intr-un punct, chibrituri asezate dupa directiile indicate in figura.

26. O adunare. Deplasati un chibrit, astfel incat din operatia incorecta sa se obtina o egalitate corecta.

27. Unghiul drept. Folosind numai trei bete de chibrit, realizati de patru ori mai mule unghiuri drepte.

28. O constructie continua. Gasiti un drum continuu de-a lungul constructiei din figura cu conditia ca acesta sa
treaca peste fiecare bat o singura data.

29. Casuta din chibrituri. Casuta din figura are usa pe peretele din stanga. Deplasati doua chibrituri pentru a
obtine o casuta cu usa pe peretele din dreapta.

30. 6 = 2 ? Mutînd doar un singur băţ de chibrit să se restabilească egalitatea:

Probleme de logică şi judecată

1. Substituirea literelor. Subtituiţi literele cu cifre astfel încît următoarele adunări să fie
corecte: GERALD + DONALD = ROBERT ; FORTY + TEN + TEN = SIXTY ; BALON
+ OVAL = RUGBY.

2. Test de angajare la Microsoft. Patru excursionişti ajung pe malul unui rîu pe care doresc
să-l traverseze. Întrucît s-a înoptat şi ei dispun doar de o singură lanternă, ei pot să treacă
rîul cel mult cîte doi laolaltă. Ştiind că, datorită diferenţelor de vîrstă şi datorită oboselii,
ei ar avea individual nevoie pentru a traversa rîul de 1, 2, 8 şi 10 minute, se cere să se
decidă dacă este posibilă traversarea rîului în aceste conditţii în doar 17 minute ?

8
3. (!) Imposibilă. Să se taie toate cele 16 segmente ale figurii următoare cu o singură linie
curbă continuă şi care nu se intersectează cu ea însăşi.

4. (!) Problema "ochilor albaştri". Sîntem martorii următorului dialog între două persoane
X şi Y. << X: Eu am trei copii. Produsul vîrstei lor este 36 iar suma vîrstei lor este egală
cu numărul de etaje al blocului din vecini de mine. Îl ştii, nu-i aşa ? Y: Desigur. Dar
numai din cît mi-ai spsus nu pot să deduc care este vîrsta copiilor tăi. X: Bine, atunci află
că cel mare are ochi albaştrii.>> Puteţi afla care este vîrsta celor trei copii ?

5. Problema călugărului budhist. Într-o dimineaţă, exact la răsăritul soarelui, un călugăr


budhist porneşte de la templul de la baza muntelui pentru a ajunge la templul din vîrful
muntelui exact la apusul soarelui, unde el se roagă toată noaptea. A doua zi el porneşte din
vîrf pe aceeşi cărare, tot la răsăritul soarelui, pentru a ajunge la templul de la baza
muntelui exact la apusul soarelui. Să se arate că a existat un loc pe traseu în care călugărul
s-a aflat în ambele zile exact la aceaşi oră.

6. Vinul în apă şi apa în vin. Dintr-o sticlă ce conţine un litru de apă este luat un pahar (un
decilitru) ce este turnat pest un litru de vin. Vinul cu apa se amestecă bine după care se ia
cu acelaşi pahar o cantitate egală de "vin cu apă" ce se toarnă înapoi peste apa din sticlă.
Avem acum mai multă apă în vin decît vin în apă, sau invers ?

7. (!!!!) Cuiele în echilibru. Avem la dispoziţie 7 cuie normale, cu capul obişnuit. Înfigem
unul vertical în podea (sau într-o placă de lemn). Se cere să se aşeze cele 6 cuie rămase în
echilibru stabil pe capul cuiului vertical, fără ca niciunul din cele şase cuie să atingă
podeaua.

8. (!!) Ţigările tangente. Este posibil să aşezăm pe masă şase ţigări astfel încît fiecare să se
atingă cu fiecare (oricare două să fie tangente) ? (!!!) Dar şapte ţigări ?

9. (!) Problema celor 12 înţelepţi (în variantă modernă). Managerul unei mari companii
doreşte să pună la încercare inteligenţa şi puterea de judecată a celor 12 membrii ai
consiliului său de conducere. Luînd 12 cărţi de joc, unele de pică şi altele de caro, el le
aşează cîte una pe fruntea fiecărui consilier astfel încît fiecare să poată vedea cărţile de pe
frunţile celorlalţi dar nu şi pe a sa. Managerul le cere celor care consideră că au pe frunte o
carte de caro (diamond) să facă un pas în faţă, altfel ei nu vor mai putea face parte din
consiliu. După ce îşi repetă cererea de şapte ori, timp în care niciunul din cei 12 consilieri
nu face nici o mişcare (ci doar se privesc unii pe alţii), toţi consilierii care au într-adevăr
pe frunte o carte de caro ies deodată în faţă. Puteţi deduce cîţi au ieşit şi cum şi-au dat ei
seama ce carte este aşezată pe fruntea lor ?

10. Păianjenul şi musca. Pe peretele lateral al unei hale cu dimensiunile de 40 x 12 x12


metri, pe linia mediană a peretelui lateral şi exact la 1 metru de tavan, se află un păianjen.
Pe peretele lateral opus, tot pe linia mediană şi exact la 1 metru de podea, se află o muscă
amorţită. Care este distanţa cea mai scurtă pe care păianjenul o are de parcurs de-a lungul
pereţilor pentru a se înfrupta din muscă ?

9
11. Rifi şi Ruf. Cei doi iubiţi Rifi şi Ruf, din nordica ţară Ufu-Rufu, locuiesc în sate diferite
aflate la distanţa de 20 km unul de altul. În fiecare dimineaţă ei pornesc exact deodată (la
răsărit) unul spre celălalt spre a se întîlni şi a se săruta confrom obiceiului nordic: nas în
nas. Într-o dimineaţă o muscă rătăcită porneşte exact la răsăritul soarelui de pe nasul lui
Rifi direct spre nasul lui Ruf, care o alungă trimiţînd-o din nou spre nasul lui Rifi, ş.a.m.d.
..., pînă cînd ea sfîrşeşte tragic în momentul "sărutului" celor doi. Ştiind că Rifi se
deplasează cu 4 km/oră, Ruf cu 6 km/oră iar musca zboară cu 10 km/oră, se cere să se afle
ce distanţă a parcurs musca în zbor de la răsărit şi pînă în momentul tragicului ei sfîrşit.

12. O anti-problemă de şah. În următoarea configuraţie a pieselor pe o tablă de şah se cere


să nu daţi mat dintr-o mutare ! (Albul atacă de jos în sus. Legenda: P-pion, N-nebun, R-
rege, T-turn, C-cal. Alăturat fiecărei piese este scrisă culoarea sa, alb-a sau negru-n.)

Na Ra Ta

Tn Na

Ta

Nn Pn Pn

Pa Rn Pa

Pn Pa Pn

Pa Pa Pa

Ca Ca

13. Bronx contra Brooklyn. Un tînăr, ce locuieşte în Manhattan în imediata apropiere a unei
staţii de metrou, are două prietene, una în Brooklyn şi cealaltă în Bronx. Pentru a o vizita
pe cea din Brooklyn el ia metroul ce merge spre partea de jos a oraşului, în timp ce, pentru
a o vizita pe cea din Bronx, el ia din acelaşi loc metroul care merge în direcţie opusă.
Metrourile spre Brooklyn şi spre Bronx intră în staţie cu aceeşi frecvenţă: din 10 în 10
minute fiecare. Dar, deşi el coboară în staţia de metrou în fiecare sîmbătă la întîmplare şi
ia primul metrou care vine (nedorind să "favorizeze" pe nici una din prietenele sale), el a
constatat că, în medie, el merge în Brooklyn de 9 ori din 10. Puteţi găsi o explicaţie logică
a fenomenul ?

14. (!!) Problema celor 12 bile. În faţa noastră se află 12 bile identice ca formă, vopsite la
fel, dar una este cu siguranţă falsă, ea fiind fie mai grea, fie mai uşoară, fiind făcută dintr-
un alt material. Avem la dispoziţie o balanţă şi se cere să determinăm doar prin 3 cîntăriri
care din cele 12 bile este falsă precizînd şi cum este ea: mai grea sau mai uşoară. (!!!) Mai
mult, puteţi determina care este numărul maxim de bile din care prin 4 cîntăriri cu balanţa
se poate afla exact bila falsă şi cum este ea ?

10
15. (!) Problema celor 2 perechi de mănuşi. Aflat într-o situaţie ce implică intervenţia de
urgenţă, un medic chirurg constată că are la dispoziţie doar 2 perechi de mănuşi sterile
deşi el trebuie să intervină rapid şi să opereze succesiv 3 bolnavi. Este posibil ca cele trei
operaţii de urgenţă să se desfăşoare în condiţii de protecţie normale cu numai cele 2
perechi de mănuşi ? (Sîngele fiecăruia din cei 3 pacienţi, precum şi mîna doctorului nu
trebuie să conducă la un contact infecţios.)

16. (!!) Problema frînghiei prea scurte. O persoană ce are asupra ei doar un briceag şi o
frînghie lungă de 30 metri se află pe marginea unei stînci, privind în jos la peretele vertical
de 40 metri aflat sub ea. Frînghia poate fi legată doar în vîrf sau la jumătatea peretelui (la
o înălţime de 20 metri de sol) unde se află o mică platformă de sprijin. Cum este posibil ca
persoana aflată în această situaţie să ajungă teafără jos coborînd numai pe frînghie, fără a
fi nevoită să sară deloc punîndu-se astfel în pericol ?

17. (!!) O jumătate de litru. Avem în faţa noastră un vas cilindric cu capacitatea de 1 litru,
plin ochi cu apă. Se cere să măsurăm cu ajutorul lui ½ litru de apă, fără a ne ajuta de nimic
altceva decît de mîinile noastre.

18. (!) Să v ezi şi să nu crezi. Priviţi următoarele două figuri: prin reaşezarea decupajelor
interioare ale primeia se obţine din nou aceeaşi figură dar avînd un pătrăţel lipsă ! Cum
explicaţi "minunea" ?

Probleme de logică şi judecată cu "tentă informatică"

1. (!!!) Decriptarea scrierii încifrate. Se dau următoarele numere împreună cu denumirile


lor cifrate:
5 nabivogedu
6 nagevogedu
10 nabivobinaduvogedu
15 nabivonagevogedunaduvogedu
20 nabivogenagevogenaduvogedu
25 nabivonabivobinagevogedunagevogenaduvogedu
30 nabivodunanabivobiduvogedu
50 nabivonabivonabivogedunagevogenaduvogedunanabivobiduvogedu
60 nabivonagevogedunagevogenanabivobiduvogedu
90 nabivonaduvogedunagevodunanabivobiduvogedu
100 nabivonabivobinagevogenaduvogedunagevodunanabivobiduvogedu

11
Care este regula de încifrare? Ce numere reprezintă următoarele coduri cifrate:
nagevonagevogedunanabivobiduvogedu;
nagevonaduvogedunanabivobiduvogedu;
naduvogenanabivobiduvogedu;
nanabivogeduvogedu;
nabivonabivonaduvogedunagevonagevogedunanabivobiduvogedu;
nanagevobiduvogedu?
Încifraţi numerele 256 şi 1024 prin acestă metodă.

2. (!!!) Altfel de codificare binară a numerelor. Descoperiţi metoda de codificare binară a


numerelor folosită în continuare:

1 1 20 101010
2 10 25 1000101
3 11 30 1010001
5 110 40 10001001
10 1110 50 10100100
15 10010 60 100001000
Puteţi spune ce numere sînt codificate prin 100, 101, 1000, 1111, 10000 şi 11111 ?
Puteţi codifica numerele 70, 80, 90, 100, 120, 150 şi 1000 ?

3. (!!!) Problema dialogului perplex. Există două numere m şi n din intervalul [2..99] şi
două persoane P şi S astfel încît persoana P ştie produsul lor, iar S ştie suma lor. Ştiind că
între P şi S a avut loc următorul dialog:
"Nu ştiu numerele" spune P.
"Ştiam ca nu ştii" răspunde S, "nici eu nu ştiu."
"Acuma ştiu !" zice P strălucind de bucurie.
"Acum ştiu şi eu…" şopteşte satisfăcut S.
să se determine toate perechile de numere m şi n ce "satisfac" acest dialog (sînt soluţii ale
problemei).

4. (!!!!) Împăturirea celor 8 pătrate. Împăturiţi iniţial în opt o foaie dreptunghiulară după
care desfaceţi-o şi însemnaţi fiecare pătrăţel obţinut cu o cifră de la 1 la 8. Proiectaţi un
algoritm şi realizaţi un program care, primind configuraţia (numerotarea) celor 8 pătrăţele,
să poată decide dacă se poate împături foaia astfel obţinută reducînd-o de opt ori (la un
singur pătrat) astfel încît trecînd cu un ac prin cele opt foi suprapuse acesta să le perforeze
exact în ordinea 1, 2, 3, …, 8.

5. (!!!!) Problema fetelor de la pension. Problema a apărut pe vremea cînd fetele învăţau la
pension fără ca prin prezenţa lor băieţii să le tulbure educaţia. Pedagoaga fetelor unui
pension de 15 fete a hotarît ca în fiecare dupa-amiază, la ora de plimbare, fetele să se
plimbe în cinci grupuri de cîte trei. Se cere să se stabilească o programare a plimbărilor pe
durata unei săptămîni (şapte zile) astfel încît fiecare fată să ajungă să se plimbe numai o
singură dată cu oricare din celelalte paisprezece (oricare două fete să nu se plimbe de două
ori împreună în decursul unei săptămîni).

12
CAPITOLUL 2
Noţiuni fundamentale de programare

Programarea are ca scop realizarea de programe care să constituie soluţiile oferite cu


ajutorul calculatorului unor probleme concrete. Programatorii sînt acele persoane capabile să
implementeze într-un limbaj de programare metoda sau algoritmul propus ca soluţie
respectivei probleme, ce se pretează a fi soluţionată cu ajutorul calculatorului. După nivelul de
implicare în efortul de rezolvare a problemelor, specialiştii în programare pot fi împărţiţi în
diverse categorii: analişti, analişti-programatori, ingineri-programatori, simpli programatori,
etc. Cu toţii au însă în comun faptul că fiecare trebuie să aibă noţiuni avansate de programare
şi să fie capabil, nu doar să citească, ci chiar să scrie “codul sursă”, adică programul propriu-
zis. Din acest punct de vedere cunoştinţele de programare sînt considerate “ABC-ul”
informaticii şi sînt indispensabile oricărui profesionist în domeniu.

Etapele rezolvării unei probleme cu ajutorul calculatorului

În rezolvarea sa cu ajutorul calculatorului orice problemă trece prin următoarele etape


obligatorii: Analiza problemei, Proiectarea algoritmului de soluţionare, Implementarea
algoritmului într-un program pe calculator, Testarea şi Întreţinerea programului. Aceste
etape formează “ciclul de viaţă” al oricărui produs-program dar, în continuare în acest curs ne
vom referi doar la primele trei etape.
Dacă etapa implementării algoritmului într-un program executabil este o etapă
exclusiv practică, realizată “în faţa calculatorului”, celelalte două etape au un pronunţat
caracter teoretic. În consecinţă, primele două etape sînt caracterizate de un anumit grad de
abstractizare. Din punct de vedere practic însă, şi în ultimă instanţă, criteriul decisiv ce
conferă succesul rezolvării problemei este dat de calitatea implementării propriu-zise. Mai
exact, succesul soluţionării este dat de performanţele programului: utilitate, viteză de
execuţie, fiabilitate, posibilităţi de dezvoltare ulterioare, lizibilitate, etc. Cu toate acestea este
imatură şi neprofesională “strategia” programatorilor începători care, neglijînd primele două
etape, sar direct la a treia fugind de analiză şi de componenta abstractă a efortului de
soluţionare. Această abordare este complet greşită deoarece se pot ajunge la soluţii care nu
funcţionează decât pentru anumite cazuri particulare ale problemei, neoferind o soluţie optimă
şi generală.

Corectitudinea şi eficienţa soluţionării

Este adevărat că ultima etapă în rezolvarea unei probleme – implementarea – este


decisivă, dar primele două etape au o importanţă capitală. Ele sînt singurele ce pot oferi
răspunsuri corecte la următoarele întrebări dificile: Avem certitudinea că soluţia găsită este
corectă? Avem certitudinea că problema este complet rezolvată? Cît de eficientă este soluţia
găsită? Cît de departe este soluţia aleasă de o soluţie optimă?
Să menţionăm în plus că literatura informatică de specialitate conţine un număr
impresionant de probleme “capcană” pentru începători, şi nu numai pentru ei. Ele provin
majoritatea din realitatea imediată dar pentru fiecare dintre ele nu se cunosc soluţii eficiente.
De exemplu, este dovedit teoretic că problema, “aparent banală” pentru un calculator, a
proiectării Orarului optim într-o instituţie de învăţămînt (şcoală, liceu, facultate) este o
problemă intratabilă la ora actuală (toate programele care s-au realizat pînă acum nu oferă
decît soluţii aproximative fără a putea spune cît de aproape sau de departe este soluţia optimă
de orar).
Majoritatea programatorilor începători ar fi surprinşi să afle că o problemă foarte
simplă ca enunţ, a cărei soluţionare tocmai au abandonat-o, este de fapt o problemă dovedită
teoretic ca fiind intratabilă sau chiar insolvabilă algoritmic. Partea proastă a lucrurilor este că,
aşa cum ciupercile otrăvite nu pot fi cu uşurinţă deosebite de cele comestibile, tot astfel
problemele netratabile pot fi cu uşurinţă confundate cu nişte probleme uşoare la o privire
rapidă şi lipsită de experienţă.
Dacă ar fi să sintetizăm în cîte un cuvînt efortul asupra căruia se concentrează fiecare
din cele trei etape – analiza, proiectarea şi implementarea– cele trei cuvinte ar fi:
corectitudine, eficienţă şi impecabilitate. Etapa de analiză este singura care permite dovedirea
cu argumente riguroase a corectitudinii soluţiei, iar etapa de proiectare este singura care poate
oferi argumente precise în favoarea eficienţei soluţiei propuse.
În general problemele concrete din informatică au în forma lor iniţială sau în enunţ o
caracteristică pragmatică, fiind foarte ancorate în realitatea imediată. Totuşi ele conţin în
formularea lor iniţială un grad mare de eterogenitate, diversitate şi lipsă de rigoare. Fiecare
dintre aceste “defecte” este un obstacol major pentru demonstrarea corectitudinii soluţiei.
Rolul esenţial al etapei de analiză este acela de a transfera problema “de pe nisipurile
mişcătoare” ale realităţii imediate de unde ea provine într-un plan abstract, adică de a o
modela. Acest “univers paralel abstract” este dotat cu mai multă rigoare şi disciplină internă,
avînd legi precise, şi poate oferi instrumentele logice şi formale necesare pentru demonstrarea
riguroasă a corectitudinii soluţiei problemei. Planul abstract în care sînt “transportate” toate
problemele de informatică este planul sau universul obiectelor matematice iar corespondentul
problemei în acest plan va fi modelul matematic abstract asociat problemei. Demonstrarea
corectitudinii proprietăţilor ce leagă obiectele universului matematic a fost şi este sarcina
matematicienilor. Celui ce analizează problema din punct de vedere informatic îi revine
sarcina nu tocmai uşoară de a dovedi printr-o demonstraţie constructivă că există o
corespondenţă precisă (o bijecţie) între părţile componente ale problemei reale, rezultate în
urma analizei, şi părţile componente ale modelului abstract asociat. Odată descoperită,
formulată precis şi dovedită, această “perfectă oglindire” a problemei reale în planul
obiectelor matematice oferă certitudinea că toate proprietăţile şi legăturile ce există între
subansamblele modelului abstract se vor regăsii precis (prin reflectare) între părţile interne ale
problemei reale, şi invers. Atunci, soluţiei abstracte descoperite cu ajutorul modelului
matematic abstract îi va corespunde o soluţie reală concretizată printr-un algoritm ce poate fi
implementat într-un program executabil.
Aceasta este calea generală de rezolvare a problemelor şi oricine poate verifica acest
fapt. De exemplu, ca şi exerciţiu, încercaţi să demonstraţi corectitudinea (adică să se aducă
argumente precise, clare şi convingătoare în favoarea corectitudinii) metodei de extragere a
radicalului învăţată încă din şcoala primară (cu grupare cifrelor numărului în grupuri cîte
două, etc…) sau a algoritmului lui Euclid de determinare a celui mai mare divizor comun a
două numere prin împărţiri întregi repetate. Desigur nu pot fi acceptate argumente de forma:
“Algoritmul este corect pentru că aşa l-am învăţat!” sau “Este corect pentru că aşa face toată
lumea !” din moment ce nu se oferă o argumentaţie matematică riguroasă.
Ideea centrală a etapei a doua – proiectarea unui algoritm de soluţionare eficient poate
fi formulată astfel: din studiul proprietăţilor şi limitelor modelului matematic abstract asociat
problemei se deduc limitele inferioare ale complexităţii minimale (“efortului minimal
obligatoriu”) inerente oricărui algoritm ce va soluţiona problema în cauză. Complexitatea
internă a modelului abstract şi complexitatea soluţiei abstracte se va reflecta imediat asupra
complexităţii reale a algoritmului, adică asupra eficienţei de soluţionare a problemei. Acest

2
fapt permite prognosticarea încă din această fază – faza de proiectare a algoritmului de
soluţionare – a eficienţei practice, măsurabilă ca durată de execuţie, a programului.

Algoritmul

Se ştie că la baza oricărui program stă un algoritm (care, uneori, este numit metodă de
rezolvare). Noţiunea de algoritm este o noţiune fundamentală în informatică şi înţelegerea ei,
alături de înţelegerea modului de funcţionare a unui calculator, permite înţelegerea noţiunii de
program executabil. Vom oferi în continuare o definiţie unanim acceptată pentru noţiunea de
algoritm:

Definiţie. Prin algoritm se înţelege o mulţime finită de operaţii (instrucţiuni)


elementare care executate într-o ordine bine stabilită (determinată), pornind de la un set de
date de intrare dintr-un domeniu de valori posibile (valide), produce în timp finit un set de
date de ieşire (rezultate).

Cele trei caracteristici esenţiale ale unui algoritm sînt:

Determinismul – dat de faptul că ordinea de execuţie a instrucţiunilor algoritmului este bine


precizată (strict determinată).
Acest fapt dă una din calităţile de bază ale calculatorului: el va face întotdeauna ceea ce i
s-a cerut prin program să facă, el nu va avea iniţiative sau opţiuni proprii, el nu-şi permite
să greşească nici măcar odată, el nu se va plictisi ci va duce programul la acelaşi sfîrşit
indiferent de cîte ori i se va cere să repete acest lucru. Nu aceeaşi situaţie se întîmplă cu
fiinţele umane (Errare humanum est). Oamenii pot avea în situaţii determinate un
comportament non-deterministic. Acesta este motivul pentru care numeroşi utilizatori de
calculatoare, datorită fenomenului de personificare a calculatorului (confundarea
acţiunilor şi dialogului simulat de programul ce rulează pe calculator cu reacţiile unei
persoane), nu recunosc perfectul determinism ce stă la baza executării oricărui program
pe calculator. Exprimîndu-se prin propoziţii de felul: “De trei ori i-am dat să facă
calculele şi de fiecare dată mi-a scos aceleaşi valori aiurea!” ei îşi trădează propria
viziune personificatoare asupra unui fenomen determinist.
Universalitatea – dată de faptul că, privind algoritmul ca pe o metodă automată de rezolvare,
această metodă are un caracter general-universal. Algoritmul nu oferă o soluţie punctuală,
pentru un singur set de date de intrare, ci oferă soluţie pentru o mulţime foarte largă, de
cele mai multe ori infinită, de date de intrare valide. Aceasta este trăsătura de bază care
explică deosebita utilitate a calculatoarelor şi datorită acestei trăsături sîntem siguri că
investiţia financiară făcută prin cumpărarea unui calculator şi a produsului-soft necesar va
putea fi cu siguranţă amortizată. Cheltuiala se face o singură dată în timp ce programul pe
calculator va putea fi executat rapid şi economicos de un număr oricît de mare de ori, pe
date diferite!
De exemplu, algoritmul de rezolvare a ecuaţiilor de gradul doi: ax2+bx+c=0, se aplică cu
succes pentru o mulţime infinită de date de intrare: (a,b,c)∈ℜ\{0}xℜxℜ.
Finitudinea – pentru fiecare intrare validă orice algoritm trebuie să conducă în timp finit,
adică după un număr finit de paşi, la un rezultat. Această caracteristică este analogă
proprietăţii de convergenţă a unor metode din matematică: trebuie să avem garanţia,
dinainte de a aplica algoritmul, că metoda se termină cu succes, adică ea converge către
soluţie.

3
Să observăm şi diferenţa: în timp ce metoda matematică este corectă chiar dacă ea
converge către soluţie doar la infinit, un algoritm trebuie să întoarcă rezultatul după un
număr finit de paşi. Să observăm de asemenea că, acolo unde matematica nu oferă
dovada, algoritmul nu va fi capabil să o ofere nici el. De exemplu, nu este greu de scris un
algoritm care să verifice corectitudinea Conjecturii lui Goldbach: “Orice număr par se
scrie ca sumă de două numere prime”, dar, deşi programul rezultat poate fi lăsat să ruleze
pînă la valori extrem de mari, fără să apară nici un contra-exemplu, totuşi conjectura nu
poate fi astfel infirmată, dar nici afirmată.

Descrierea algoritmilor

Două dintre metodele clasice de descriere a algoritmilor sînt denumite Schemele logice şi
Pseudo-Codul. Ambele metode de descriere conţin doar patru instrucţiuni elementare care au
fiecare un corespondent atît schemă logică cît şi în pseudo-cod.
În cele ce urmează vom prezenta amândouă variantele, dar în ultimul timp se foloseşte
numai varianta pseudo-cod, folosirea schemelor logice s-a redus drastic în ultimii ani.
Avantajul descrierii algoritmilor prin scheme logice este dat de libertatea totală de înlănţuire a
operaţiilor (practic, săgeata care descrie ordinea de execuţie, pleacă de la o operaţie şi poate fi
trasată înspre orice altă operaţie). Este demonstrat matematic riguros că descrierea prin
pseudo-cod, deşi pare mult mai restrictivă (operaţiile nu pot fi înlănţuite oricum, ci trebuie
executate în ordinea citirii: de sus în jos şi de la stînga la dreapta), este totuşi perfect
echivalentă. Deci, este dovedit că plusul de ordine, rigoare şi simplitate pe care îl oferă
descrierea prin pseudo-cod nu îngrădeşte prin nimic libertatea programării. Totuşi,
programele scrise în limbajele de asamblare, care sînt mult mai compacte şi au dimensiunile
mult reduse, nu ar putea fi descrise altfel decît prin scheme logice.

Varianta descrierii prin schemă logică este caracterizată prin folosirea următoarelor
blocuri elementare:

1. Atribuirea –

2. Intrare/Ieşire –

3. Condiţionala -

4. Ciclurile – cu ajutorul blocurilor elementare de mai sus se pot realiza orice fel de cicluri,
atât cele cu repetare condiţionată (de tip Repeat-Until şi Do-While) cât şi cele cu repetare
de un număr cunoscut de ori (de tip For-Next).

4
Varianta descrierii prin pseudo cod este caracterizată prin folosirea următoarelor
instrucţiuni elementare:

1. Atribuirea – var:=expresie;

2. Intrare/Ieşire – Read var1, var2, var3, …;


Write var1, var2, var3, …;
Write expresia1, expresia2, expresia3,…;

3. Condiţionala - If <condiţie_logică> then instrucţiune1 [else instrucţiune2];

4. Ciclurile – Există (din motive de uşurinţă a descrierii algoritmilor) trei tipuri de


instrucţiuni de ciclare. Ele sînt echivalente între ele, oricare variantă de descriere putînd fi
folosită în locul celorlalte două, cu modificări sau adăugiri minimale:

Repeat instrucţiune1, instrucţiune2, … until <condiţie_logică>;

While <condiţie_logică> do instrucţiune;

For var_contor:=val_iniţială to val_finală do instrucţiune;

În cazul ciclurilor, grupul instrucţiunilor ce se repetă se numeşte corpul ciclului iar


condiţia logică ce permite sau nu reluarea execuţiei ciclului este denumită condiţia de ciclare.
Observăm că ciclul de tipul Repeat-Until are condiţia de repetare la sfîrşit ceea ce are ca şi
consecinţă faptul că instrucţiunile din corpul ciclului se execută cel puţin odată, în mod
obligatoriu, înainte de verificarea condiţiei logice. Nu acelaşi lucru se întîmplă în cazul
ciclului de tipul Do-While, cînd este posibil ca instrucţiunea compusă din corpul ciclului să nu
poată fi executată nici măcar odată. În plus, să mai observăm că ciclul de tipul For-Next
conţine ascunsă o instrucţiune de incrementare a variabilei contor.
Să observăm că, mai ales pentru un vorbitor de limbă engleză, programele scrise într-
un limbaj de programare ce cuprinde aceste instrucţiuni este foarte uşor de citit şi de înţeles, el
fiind foarte apropiat de scrierea naturală. Limbajele de programare care sînt relativ apropiate
de limbajele naturale sînt denumite limbaje de nivel înalt (high-level), de exemplu limbajul
Visual Basic, spre deosebire de limbajele de programare mai apropiate de codurile numerice
ale instrucţiunilor microprocesorului. Acestea din urmă se numesc limbaje de nivel scăzut
(low-level), de exemplu limbajul de asamblare. Limbajul de programare C are un statut mai
special el putînd fi privit, datorită structurii sale, ca făcînd parte din ambele categorii.
Peste tot unde în pseudo-cod apare cuvîntul instrucţiune el poate fi înlocuit cu oricare
din cele patru instrucţiuni elementare. Această substituire poartă numele de imbricare (de la
englezescul brick-cărămidă). Prin instrucţiune se va înţelege atunci, fie o singură instrucţiune
simplă (una din cele patru), fie o instrucţiune compusă. Instrucţiunea compusă este formată
dintr-un grup de instrucţiuni delimitate şi grupate în mod precis (între acolade { } în C sau
între begin şi end în Visual Basic).
Spre deosebire de pseudo-cod care permite doar structurile noi formate prin imbricarea
repetată a celor patru instrucţiuni în modul precizat, schemele logice permit structurarea în
orice succesiune a celor patru instrucţiuni elementare, ordinea lor de execuţie fiind dată de
sensul săgeţilor. Deşi, aparent, pseudo-codul limitează libertatea de descriere doar la
structurile prezentate, o teoremă fundamentală pentru programare afirmă că puterea de
descriere a pseudo-limbajului este aceeaşi cu cea a schemelor logice.

5
Forma de programare care se bazează doar pe cele patru structuri se numeşte
programare structurată (spre deosebire de programarea nestructurată bazată pe descrierea
prin scheme logice). Teorema de echivalenţă a puterii de descriere prin pseudo-cod cu
puterea de descriere prin schemă logică afirmă că programarea structurată (aparent limitată
de cele patru structuri) este echivalentă cu programarea nestructurată (liberă de structuri
impuse). Evident, prin ordinea, lizibilitatea şi fiabilitatea oferită de cele patru structuri
elementare (şi asta fără a îngrădi libertatea de exprimare) programarea structurată este net
avantajoasă. În fapt, limbajele de nivel înalt de programare nestructurată (Fortran, Basic) au
fost de mult scoase din uz. În schimb, cele de nivel scăzut (limbajele de asamblare) sînt
necesare a fi folosite în continuare în cazurile necesităţii unor viteze mari de execuţie la
programarea în timp-real, mai ales pentru software de conducere a proceselor (în
automatizări).

Programul

Prin program se înţelege un şir de instrucţiuni-maşină care sînt rezultatul compilării


algoritmului proiectat spre rezolvarea problemei dorite ce a fost descris într-un limbaj de
programare (prin cod sursă).
Etapele realizării unui program sînt:
 Editarea codului sursă, etapă ce se realizează cu ajutorul unui program editor de texte
rezultatul fiind un fişier Visual Basic sau C, cu extensia .bas sau .c (.cpp)
 Compilarea, etapa de traducere din limbajul de programare VisualBasic sau C în limbajul
intern al micro-procesorului, şi este realizată cu ajutorul programului compilator Visual
Basic sau C şi are ca rezultat un fişier obiect, cu extensia .obj (în limbajul C) sau .exe (în
limbajul Visual Basic)
 Link-editarea, etapă la care se adaugă modulului obiect rezultat la compilare diferite
module conţinînd subprograme şi rutine din biblioteci, rezultînd un fişier executabil
(această etapă este comasată în Visual Basic cu etapa de compilare), cu extensia .exe
 Execuţia (Run), etapa de lansare în execuţie propriu-zisă a programului obţinut, lansare
realizată de interpretorul de comenzi al sistemului de operare (command.com pentru
sistemele DOS+Windows)
Observăm că aceste patru (sau trei, pentru Visual Basic) etape sînt complet independente
în timp unele de altele şi necesită utilizarea a patru programe ajutătoare: Editor de texte,
Compilator Visual Basic sau C, Link-editor şi Interpretorul de comenzi al sistemului de
operare. În cazul mediilor de programare integrate (Turbo sau Borland) comandarea acestor
patru programe ajutătoare precum şi depanarea erorilor de execuţie este mult facilitată.
De asemenea, merită subliniat faptul că în timp ce fişierul text Visual Basic sau C, ce
conţine codul sursă, poate fi transportat pe orice maşină (calculator) indiferent de micro-
procesorul acesteia urmînd a fi compilat "la faţa locului", în cazul fişierului obiect acesta nu
mai poate fi folosit decît pe maşina (calculatorul) pentru care a fost creat (datorită
instrucţiunilor specifice micro-procesorului din care este compus). Deci, pe calculatoare
diferite (avînd micro-procesoare diferite) vom avea nevoie de compilatoare Visual Basic sau
C diferite.
În plus, să remarcăm faptul că fişierele obiect rezultate în urma compilării pot fi link-
editate împreună chiar dacă provin din limbaje de programare diferite. Astfel, un program
rezultat (un fişier .exe sau .com) poate fi compus din module obiect care provin din surse
diferite (fişiere Visual Basic, C, asamblare, etc.).

6
Exemple de probleme rezolvate

Prezentăm în continuare, spre iniţiere, cîteva exemple de probleme rezolvate. Vom


prezenta numai pseudo-codul, acesta urmând a fi transcris într-un limbaj de programare
pentru a fi implementat pe calculator. De asemenea, fiecare program va fi precedat de o scurtă
descriere a modului de elaborare a soluţiei.

1. Se citesc a,b,c coeficienţii reali a unei ecuaţii de gradul II. Să se afişeze soluţile
ecuaţiei.

Descrierea algoritmului:
- ecuaţia de gradul II este de forma ax2+bx+c=0
- presupunînd că a ≠ 0 calculăm determinantul ecuatiei delta = b 2 − 4 ⋅ a ⋅ c
− b ± delta
- dacă delta >= 0 atunci ecuaţia are soluţiile reale x1, 2 =
2⋅a
b delta
- dacă delta < 0 atunci ecuaţia are soluţiile complexe z1, 2 = − ± j⋅
2⋅a 2⋅a

Ecuatie_grad_2;
Var a,b,c,delta,x1,x2,Rex1,Rex2,Imx1,Imx2:real;
BEGIN
Read a,b,c;
If a=0 then
Begin
x1:=-c/b;
Write x1;
End
else
Begin
delta:=b*b-4*a*c;
If delta>=0 then
Begin
x1:=(-b-sqrt(delta))/(2*a);
x2:=(-b+sqrt(delta))/(2*a);
Write x1,x2;
End
else
Begin
Rex1:=-b/(2*a);
Rex2:=-b/(2*a);
Imx1:=-sqrt(-delta))/(2*a);
Imx2:=sqrt(-delta))/(2*a);
Write Rex1,Imx1;
Write Rex2,Imx2;
End
End
END.

7
2. Să se determine dacă trei numere a,b,c reale pot reprezenta laturile unui triunghi.
Dacă da, să se calculeze perimetrul şi aria sa.

Descrierea algoritmului:
- condiţia necesară pentru ca trei numere să poată fi lungimile laturilor unui triunghi este ca
cele trei numere să fie pozitive (condiţie implicită) şi suma a oricăror două dintre ele să fie
mai mare decît cel de-al treilea număr
- după condiţia este îndeplinită vom calcula perimetrul triunghiului cu formula p=a+b+c şi
aria folosind formula lui Heron s=sqrt(2*p(2*p-a)( 2*p-b)( 2*p-c)).

Laturile_Unui_Triunghi;
Var a,b,c,s,p:real;
function laturi_ok:boolean;
begin
laturi_ok:= (a>0) and (b>0) and (c>0) and (a+b>c) and (a+c>b) and (b+c>a);
end;
BEGIN
Read a,b,c;
IF laturi_ok then
begin
p:=a+b+c;
s:=sqrt(2*p*(2*p-a)*( 2*p-b)*( 2*p-c));
write s;
write p;
end
else write 'Nu formeaza triunghi';
END.

3. Se citeşte n întreg. Să se determine suma primelor n numere naturale.

Descrierea algoritmului:
- vom oferi varianta în care suma primelor n numere naturale va fi calculata cu una dintre
instructiunile repetitive cunoscute (for, while, repeat) fără a apela la formula matematică
cunoscută S(n)=n*(n+1)/2

Suma_n;
Var n,s,i:integer;
BEGIN
Read n;
s:=0;
For i:=1 to n do s:=s+i;
Write s;
END.

4. Se citeşte valoarea întreagă p. Să se determine daca p este număr prim.

Descrierea algoritmului:
- un număr p este prim dacă nu are nici un divizor în afară de 1 şi p cu ajutorul unei variabile
contor d vom parcurge toate valorile intervalului [2.. p ]; acest interval este suficient pentru

8
depistarea unui divizor, deoarece dacă d1 | p ⇒ p = d1 ⋅ d 2 (unde d1 < d2) ⇒ d1 ≤ d1 ⋅ d 2 =
p iar d2 ≥ d1 ⋅ d 2 = p

Nr_prim;
Var p,i:integer;
prim:boolean;
BEGIN
Read p;
prim:=true;
for i:=2 to trunc(sqrt(p)) do
if n mod i=0 then prim:=false;
if prim then
write 'este nr prim'
else
write 'nu e nr prim';
END.

5. Se citeşte o propoziţie (şir de caractere) terminată cu punct. Să se determine cîte


vocale, cîte consoane şi câte caractere nealfanumerice conţine propoziţia.

Vocale;
Var sir:string[80];
Vocale,Consoane,i:integer;
BEGIN
Read sir;
i:=1; Vocale:=0; Consoane:=0;
While sir[i]<>’.’ do
begin
If Upcase(sir[i]) in [‘A’,’E’,’I’,’O’,’U’] then Inc(Vocale)
else If Upcase(sir[i]) in [‘A’..’Z’] then Inc(Consoane);
Inc(i);
end;
Write Vocale, Consoane, i-Vocale-Consoane;
END.

9
În continuare se prezintă o listă de probleme propuse spre rezolvare.

1. Se citesc a, b, c trei variabile reale.


 Să se afişeze maximul şi minimul celor trei numere.
 Să se afişeze cele trei numere în ordine crescătoare.
 Să se determine dacă cele trei numere pot reprezenta laturile unui triunghi. Dacă da, să
se determine dacă triunghiul respectiv este isoscel, echilateral sau oarecare.
 Să se determine dacă cele trei numere pot reprezenta laturile unui triunghi. Dacă da, să
se determine mărimile unghiurilor sale şi dacă este ascuţit-unghic sau obtuz-unghic.
 Să se afişeze media aritmetică, geometrică şi hiperbolică a celor trei valori.

2. Se citeşte n o valoare întreagă pozitivă.


 Să se determine dacă n este divizibil cu 3 dar nu este divizibil cu 11.
 Să se determine dacă n este pătrat sau cub perfect.
 Să se afişeze primele n pătrate perfecte.
 Să se determine numărul cuburilor perfecte mai mici decît n.
 Să se găsească primul număr prim mai mare decît n.
 Să se afişeze primele n numere prime: 2, 3, 5, 7,…, pn.
 Să se determine toate numerele de 4 cifre divizibile cu n.
 Să se determine suma cifrelor lui n.
 Să se afişeze răsturnatul lui n. (Ex: n=1993 => n_răsturnat =3991).
 Să se afişeze următorul triunghi de numere:
1
12
123
……..
123…n

3. Se citesc m, n două variabile întregi pozitive.


 Să se determine toate pătratele perfecte cuprinse între m şi n, inclusiv.
 Să se determine toate numerele prime cuprinse între m şi n.
 Să se determine toate numerele de 4 cifre care se divid atît cu n cît şi cu m.
 Să se determine c.m.m.d.c. al celor două numere folosind algoritmul lui Euclid.

4. Să se calculeze u20 , u30 , u50 ai şirului cu formula recursivă un=1/12un-1+1/2un-2 pentru


n>=2 şi u0=1, u1=1/2.

5. Se citeşte n gradul unui polinom şi şirul an, an-1, … , a1, a0 coeficienţilor unui polinom P.
 Se citeşte x, să se determine P(x).
 Se citesc x şi y, să se determine dacă polinomul P schimbă de semn de la x la y.
 Se citeşte a, să se determine restul împărţirii lui P la x-a.

6. Se citesc m, n gradele a două polinoame P şi Q, şi coeficienţii acestora. Să se determine


polinomul produs R=PxQ.

7. Se citeşte o propoziţie (şir de caractere) terminată cu punct.


 Să se determine cîte vocale şi cîte consoane conţine propoziţia.
 Să se afişeze propoziţia în ordine inversă şi cu literele inversate (mari cu mici).
 Să se afişeze fiecare cuvînt din propoziţie pe cîte o linie separată.

10
 Să se afişeze propoziţia rezultată prin inserarea în spatele fiecărei vocale ‘v’ a şirului
“pv” (“vorbirea găinească”).

8. Se citeşte m, n dimensiunea unei matrici A=(ai,j)mxn de valori reale.


 Se citesc l, c. Să se afişeze matricea obţinută prin eliminarea liniei l şi a coloanei c.
 Se citeşte n întreg pozitiv, să se afişeze matricea obţinută prin permutarea circulară a
liniilor matricii cu n poziţii.
 Să se determine suma elementelor pe fiecare linie şi coloană.
 Să se determine numărul elementelor pozitive şi negative din matrice.
 Să se determine linia şi coloana în care se află valoarea maximă din matrice.
 Să se determine linia care are suma elementelor maximă.

9. Se citesc m, n, p şi apoi se citesc două matrici A=(ai,j)mxn şi B=(bj,k)nxp.Să se determine


matricea produs C=AxB.

10. Se citeşte un fişier ce conţine mai multe linii de text.


 Să se afişeze linia care are lungime minimă.
 Să se afişeze liniile care conţin un anumit cuvînt citit în prealabil.
 Să se creeze un fişier care are acelaşi conţinut dar în ordine inversă.

Alte probleme interesante

1. Se citeşte x o valoarea reală. Să se determine radical(x) cu 5 zecimale exacte pe baza


şirului convergent xn=1/2 (xn-1+x / xn-1) cu x0>0 arbitrar ales.
2. Se citeşte x o valoarea reală şi k un număr natural. Să se determine radical de ordinul k din
x cu 5 zecimale exacte pe baza şirului convergent xn=1/k ( (k-1) xn-1+x / xn-1k-1) cu x0>0
arbitrar ales.
3. Să se determine c.m.m.m.c. a două numere m, n citite.
4. Se citeşte n, să se determine toate perechile (x, y) care au cmmmc(x,y)=n.
5. Se citesc a, b, c întregi pozitive, să se determine toate perechile întregi (x, y) care conduc
la egalitatea c=ax+by.
6. Se citeşte n o valoare întreagă pozitivă. Să se determine toate descompunerile în diferenţă
de pătrate a lui n.
7. Să se determine toate tripletele (i, j, k) de numere naturale ce verifică relaţia i2+j2+k2=n
unde n se citeşte.
8. Se citeşte n, să se afişeze toate numerele pitagoreice mai mici sau egale cu n.
9. Se citeşte n, să se determine toate numerele perfecte mai mici decît n. (Un număr este
perfect dacă este egal cu suma divizorilor săi, ex. 6=1+2+3.)
10. Se citeşte n, să se afişeze toate numerele de n cifre, formate numai cu cifrele 1 şi 2 şi care
se divid cu 2n.
11. Se citeşte n, să se afişeze toate numerele de n cifre care adunate cu răsturnatul lor dau un
pătrat perfect.
12. Se citeşte n întreg pozitiv, să se afişeze n transcris în baza 2.
13. Se citeşte n întreg pozitiv scris în baza 2, să se afişeze n transcris în baza 10.
14. Se citeşte n întreg pozitiv, să se afişeze n în transcripţia romană. (Ex: 1993=MCMXCIII ,
unde M=1000, D=500, C=100, L=50, X=10, V=5, I=1.)
15. Se citeşte n, să se afişeze descompunerea acestuia în factori primi.
16. Se citesc m, n numărătorul şi numitorul unei fracţii. Să se simplifice această fracţie.
17. Se citeşte n, să se afişeze toate posibilităţile de scriere a lui n ca sumă de numere
consecutive.

11
18. Se citeşte n şi k, să se afişeze n ca sumă de k numere distincte.
19. Se citeşte n, să se determine o alegere a semnelor + şi – astfel încît să avem relaţia
1±2±…±(n+1) ±n=0, dacă ea este posibilă.
20. Se citeşte n şi şirul de valori reale x1, x2, … , x n-1, xn ordonat crescător. Să se determine
distanţa maximă între două elemente consecutive din şir.
21. Se citeşte n gradul unui polinom şi şirul xn, xn-1, … , x1 soluţiilor reale a unui polinom P.
Să se determine şirul an, an-1, … , a1, a0 coeficienţilor polinomului P.
22. Se citesc două şiruri de valori reale x1, x2, … , x n-1, xn şi y1, y2, … , y m-1, ym ordonate
crescător. Să se afişeze şirul z1, z2, … , z n+m-1, zn+m rezultat prin interclasarea celor două
şiruri.
23. Un şir de fracţii ireductibile din intervalul [0,1] cu numitorul mai mic sau egal cu n se
numeşte şir Farey de ordinul n. De exemplu, şirul Farey de ordinul 5 (ordonat crescător)
este: 0/1, 1/5, ¼, 1/3, 2/5, ½, 3/5, 2/3, ¾, 4/5, 1/1. Să se determineşirul Farey de ordinul
n, cu n citit.
24. Se citeşte n şi S o permutare a mulţimii {1, 2, …, n}. Să se determine numărul de
inversiuni şi signatura permutării S.
25. Se citeşte n şi S o permutare a mulţimii {1, 2, …, n}. Să se determine cel mai mic număr k
pentru care Sk={1, 2, …, n}.
26. Fie M={1, 3, 4, …} mulţimea numerelor obţinute pe baza regulii R1, şi a regulii R2
aplicate de un număr finit de ori: R1) 1∈M R2 ) Dacă x∈M atunci y=2x+1 şi z=3x+1
aparţin lui M. Se citeşte n, să se determine dacă n aparţine mulţimii M fără a genera toate
elementele acesteia mai mici decît n.
27. Se citeşte n, k şi o matrice A=(ai,j) nxn pătratică. Să se determine Ak.
28. Se citeşte n şi o matrice A=(ai,j) nxn pătratică. Să se determine d determinantul matricii A.
29. Se citeşte n şi cele n perechi (xi, yi) de coordonate a n puncte Pi în plan. Să se determine
care dintre cele n puncte poate fi centrul unui cerc acoperitor de rază minimă.
30. Să se determine, cu 5 zecimale exacte, rădăcina ecuaţiei x3+x+1=0 care există şi este unică
în intervalul [-1,1].
31. Se citeşte n şi şirul de valori reale x1, x2, … , x n-1, xn. Să se determine poziţia de început şi
lungimea celui mai mare subşir de numere pozitive.
32. Se citeşte n, să se afişeze binomul lui Newton: (x+y)n.
33. Se citeşte n, să se afişeze binomul lui Newton generalizat:
(x1+x2+…+xp) =Σn!/(n1!n2!…np!) x1 1x2 2…xp p pentru n1+n2+…+np=n şi ni>0, i=1,p.
n n n n

34. Se citeşte n, să se determine descompunerea lui n ca sumă de numere Fibonacci distincte.


(Fn=Fn-1+Fn-2 pentru n>1 şi F1=1, F0=0).
35. Avem la dispoziţie următoarele trei operaţii care se pot efectua asupra unui număr n: O1) i
se adaugă la sfîrşit cifra 4; O2) i se adaugă la sfîrşit cifra 0; O3) dacă n este par se împarte
la 2. Să se afişeze şirul operaţiilor care se aplică succesiv, pornind de la 4, pentru a obţine
un n care se citeşte.
36. Fie funcţia lui Ackermann definită astfel: A(i,n)=n+1 pentru i=0; A(i,n)=A(i-1,1) pentru
i>0 şi n=0; A(i,n)=A(i-1,A(i,n-1)) pentru i>0 şi n>0. Care este cea mai mare valoare k
pentru care se poate calcula A(k,k) ?
37. Să se determine suma tuturor numerelor formate numai din cifre impare distincte.
38. Scrieţi o funcţie recursivă pentru a determina c.m.m.d.c. a două numere m şi n.
39. Scrieţi o funcţie recursivă pentru a calcula an pe baza relaţiei an=(ak)2 pentru n=2k, şi
an=a(ak)2 pentru n=2k+1.
40. Scrieţi o funcţie recursivă pentru a determina prezenţa unui număr x într-un şir de valori
reale x1, x2, … , x n-1, xn ordonate crescător folosind algoritmul căutării binare.

12
41. Scrieţi o funcţie recursivă pentru a determina o aşezare a 8 turnuri pe o tablă de şah astfel
încît să nu se atace între ele. (Tabla de şah va fi reprezentată printr-o matrice pătratică de
8x8).
42. Să se determine peste cîţi ani data de azi va cădea în aceeaşi zi a săptămînii.
43. Avem la dispoziţie un fişier ce conţine numele, prenumele şi media tuturor studenţilor din
grupă.
 Să se afişeze studentul cu cea mai mare medie.
 Să se afişeze toţi studenţii bursieri.
 Să se afişeze studentul care are media cea mai apropiată de media aritmetică a
mediilor pe grupă.
 Să se afişeze toţi studenţii din prima jumătate a alfabetului.
 Să se afişeze toţi studenţii în ordine inversă decît cea din fişier.
 Să se creeze un fişier catalog care să conţină aceleaşi informaţii în ordinea alfabetică a
numelui.
44. Avem la dispoziţie două fişiere ce conţin numele, prenumele şi media tuturor studenţilor
din cele două grupe ale anului în ordinea descrescătoare a mediilor.
 Să se afişeze toţi studenţii din ambele grupe care au media mai mare decît media
anului.
 Să se creeze prin interclasare un fişier totalizator care conţine toţi studenţii anului în
ordinea descrescătoare a mediilor.

13
Probleme dificile

La fel ca matematica şi informatica are o mulţime de probleme foarte dificile care îşi
aşteaptă încă rezolvarea. Asemănarea cu matematica ne interesează mai ales în privinţa unui
aspect "capcană" asupra căruia dorim să atragem atenţia aici.
Enunţurile problemelor dificile sau foarte dificile de informatică este, în 99% din
cazuri, foarte simplu şi poate fi citit şi înţeles de oricine. Acest fapt consituie o capcană sigură
pentru începători. Dacă în matematică lucrurile nu stau aşa, asta se datorează numai faptului
că studiul matematicii are vechime şi problemele, împreună cu dificultăţile lor, sînt ceva mai
bine cunoscute. În informatică nu avem însă aceeaşi situaţie. Ba chiar se întîmplă că probleme
foarte dificile sînt amestecate în culegerile de probleme de informatică printre probleme
uşoare.
Acest paragraf îşi propune să pună în gardă în privinţa dificultăţii problemelor oferind
o mică iniţiere în acest domeniu şi să umple lacuna ce mai există încă la ora actuală în cultura
de specialitate.
Dificultatea problemelor de programare a căror enunţuri urmează este considerată
maximă de teoreticienii informaticii (ele se numesc probleme NP-complete). Este foarte
posibil să existe aceste probleme în unele culegeri de programare. Ele sînt depăşite în
dificultate doar de problemele insolvabile algoritmic.
Spre deosebire de matematică, dificultatea problemelor de informatică nu este dată de
faptul că nu se cunoaşte un algoritm de rezolvare a lor, ci datorită faptului că nu se cunoaşte
un algoritm eficient de rezolvare a lor. Existenţa unei metode de proiectare a algoritmilor atît
de general valabilă, cum este metoda back-tracking, face ca prea puţine probleme cu care ne
putem întîlni să nu aibă o soluţie. Dar, întrucît în cazul metodei back-tracking, căutarea
soluţiei se face într-un mod exhaustiv (se caută peste tot, pentru ca să fim siguri că nu lăsăm
nici o posibilitate neexplorată), durata căutării are o creştere exponenţial-proporţională cu
dimesiunea datelor de intrare. De exemplu, timpul de căutare care depinde de valoarea de
intrare n poate avea o expresie de forma T(n)=c⋅2n secunde, unde c este un factor de
proporţionalitate ce poate varia, să zicem, de la c=62.8 cînd algoritmul este executat pe un
calculator sau c=12.5 cînd el este rulat pe un calculator de cinci ori mai performant. Dar,
indiferent de calculator, pentru n=100 avem 2100=(210)10≈(103)10=1030, deci timpul măsurat în
secunde are ordinul de mărime mai mare de 30. Cea mai largă estimare pentru vîrsta
Universului nostru nu depăşeşte 20 mildiarde de ani ceea ce transformat în secunde conduce
la un ordin de mărime mai mic de 20. Deci, chiar şi pentru valori mici ale lui n (de ordinul
sutelor) am avea de aşteptat pentru găsirea soluţiei de 10 miliarde de ori mai mult decît a
trecut de la Big Bang încoace. Se pune problema dacă în această situaţie pot fi considerate
astfel de programe ca rezolvări rezonabile, doar pentru că ele găsesc soluţia în cazurile în care
n=2, 3, 4, …, 10 ?
Exemplele următoare sînt doar cîteva, cele mai uzuale, dintr-o listă cunoscută ce
conţine la ora actuală peste şase sute de astfel de probleme. Pentru fiecare dintre aceste
probleme nu se cunosc alte soluţii decît inutilii algoritmi de gen back-tracking. În listă apare
des noţiunea de graf, aşa că o vom introduce în continuare cît mai simplu cu putinţă: printr-un
graf se înţelege o mulţime de vîrfuri şi o mulţime de muchii care unesc unele vîrfuri între ele.
Orice hartă rutieră, feroviară sau de trafic aerian schematizată reprezintă desenul unui graf.

1. Problema partiţionării sumei. Fie C un întreg pozitiv şi d1, d2, …, dn o mulţime de n


valori întregi pozitive. Se cere să se găsească o partiţionare a mulţimii d1, d2, …, dn astfel
încît suma elementelor partiţiei să fie exact C.

14
2. Problema rucsacului. Avem un rucsac de capacitate întreagă pozitivă C şi n obiecte cu
dimensiunile d1, d2, …, dn şi avînd asociate profiturile p1, p2, …, pn (în caz că ajung în
rucsac). Se cere să se determine profitul maxim ce se poate obţine prin încărcarea
rucsacului (fără ai depăşi capacitatea).
3. Problema colorării grafului. Să se determine numărul minim de culori (numărul
cromatic) necesar pentru colorarea unui graf astfel încît oricare două vîrfuri unite printr-o
muchie (adiacente) să aibă culori diferite.
4. Problema împachetării. Presupunînd că dispunem de un număr suficient de mare de cutii
fiecare avînd capacitatea 1 şi n obiecte cu dimensiunile d1, d2, …, dn, cu 0<di<1, se cere să
se determine numărul optim (cel mai mic) de cutii necesar pentru împachetarea tutror
celor n obiecte.
5. Problema comisului voiajor. (varianta simplificată) Dîndu-se o hartă (un graf), se cere să
se găsească un circuit (un şir de muchii înlănţuite) care trece prin fiecare vîrf o singură
dată.

Majoritatea acestor probleme apar ca probleme centrale la care se reduc în ultimă


instanţă problemele concrete ale unor domenii capitale ale economiei şi industriei, cum sînt de
exemplu planificarea investiţiilor, planificarea împrumuturilor şi eşalonarea plăţii dobînzilor,
alocarea şi distribuirea resurselor primare (mai ales financiare) etc. Pentru nici una din aceste
probleme strategice nu se cunoaşte un algoritm optim de rezolvare, ci doar soluţii
aproximative. Dacă s-ar cunoaşte algoritmii de soluţionare optimă atunci majoritatea
sectoarelor şi proceselor macro- şi micro-economice ar putea fi conduse în timp real şi optim
cu calculatorul, fără a mai fi necesară prezenţa umană.
Un exemplu cert de domeniu care s-a dezvoltat extraordinar şi în care rolul soft-ului a
fost esenţial este chiar domeniul construcţiei de calculatoare, mai ales domeniul proiectării şi
asamblării de micro-procesoare. Schema electronică internă de funcţionare a unui
microprocesor din familia Pentium, dacă ar fi desenată clasic, ar ocupa o planşă de dimensiuni
5x5 metri. Este evident că numai un soft de proiectare şi cablare performant mai poate
controla şi stăpîni super-complexitatea rezultată. Puţină lume ştie însă că astfel de programe
de proiectare performante au putut să apară numai datorită faptului că problema ce stă în
spatele funcţionării lor, problema desenării grafurilor planare, nu se află pe lista de mai sus a
problemelor foarte dificile ale informaticii.

15
Probleme nesoluţionate încă

Aşa cum s-a putut constata în paragraful anterior, există multe probleme în informatică
pentru care încă nu se cunosc soluţii eficiente. În continuare vom oferi o listă de probleme
nesoluţionate încă. De fapt, ele apar mai ales în matematică, fiind cunoscute sub numele de
conjecturi, şi au toate ca specific un fapt care este de mare interes pentru programatori.
Incertitudinea asupra lor ar putea fi definitiv înlăturată nu numai prin demonstraţie
matematică ci şi cu ajutorul formidabilei puteri de calcul a computerelor. Astfel, fiecare din
aceste conjecturi numerice ar putea fi infirmată (concluzia ar fi atunci că respectiva
conjectură este falsă) dacă i s-ar găsi un contraexemplu. Este necesar doar să se găsească un
set de numere pentru care propoziţia respectivă să fie falsă. Ori, acest efort nu este la
îndemâna niciunui matematician dar este posibil pentru un programator. El nu are decît să
scrie un program eficient şi să pună calculatorul să caute un contra-exemplu.
Fiecare problemă conţine aceeaşi capcană ca şi în problemele paragrafului anterior:
algoritmii de căutare a contra-exemplelor pot fi concepuţi rapid, relativ simpli şi cu efort de
programare redus (de exemplu, prin trei-patru cicluri for imbricate sau printr-o soluţie gen
back-tracking) dar ei vor deveni în scurt timp total ineficienţi şi vor conduce la programe mari
consumatoare de timp. De aceea, problemele din acest paragraf trebuie tratate cu multă
atenţie. Abordarea acestui tip de probleme cere din partea programatorului un anumit grad de
măiestrie.

Rezolvînd numai una dintre ele veţi fi recompensaţi pe măsură: riscaţi să deveniţi celebri!

1. Conjectura lui Catalan. Singurele puteri naturale succesive sînt 8=23 şi 9=32.

Observaţie: într-o exprimare matematică riguroasă, singura soluţie în numere naturale m, n, p,


q a ecuaţiei nm+1=pq este n=2, m=3, p=3 şi q=2.
Comentariu: avem şirul numerelor naturale 1, 2, 3, 4, 5,…; încercuind toate puterile de gradul
2: 1, 4, 9, 16, 25,… apoi toate cele de gradul 3: 1, 8, 27, 64, 125, … apoi cele de grad 4, 5, …
vom constata că singurele două numere încercuite alăturate sînt 8 şi 9 ! Adică puterile
obţinute, cu cît sînt mai mari, cu atît au tendinţa să se "împrăştie" şi să se "distanţeze" unele
de altele tot mai mult.

2. Conjectura cutiei raţionale. Nu se cunoaşte existenţa unei cutii paralelipipedice avînd


lungimile celor trei laturi, ale celor trei diagonale ale feţelor şi a diagonalei principale
întregi.

Observaţie: într-o exprimare matematic riguroasă, nu se cunoaşte să existe trei întregi a, b, c


astfel încît a2+b2, b2+c2 , c2+a2 şi a2+b2+c2 să fie toate patru pătrate perfecte.
Comentariu: în multe subdomenii ale construcţiilor, de exemplu să ne gîndim la stîlpii de
înaltă tensiune ridicaţi pe vîrfuri înalte de munte şi asamblaţi în întregime "la faţa locului"
numai din bare îmbinate cu şuruburi (fără sudură), este de mare interes ca dintr-un număr cît
mai mic de subansamble simple (un fel de "cărămizi") să se asambleze obiecte mari cu cît mai
multe configuraţii. Evident, dimensiunile obiectelor rezultate vor avea mărimea ca o
combinaţie întreagă ale dimensiunilor subansamblelor iniţiale. După cum rezultă însă din
conjectură, se pare că este imposibil să se construiască scheletul întărit (pe diagonale) al unei
cutii paralelipipedice din bare de lungimi tipizate. Cel puţin una din diagonale necesită
ajustarea lungimii unei bare.

16
3. Problema umplerii pătratului unitate. Întrebare: este posibil ca mulţimea
dreptunghiurilor de forma 1/k x 1/(k+1), pentru fiecare k întreg pozitiv, să umple în
întregime şi fără suprapuneri pătratul unitate, de latură 1x1 ?

Observaţie: este evident că suma infinită a ariilor dreptunghiurilor este egală cu aria pătratului
unitate. Avem Σk>01/(k(k+1))=Σk>0(1/k-1/(k+1))=1.
Comentariu: aparent, descoperirea dezvoltărilor în serie pare să fi plecat de la unele evidente
propietăţi geometrice, uşor de sesizat chiar din desene simple în care valorilor numerice li se
asociază segmente de lungimi corespunzătoare. Iată însă o surpriză în această situaţie: suma
seriei numerice este evidentă analitic însă reprezentarea geometrică a "fenomenului" este
"imposibilă".

4. Conjectura fracţiilor egiptene (atribuită lui Erdös şi Graham). Orice fracţie de forma
4/n se descompune ca sumă de trei fracţii egiptene (de forma 1/x).

Observaţie: într-o exprimare matematic riguroasă, pentru orice n natural există trei valori
naturale, nu neapărat distincte, x, y, şi z astfel încît 4/n=1/x+1/y+1/z.
Comentariu: este încă un mister motivul pentru care egiptenii preferau descompunerea
facţiilor numai ca sumă de fracţii egiptene. Descoperiseră ei această descompunere minimală
a fracţiilor de forma 4/n ? Dar mai ales, ce procese fizice reale erau astfel mai bine modelate ?
Înclinăm să credem că există o legătură între fenomenele fizice ondulatorii, transformata
Fourier şi fracţiile egiptene.

5. Problema punctului raţional. Există un punct în plan care să se afle la o distanţă


raţională de fiecare din cele patru vîrfuri ale pătratului unitate ?

Observaţie: dacă considerăm un pătrat unitate avînd vîrfurile de coordonate (0,0), (1,0), (0,1)
şi (1,1) atunci se cere găsirea unui punct (x,y) astfel încît x2+y2, (x-1)2+y2, x2+(y-1)2 şi (x-
1)2+(y-1)2 să fie toate patru pătrate perfecte. Atenţie, x şi y nu este obligatoriu să fie întregi.
Acest fapt ridică foarte serioase probleme la proiectarea unui algoritm de căutare a unui astfel
de punct (x,y).
Comentariu: la fel ca şi în cazul cutiei raţionale, se pare că există limitări serioase şi
neaşteptate în încercarea de optimizare a numărului de subansamble necesare pentru
construierea scheletelor sau cadrelor de susţinere. Se pare că cele două dimensiuni pe care
geometria plană se bazează conduce la o complexitate inerentă neaşteptat de mare.

6. Problema sumei de puteri. Care este suma seriei de inverse de puteri


1/1+1/23+1/33+1/43+1/53+… ?

Observaţie: se cere să se spună către ce valoare converge seria Σk>01/k3 sau Σk>0k-3. Se ştie că
în cazul în care în locul puterii a 3-a punem puterea a 2-a seria converge la π 2/6, în cazul în
care în locul puterii a 3-a punem puterea a 4-a seria converge la π 4/90.
Comentariu: deşi pare a fi o problemă de analiză matematică pură deoarece ni se cere să
găsim expresia sintetică şi nu cea numerică aproximativă a sumei seriei, există însă uluitoare
descoperiri asemănătoare ale unor formule de analiză numerică sau chiar dezvoltări în serie
(cea mai celebră fiind cea a cifrelor hexazecimale ale lui π) făcute cu ajutorul calculatorului
prin calcul simbolic.

7. Problema ecuaţiei diofantice de gradul 5. Există a, b, c, and d întregi pozitivi astfel


încît a5+b5=c5+d5 ?

17
Observaţie: Se cunoaşte că în cazul în care puterea este 3 avem soluţia: 13+123=93+103 iar în
cazul în care puterea este 4 avem soluţia: 1334+1344=594+1584 .
Comentariu: căutarea unor algoritmi generali de rezolvare a ecuaţiilor diofantice a condus la
importante descoperiri în matematică dar şi în informatică. De exemplu, celebrul
matematician Pierre Fermat, “stîrnit” fiind de problemele conţinute în lucrarea Arithmetika a
matematicianului antic Diofant din Alexandria (de unde şi numele ecuaţiilor diofantice), a
descoperit în 1637 faimoasa sa teoremă: Ecuaţia diofantică xn+yn=zn nu admite soluţie pentru
n>2. Dar tot în aceeaşi perioadă a descoperit şi faptul că cea mai mică soluţie a ecuaţiei
diofantice x2 - 109*y2 = 1 este perechea x=158 070 671 986 249 şi y= 15 140 424 455 100.
Dacă încercăm doar să verificăm această soluţie fără ajutorul calculatorului ne putem da
seama de performanţele pe care le-a realizat Fermat. În informatică este acum cunoscut şi
demonstrat că este imposibil să se construiască un algoritm general pentru rezolvarea
ecuaţiilor diofantice.

8. Problema celor 13 oraşe. Puteţi localiza 13 oraşe pe o planetă sferică astfel încît distanţa
minimă dintre oricare două dintre ele să fie cît mai mare cu putinţă ?

Observaţie: de fapt nu se cunoaşte cît de mult poate fi mărită


distanţa minimală ce se obţine dintre cele 78 de distanţe (date
de cele 78=C213 de împerecheri posibile de oraşe).
Comentariu: dacă s-ar cere localizarea a doar 12 puncte pe
sferă, nu este greu de arătat că aşezarea care îndeplineşte
condiţia cerută este în vîrfurile unui icosaedru (vezi figura
alăturată). În acest caz, distanţa minimă maximizată este egală
cu latura icosaedrului. Este greu de crezut că în cazul
descoperirii aşezării a 13 puncte pe sferă se poate porni tocmai
de la icosaedru. Evident că în rezolvarea aplicativ-practică a
acestui tip de probleme nesoluţionate geometric pînă în
prezent rolul programatorului poate fi capital. La ora actuală pentru astfel de situaţii se oferă
soluţii aproximative. Acestea constau din algoritmi care încearcă să aproximeze cît mai exact
soluţia optimă într-un timp rezonabil de scurt. Evident că în aceste condiţii algoritmii de
căutare exhaustivă (gen back-tracking) sînt cu totul excluşi.

9. Conjectura lui Collatz. Se pleacă de la un n întreg pozitiv. Dacă n este par se împarte la
doi; dacă n este impar se înmulţeşte cu trei şi i se adună unu. Repetînd în mod
corespunzător doar aceşti doi paşi se va ajunge întotdeauna la 1 indiferent de la ce valoare
n se porneşte ?

Observaţie: de exemplu, pornind de la n=6 obţinem în 8 paşi şirul valorilor: 6, 3, 10, 5, 16, 8,
4, 2, 1.
Comentariu: valoarea finală 1 este ca o "gaură neagră" care absoarbe în final şirul obţinut.
"Raza" de-a lungul căreia are loc "căderea" în gaura neagră 1 este dată mereu de şirul
puterilor lui 2: 2, 4, 8, 16, 32, 64, … cu ultima valoare de forma 3k+1, adică 4, 16, 64, 256,
…. Se pare că valorile obţinute prin cele două operaţii nu pot "să nu dea" nicicum peste acest
şir care le va face apoi să "cadă în gaura neagră" 1.

10. Problema înscrierii pătratului. Dîndu-se o curbă simplă închisă în plan, vom putea
întotdeauna găsi patru puncte pe curbă care pot să constituie vîrfurile unui pătrat ?

18
Observaţie: în cazul curbelor închise regulate (ce au axe de simetrie: cerc, elipsă, ovoid) nu
este greu de arătat prin construire efectivă că există un pătrat ce se "sprijină" pe curbă. Pare
însă de nedovedit acelaşi fapt în cazul unor curbe închise foarte neregulate. Găsirea celor
patru puncte, într-o astfel de situaţie, este de neimaginat fără ajutorul calculatorului.
Comentariu: o consecinţă surprinzătoare a acestei conjecturi este faptul că pe orice curbă de
nivel (curbă din teren care uneşte punctele aflate toate la aceaşi altitudine) am putea găsi patru
puncte de sprijin pentru o platformă pătrată (un fel de masă) perfect orizontală, de mărime
corespunzătoare. Acest fapt ar putea să explice ampla răspîndire a meselor cu patru picioare în
detrimentul celor cu trei: dacă îi cauţi poziţia, cu siguranţă o vei găsi şi o vei putea aşeza pe
toate cele patru picioare, astfel masa cu patru picioare va oferi o perfectă stabilitate şi va sta
perfect orizontală, pe cînd cea cu trei picioare deşi stă acolo unde o pui din prima (chiar şi
înclinată) nu oferă aceeaşi stabilitate.

În speranţa că am reuşit să stîrnim interesul pentru astfel de probleme nesoluţionate


încă şi care sînt grupate pe Internet în liste cuprinzînd zeci de astfel de exemple, încheiem
acest paragraf cu următoarea constatare: descoperirile deosebite din matematica actuală au
efecte rapide şi importante nu numai în matematică ci şi în informatică. Să oferim doar un
singur exemplu de mare interes actual: algoritmii de încriptare/decriptare cu cheie publică, atît
de folosiţi în comunicaţia pe Internet, se bazează în întregime pe proprietăţile matematice ale
divizibilităţii numerelor prime.
Ceea ce este interesant şi chiar senzaţional este faptul că în informatică nevoia de
programe performante a condus la implementarea unor algoritmi care se bazează pe cele mai
noi descoperiri din matematică, chiar dacă acestea sînt încă în stadiul de conjecturi.
Se pune problema daca putem avea încredere în astfel de programe. Putem acorda o
totală încredere acestor algoritmi dar numai în limitele "orizontului" atins de programele de
verificare a conjecturii folosite. Dacă programul de verificare a verificat conjectura numerică
pe intervalul 1-1030 atunci orizontul ei de valabilitate este 1030. Domeniile numerice pe care le
pot acoperi calculatoarele actuale sînt oricum foarte mari şi implicit oferă o precizie suficientă
pentru cele mai multe calcule cu valori extrase din realitatea fizică.

19
Probleme insolvabile algoritmic

Acest paragraf special are ca scop trezirea interesului şi pasiunii pentru informatică
celor care pot acum să vadă cît de deosebite sînt descoperirile şi rezultatele din acest domeniu,
precum şi punerea în gardă a celor care, în entuziasmul lor exagerat, îşi închipuie că pot
programa calculatorul să facă orice treabă sau să rezolve orice problemă. Aşa cum am văzut şi
în paragraful ce prezintă problemele dificile ale informaticii, enunţurile problemelor foarte
dificile sau chiar insolvabile sînt foarte simple şi pot uşor constitui o capcană pentru
necunoscători.

1. Problema Stopului. Nu există un algoritm universal valabil prin care să se poată


decide dacă execuţia oricărui algoritm se opreşte vreodată sau nu.

Comentariu: acesta este cel dintîi şi cel mai celebru exemplu de problemă insolvabilă.
Demonstraţia riguroasă a acestui fapt a fost dată pentru prima dată în 1936 de inventatorul
calculatorului actual matematicianul englez Alan Mathison Turing. Odată existînd această
demonstraţie, multe din următoarele probleme insolvabile algoritmic s-au redus la aceasta.
Implicaţiile practice, teoretice şi filozofice ale problemei Stopului sînt foarte importante atît
pentru informatică cît şi pentru matematică. Astfel, două consecinţe strategice ale problemei
Stopului sînt:
1. nu poate exista un calculator oricît de puternic cu ajutorul căruia să se poată decide asupra
comportamentului viitor al oricărui alt calculator de pe glob;
2. nu poate să existe în matematică o metodă generală de demonstrare inductivă-logică a
propoziţiilor matematice (se închide în acest fel o mai veche căutare a matematicienilor şi
logicienilor cunoscută sub numele de Entscheidungs Problem sau Problema deciziei).

2. Problema ecuaţiilor diofantice. Nu există o metodă generală (un algoritm) de aflare a


soluţiilor întregi ale unui sistem de ecuaţii diofantice.

Comentariu: sistemele de ecuaţii diofantice sînt sistemele de ecuaţii algebrice de mai multe
variabile cu coeficienţi întregi şi cărora li se caută soluţii întregi. De exemplu, a fost nevoie de
ajutorul calculatorului pentru a se descoperi cea mai mică soluţie a ecuaţiei diofantice
p4+q4+r4=s4 şi care este cvadrupletul p=95600, q=217519, r=414560, s=422461
(infirmîndu-se în acest fel "conjectura" lui Leonard Euler care în 1796 a presupus că această
ecuaţie diofantică nu are soluţii întregi). Această problemă ce cere o metodă generală de
rezolvare a ecuaţiilor diofantice este cunoscută sub denumirea de Problema a 10-a a lui
Hilbert.

3. Problema acoperirii planului (Problema pavajului sau Problema croirii). Fiind dată o
mulţime de forme poligonale, nu există o metodă generală (un algoritm) care să decidă
dacă utilizând aceste forme este posibilă acoperirea completă a planului (fără suprapuneri
şi goluri).

Comentariu: în practică este mult mai importantă problema croirii care cere să se decupeze
fără pierderi un set cît mai mare de forme date (croiuri) dintr-o bucată iniţială de material
oricît de mare. Este, de asemenea, demonstrat că problema rămîne insolvabilă algoritmic chiar
şi atunci cînd formele poligonale sînt reduse la poliomine (un fel de "mozaicuri") care se
formează doar pe o reţea rectangulară caroiată. Iată cîteva exemple de mulţimi formate dintr-o

20
singură poliomină şi, alăturat, răspunsul la întrebarea dacă cu ele se poate acoperi planul sau
nu:

DA NU DA

4. Problema şirurilor lui Post. Se dau două mulţimi egale de şiruri finite de simboluri ce
sînt împerecheate astfel: un şir dintr-o mulţime cu şirul corespunzător din a doua mulţime.
Nu există un algoritm general prin care să se decidă dacă există o ordine de concatenare
a şirurilor (simultan din cele două mulţimi) astfel încît cele două şiruri lungi pereche
rezultate să fie identice.

Comentariu: de exemplu, fie A={ 101, 010, 00 } şi B={ 010, 10, 001 } cele două mulţimi de
şiruri de simboluri (pentru uşurinţă au fost alese simbolurile binare 1 şi 0). Perechile
corespunzătoare de şiruri sînt 1.(101,010), 2.(010,10) şi 3.(00,001). Observăm că şirurile
pereche pot avea lungimi diferite (ca în perechile 2 şi 3). În continuare, pentru a vedea cum se
procedează, cele două şiruri pereche rezultante prin concatenare le vom scrie unul deasupra
celuilalt sesizînd cum avansează procesul de egalizare a lor. Punctele sînt intercalate doar
pentru a evidenţia perechile, ele nu contribuie la egalitate:

00. Concatenarea poate începe doar cu 00.101. Obligatoriu urmează perechea 1-a
001. perechea a 3-a,00 de "sus" ⊂ 001 de "jos" 001.010. singura care începe cu 1 "sus".
00.101.00. Dacă am continua cu perechea 00.101.010 … nu s-ar obţine rezultatul
001.010.001. a 3-a … 001.010.10 final oferit de perechea 2-a !

5. Problema cuvintelor "egale". Se dă un anumit număr de "egalităţi" între cuvinte.


Bazîndu-ne pe aceste "egalităţi" se pot obţine unele noi substituind apariţiile cuvintelor
dintr-o parte a egalului cu cele din cealaltă parte. Nu există un algoritm general de a
decide dacă un cuvînt oarecare A poate fi "egal" cu un altul B.

Comentariu: de exemplu, fie următoarele cinci egalităţi (citiţi-le în limba engleză) EAT=AT,
ATE=A, LATER=LOW, PAN=PILLOW şi CARP=ME. Este CATERPILLAR egal cu
MAN? Iată şirul egalităţilor iterate care ne poate oferi răspunsul: CATERPILLAR =
CARPILLAR =CARPILLATER =CARPILLOW= CARPAN= MEAN= MEATEN=
MATEN= MAN.
Dar de la CARPET putem ajunge la MEAT? Întrucît se vede că numărul total de A-uri plus
W-u ri şi M-uri nu se poate modifica prin nici o substituţie şi întrucît CARPET are un A
(adică numărul asociat este 1) iar MEAT are un A şi un M (deci 2), rezultă că această egalitate
nu este permisă.
Mai mult, se ştie că există liste particulare de cuvinte pentru care nu poate exista un algoritm
ce decide dacă două cuvinte sînt egale sau nu. Iată o astfel de listă de şapte egalităţi: AH=HA,
OH=HO, AT=TA, OT=TO, TAI=IT, HOI=IH şi THAT=ITHT.

Numărul problemelor cunoscute ca fiind insolvabile algoritmic este destul de mare.


Cele mai multe probleme provin din matematică, subdomeniul matematicii care studiază
aceste probleme se numeşte Matematica nerecursivă. De aceea ele pot fi întîlnite mai ales sub

21
numele de probleme nedecidabile sau probleme nerecursive, în enunţul lor cuvîntul algoritm
fiind înlocuit mai ales cu cuvintele metodă generală.
Studierea acestui domeniu a creat condiţii pentru apariţia de noi direcţii de cercetare
prin care se încearcă explicarea raţionamentelor matematice ba chiar se încearcă descoperirea
limitelor raţiunii umane în general. Unii oameni de ştiinţă contemporani, cum este celebrul
matematician-fizician englez Roger Penrose, depun eforturi mari pentru a oferi o
demonstraţie matematică riguroasă pentru ipoteza că, în cele din urmă şi în esenţă,
raţionamentele umane nu sînt algoritmice, nici măcar cele matematice. După părerea lui
Penrose mintea umană nu poate fi asimilată cu un calculator ci este mai mult decît atît şi nu
vor putea exista vreodată calculatoare sau roboţi mai inteligenţi decît oamenii.

22
Capitolul 3
DIVIDE ET IMPERA

Prima abordare în proiectarea aloritmilor, divide-et-impera, este denumită


după strategia genială utilizată de împăratul francez Napoleon în bătălia de la
Austerlitz în 2 decembrie 1805. În această bătălie armata lui Napoleon s-a confruntat
cu o armată formată din austrieci şi ruşi. Această armată austro-rusă depăşea numeric
armata franceză cu circa 15000 de soldaţi. Armata austro-rusă a lansat un atac masiv
împotriva flancului drept al francezilor. Anticipându-le atacul, Napoleon a atacat spre
centrul lor şi i-a împărţit în două. Deoarece cele două armate mai mici nu reprezentau
separat nici o problemă pentru Napoleon, fiecare au avut pierderi mari şi au fost
forţate să se retragă. Divizând armata numeroasă în două armate mai mici şi
concerindu-le individual pe fiecare dintre acestea, Napoleon a reuşit să cucerească o
armata mai mare decât a lui.
Metoda Divide-et-impera aplică aceeaşi strategie asupra unei probleme. Adică,
împarte o problemă în două sau mai multe mai mici. Problemele mai mici sunt de
obieci cazuri ale problemei originale. Dacă pot fi obţinute uşor soluţii pentru
problemele mai mici, soluţia pentru problema originală poate fi obţinută prin
combinarea acestor soluţii. Dacă problemele mai mici sunt încă prea mari pentru a
putea fi soluţionate uşor, atunci ele pot fi împărţite în alte probleme mai mici. Acest
proces de divizare a problemelor continuă până când sunt atât de mici încât soluţia lor
să se obţină uşor.
Metoda Divide-et-impera realizează o abordare top-down. Adică, soluţia
pentru un caz de la nivelul cel mai înalt al unei probleme este obţinută prin parcurgere
de sus în jos şi obţinerea de soluţii la cazuri mai mici. Aceasta este metoda utilizată de
rutinele recursive. Atunci când se scriu rutine recursive, programatorul are în vedere
nivelul de soluţionare a problemei şi lasă sistemul să se ocupe de detaliile de obţinere
a soluţiei (prin intermediul manipulării stivei). Când se dezvoltă un algoritm divide-
et-impera, de obicei utilizăm aceeaşi abordare şi scriem algoritmul ca o rutină
recursivă. Apoi, se poate realiza o versiune iterativă mai eficientă a algoritmului.
În continuare, vom introduce metoda divide-et-impera cu câteva exemple,
începând cu căutarea binară.

Căutare binară
Problema căutării unei valori particulare într-un şir de elemente este o
problemă fundamentală în proiectarea algoritmilor. Această problemă în cazul general
poate fi formulată după cum urmează: „Să se determine dacă numărul x este în şirul S
de n elemente. Dacă da să se specifice poziţia în şir, dacă nu poziţia va fi 0.”
Pentru rezolvarea acestei probleme cea mai simplă abordare este căutarea
secvenţială, care începând de la primul element din şirul S, îl compară succesiv pe x
cu fiecare element al şirului S până se găseşte un element egal cu x, sau până când se
termină elementele din S. Daca x este găsit se memorează poziţia iar dacă nu este
găsit se iniţializează poziţia egală cu 0. Acest algoritm nu necesită ca elementele
şirului S să fie ordonate. În continuare se prezintă algoritmul descris mai sus.

Intrări: numărul întreg pozitiv n (numărul de elemente ale şirului), şirul S


indexat de la 1 la n şi un număr x.
Ieşiri: pozitie, poziţia numărului x în şirul S (0 dacă x nu este în şir).

cautare_secventiala;
BEGIN
var n,S(1..n),x,pozitie:int;
read n, S(1..n), x;
pozitie:=1;
while pozitie<=n AND S(pozitie)<>x
pozitie:=pozitie+1;
if pozitie>n then pozitie:=0;
write pozitie;
END

O abordare mult mai eficientă, mai ales ca timp de execuţie în cazul şirurilor
cu număr foarte mare de elemente, este căutarea binară. Aceasta se poate aplica pentru
şiruri S ordonate, să presupunem în ordine crescătoare. Cu modificări minore se poate
realiza algoritmul pentru cazul şirului S ordonat în ordine descrescătoare.
Prezentat în cuvinte, acest algoritm de căutare binară la început compară pe x
cu elementul din mijlocul şirului. Dacă sunt egale, atunci am găsit poziţia lui x şi
algoritmul se termină. Dacă x este mai mic decât elementul din mijlocul şirului S
atunci înseamnă că x ar putea să fie în prima jumătate (din stânga) a şirului şi
algoritmul continuă căutarea în acelaşi mod în prima jumătate a şirului (adică, x este
comparat cu elementul de la mijlocul subşirului. Dacă este egal, poziţia lui x a fost
găsită, dacă nu se continuă.). Dacă x este mai mare decât elementul din mijlocul
şirului, atunci x ar putea fi în a doua jumătate (din dreapta) a şirului. Aceşti paşi sunt
repetaţi, înjumătăţind treptat şirul S, până când se găseşte poziţia lui x în şir sau până
când se determină că x nu face parte din S. În continuare se prezintă scris în
pseudocod algoritmul descris mai sus.

Intrări: numărul întreg pozitiv n, şirul S ordonat crescător şi un număr x.


Ieşiri: pozitie, poziţia numărului x în şirul S (0 dacă x nu este în şir).

cautare_binara;
BEGIN
var n,S(1..n),x,pozitie,low,high,mid:int;
read n, S(1..n), x;
low:=1; high:=n;
pozitie:=0;
while low<=high AND pozitie=0
begin
mid:=int((low+high)/2);
if x=S(mid) then pozitie:=mid
else if x<S(mid) then high:=mid-1
else low:=mid+1;
end;
write pozitie;
END

Să facem o comparaţie între cei doi algoritmi de căutare prezentaţi anterior.


Vom determina numărul de comparaţii făcute de fiecare algoritm pentru acelaşi caz.
Dacă S conţine 32 de elemente şi x nu se află printre elementele şirului, algoritmul de
căutare secvenţială numărul x se compară cu toate cele 32 de elemente ale şirului şi se
determină că x nu este în şirul S. În general, căutarea secvenţială realizează n

2
comparaţii pentru a afla că x nu este într-un şir de dimensiune n. Pe de altă parte, dacă
x este în şirul S, numărul de comparaţii este mai mic sau egal cu n.
În algoritmul de căutare binară există două comparaţii între x şi S(mid) în
fiecare pas al buclei while (cu excepţia cazului în care se găseşte x egal cu S(mid)).
Dacă se implementează acest algoritm în limbaj de asamblare, x va fi comparat o
singură dată cu S(mid) în fiecare pas, deoarece această comparaţie setează indicatorii
de condiţie şi pe baza valorii acestora se va realiza ramificarea în program (pentru
egal, mai mic şi mai mare). Vom presupune în continuare că algoritmul este
implementat astfel încât în fiecare pas al buclei să se realizeze o singură comparaţie
între x şi S(mid). În fig.3.1 se vede că algoritmul realizează 6 ( 6 = log 2 32 + 1 )
comparaţii pentru şirul S de 32 de elemente şi x mai mare decât toate elementele
şirului. Acesta este numărul maxim de comparaţii pe care le realizează algoritmul.
Indiferent dacă x este în şir, dacă x este mai mic decât toate elementele şirului sau
dacă x este între două elemente ale şirului nu se vor realiza mai mult de 6 comparaţii.

Fig.3.1. Numărul de comparaţii realizate de căutarea binară

În tabelul următor se prezintă numărul de comparaţii realizate de cei doi


algoritmi în cazul în care şirul S conţine un număr mare de elemente şi x este mai
mare decât toate elementele şirului.
Dimensiunea lui S Nr.comparaţii la căutarea Nr.comparaţii la
secvenţială căutarea binară
128 128 8
1 024 1 024 11
1 048 576 1 048 576 21
4 294 967 296 4 294 967 296 33

Algoritmul de căutare binară de mai sus este prezentat în forma iterativă. În


continuare vom prezenta o versiune recursivă care ilustrează abordarea de sus în jos
specifică metode divide-et-impera. În termenii specifici acestei metode, problema
căutării binare constă din compararea lui x cu elementul din mijlocul şirului S. Dacă
sunt egale, atunci algoritmul s-a terminat pentru că am găsit poziţia lui x în S. Dacă
nu, şirul este împărţit în două subţiruri, unul conţinând toate elementele din jumătatea
stângă a lui S şi al doilea elementele din jumătatea dreaptă a lui S. Dacă x este mai
mic decât elementul din mijlocul şirului S, procedura descrisă anterior este aplicată
pentru jumătatea stângă, altfel este aplicată pentru jumătatea dreaptă. Adică, x se
compară cu elementul din mijlocul subşirului corespunzător. Dacă sunt egale,
algoritmul se termină, altfel subşirul se împarte din nou în două subşiruri. Această
procedură se repetă până când este găsit x sau se determică că x nu este în şirul S.
Aceşti paşi pot fi rezumaţi după cum urmează:
Dacă x este egal cu elementul din mijloc, se opreşte. Altfel:
1. Divide şirul în două jumătăţi (subşiruri). Dacă x este mai mic decât
elementul din mijloc, alege subşirul din stânga. Dacă x este mai mare
decât elementul din mijloc, alege subşirul din dreapta.
2. Impera (rezolvă) subşirul determinând dacă x este în acesta. Dacă şirul
nu este prea mic, se va folosi recursivitatea.
3. Obţine soluţia pentru şir din soluţia pentru subşir.

3
Căutarea binară este cel mai simplu algoritm de tip divide-et-impera deoarece
problema se împarte într-o singură problemă mai mică, deci nu trebuie realizată
combinarea soluţiilor. Soluţia problemei iniţiale este aceeaşi cu soluţia problemei mai
mici. De exemplu, să considerăm următoarele valori:
n=13
x=18
S=(10 12 13 14 18 20 25 27 30 35 40 45 47)
În fig.3.2 se prezintă schematic paşii căutării numărului x în şirul S prin
algoritmul de căutare binară.

Fig.3.2. Paşii căutării binare

În continuare se prezintă o versiune recursivă a căutării binare.

Intrări: numărul întreg pozitiv n, şirul S ordonat crescător şi un număr x.


Ieşiri: pozitie, poziţia numărului x în şirul S (0 dacă x nu este în şir).

Cautare_binara;
Var n,x,S(1..n):int;
Function pozitie(low,high:int):int;
begin
if low>high then return 0
else
begin
mid:=int((low+high)/2);
if x=S(mid) then return mid
else if x<S(mid) then return pozitie(low,mid-1)
else return pozitie(mid+1,high);
end;
end;

BEGIN
Read n,x,S(n);
Write pozitie(1,n);
END

Între cele două implementări ale căutării binare, şi anume iterativă respectiv
recursivă, nu există nici o diferenţă de conţinut. Algoritmul este identic.
Implementarea recursivă ilustrează foarte bine principiul metodei divide-et-impera. În

4
schimb, implementarea iterativă este mai economică din punctul de vedere al
memoriei necesare. Aceasta deoarece implementarea recursivă necesită un spaţiu de
memorie suplimentar alocat stivei în care se salvează rezultatele parţiale la fiecare
reapelare a funcţiei. Acest spaţiu de memorie poate deveni destul mare, atunci când
avea de-a face cu şiruri cu foarte multe elemente.

Concatenare de şiruri sortate


Această concatenare înseamnă combinarea a două şiruri ordonate într-un
singur şir ordonat. Prin aplicarea repetată a procedurii de concatenare, putem realiza
ordonarea unui şir. De exemplu, pentru a ordona un şir de 16 elemente, îl putem
împărţi în două subşiruri, fiecare de 8 elemente, le ordonăm pe fiecare şi apoi le
concatenăm pentru a obţine şirul ordonat. La fel, fiecare şir de 8 elemente poate fi
împărţit în două subşiruri de câte 4 elemente, iar aceste subşiruri pot fi sortate şi
concatenate. În cele din urmă dimensiunea subşirurilor va ajunge la 1 şi un şi de
dimensiune unitară este deja sortat. Această procedură se numeşte mergesort. Dându-
se un şir cu n elemente (pentru simplificare, vom considera pe n ca o putere a lui 2),
mergesort constă din următorii paşi:
1. Divide şirul în două subşiruri fiecare cu n/2 elemente.
2. Impera (rezolvă) fiecare subşir prin ordonarea lui. Dacă şirul nu este
prea mic, se va folosi recursivitatea.
3. Combină soluţiile obţinute pentru subşiruri prin concatenarea lor într-
un singur şir ordonat.

Aceşti paşi sunt ilustraţi în fig.3.3 pentru un şir conţinând elementele:


27 10 12 25 13 15 22.

Fig.3.3. Paşii algoritmului mergesort

În continuare se prezintă algoritmul mergesort scris în pseudocod:

5
Intrări: numărul întreg pozitiv n, şirul S cu n elemente.
Ieşiri: şirul S cu elementele ordonate crescător.

Ordonare_Mergesort;
Var n,S(1..n):int;
procedure mergesort(n:int,S(1..n):int);
begin
if n>1 then
begin
h:=int(n/2);
m:=n-h;
U(1..h), V(1..m):int;
Copy S(1..h) in U(1..h);
Copy S(h+1..n) in V(1..m);
mergesort(h,U);
mergesort(m,V);
merge(h,m,U,V,S);
end;
end;

procedure merge(h,m:int,U(),V(),S():int);
begin
var i,j,k:int;
i:=1;
j:=1;
k:=1;
while i=h AND j<=m
begin
if U(i)<V(j) then
begin
S(k):=U(i);
i:=i+1;
end;
else
begin
S(k):=V(j);
j:=j+1;
end;
k:=k+1;
end;
if i>h then Copy V(j..m) in S(k..h+m)
else Copy U(i..h) in S(k..h+m)
end;

BEGIN
Read n,S(1..n);
Mergesort(n,S(1..n));
Write S(1..n);
END

6
Acest algoritm de sortare se poate realiza şi în varianta numită in-place sort.
Această variantă nu utilizează spaţiu suplimentar de memorie faţă de cel necesar
pentru stocarea şirului de intrare. Algoritmul de mai sus nu este de tipul in-place sort
deoarece foloseşte şirurile U şi V pe lângă şirul de intrare S.
Algoritmul de sortare de tip in-place sort realizează majoritatea manipulărilor
de date în şirul de intrare S. În continuare se prezintă acest algoritm care foloseşte o
abordare similară algoritmului recursiv de căutare binară.

Intrări: numărul întreg pozitiv n, şirul S cu n elemente.


Ieşiri: şirul S cu elementele ordonate crescător.

Ordonare_Mergesort2;
Var n,S(1..n):int;
procedure mergesort2(low,high:int);
begin
var mid:int;
if low<high then
begin
mid:=int((low+high)/2);
mergesort2(low,mid);
mergesort2(mid+2,high);
merge2(low,mid,high);
end;
end;

procedure merge2(low,mid,high:int);
begin
var i,j,kU(low..high):int; şirul U este necesar pentru concatenare

i:=low;
j:=mid+1;
k:=low;
while i=mid AND j=high
begin
if S(i)<S(j) then
begin
U(k):=S(i);
i:=i+1;
end;
else
begin
U(k):=S(j);
j:=j+1;
end;
k:=k+1;
end;
if i>mid then Copy S(j..high) in U(k..high)
else Copy S(i..mid) in U(k..high);
Copy U(low..high) in S(low..high);
end;

7
BEGIN
Read n,S(1..n);
Mergesort2(1,n);
Write S(1..n);
END

Sortare rapidă (Quicksort)


Acest algoritm de sortare a fost dezvoltat de Hoare în anul 1962. Sortarea
rapidă este similară cu Mergesort deoarece este realizată prin împărţirea şirului în
două părţi şi apoi sortarea fiecărei părţi în mod recursiv. În Quicksort împărţirea
şirului se face prin plasarea tuturor elementelor mai mici decât un element pivot
înaintea acestuia şi a tuturor elementelor mai mari decât elementul pivot după el.
Elementul pivot poate fi orice element şi pentru uşurinţă îl vom considera primul
element al şirului. În fig.3.4 se prezintă modul de funcţionare a algoritmului Quicksort
pentru şirul de intrare: 15 22 13 27 12 10 20 25.

Fig.3.4. Paşii algoritmului Quicksort

După împărţire avem în stânga elementului pivor toate elementelor din şir mai
mici decât acesta, iar în dreapta lui toate elementele mai mari. Quicksort este apoi
apelat în mod recursiv pentru sortarea fiecărei părţi. Acest procedeu continuă până ce
se obţine un şir cu un singur element. Acest şir este deja sortat.
În continuare se prezintă algoritmul Quicksort scris în pseudocod:

Intrări: numărul întreg pozitiv n, şirul S cu n elemente.


Ieşiri: şirul S cu elementele ordonate crescător.

Ordonare_Quicksort;
Var n,S(1..n):int;
procedure quicksort(low,high:int);
begin
var pivotpoint:int;
if high>low then
begin
partition(low,high,pivotpoint);
quicksort(low,pivotpoint-1);
quicksort(pivotpoint+1,high);
end;
end;

8
procedure partition(low,high,pivotpoint:int);
begin
var i,j:int, pivotitem:int;
pivotitem:=S(low); Alege primul element din S ca pivot
j:=low;
for i=low+1 to high
if S(i)<pivotitem then
begin
j:=j+1;
Exchange S(i) and S(j);
end;
pivotpoint:=j;
Exchange S(low) and S(pivotpoint); Pune pivotitem în pivotpoint
end;

BEGIN
Read n,S(1..n);
Quicksort(1,n);
Write S(1..n);
END

Cazuri în care nu se foloseşte metoda Divide-et-impera


Această metodă nu se va folosi în următoarele două cazuri:
• O problemă de dimensiune n este împărţită în două sau mai multe
probleme, fiecare de dimensiune aproape egală cu n.
• O problemă de dimensiune n este împărţită în n probleme de
dimensiune n/c, unde c este o constantă.
Pentru fiecare dintre aceste cazuri timpul de rezolvare creşte exponenţial.
Intuitiv, pentru primul caz, ar fi ca şi cum Napoleon ar fi împărţit armata adversă de
30 000 de soldaţi în două armate de căte 29 999 de soldaţi (dacă ar fi cumva posibil).
Astfel, în loc să aibă mai puţini inamici, le-ar fi dublat aproape numărul. Dacă
Napoleon ar fi făcut aşa ceva, Waterloo-ul său ar fi apărut mult mai devreme.
Uneori, însă, există probleme care necesită această exponenţialitate şi în acest
caz simplitatea metodei Divide-et-impera este chiar recomandată. Un exemplu de
asemenea problemă este cea a Turnurilor din Hanoi. Pe scurt, aceasta cere mutarea a n
discuri dintr-o stivă în alta avâd câteva restricţii privind modul de mutare. Analizând
problema, secvenţa de mutări obţinută din algoritmul standard divide-et-impera este
exponenţial cu n, dar pentru restricţiile impuse de problemă este cea mai eficientă
secvenţă de mutări. Aşadar, problema necesită un număr exponenţial cu n de mutări.

Problema turnurilor din Hanoi:


Avem 3 stive şi n discuri de diferite dimensiuni. Obiectivul este să se mute
discurile stocate în una dintre cele trei stive, în ordine descrescătoare a dimensiunii
lor, în altă stivă utilizând a treia ca stivă temporară. Problema trebuie rezolvată ţinând
seama de următoarele reguli: (1) când este mutat un disc, acesta trebuie pus în una
dintre stive, (2) se poate muta un singur disc o dată şi acesta trebuie să fie discul din
vârful oricărei stive şi (3) sub nici o formă nu se poate plasa un disc mai mare peste
unul mai mic.

9
PROBLEME PROPUSE

10
Capitolul 4
PROGRAMARE DINAMICĂ

În multe cazuri algoritmul Divide-et-impera realizează calculul unui număr foarte


mare de termeni (cum este cazul determinării celui de al n-lea termen din şirul lui Fibonacci).
Aceasta deoarece, algoritmul divide-et-impera rezolvă o problemă prin împărţirea ei în
probleme mai mici şi apoi rezolvarea acestora. Aceasta este o abordare de tip top-down (de
sus în jos). Această metodă funcţionează foarte bine în cazul problemelor de tip mergesort, în
care problemele mai mici nu sunt legate între ele, deoarece fiecare conţine un şir de elemente
care trebuie sortate independent. Însă o problemă ca cea de calcul al n-lea termen din şirul lui
Fibonacci, împărţirea se face în probleme mai mici legate între ele. De exemplu, pentru a
calcula al cincilea termen din şirul lui Fibonacci trebuie să calculăm al patrulea şi al treilea
termen din şir. Dar determinările celor doi termeni (al patrulea şi al treilea) sunt legate între
ele prin aceea că amândouă au nevoie de al doilea termen al şirului. Deoarece algoritmul
divide-et-impera realizează aceste două calcule în mod independent, se ajunge la a calcula de
mai multe ori termenul al doilea. În problemele în care împărţirea se face în probleme mai
mici legate între ele, algoritmul divide-et-impera ajunge să rezolve în mod repetat aceleaşi
subprobleme, ceea ce conduce la un algoritm ineficient.
În continuare vom prezenta algoritmul divide-et-impera pentru determinarea celui de
al n-lea termen al şirului lui Fibonacci, pentru a putea face o comparaţie între metodele
divide-et-impera şi programarea dinamică. Şirul lui Fibonacci este definit în mod recursiv
după cum urmează:
f0 = 0
f1 = 1
f n = f n −1 + f n −2 ; n ≥ 2
De exemplu, primii 5 termeni ai şirului lui Fibonacci ar fi:
f 2 = f1 + f 0 = 1
f 3 = f 2 + f1 = 2
f 4 = f3 + f 2 = 3
f5 = f 4 + f3 = 5

Intrări: numărul întreg pozitiv n.


Ieşiri: fib, al n-lea termen al şirului lui Fibonacci.

sirul_Fibonacci;
function fib(n:int):int;
begin
if n<=1 then return n
else return fib(n-1)+fib(n-2);
end;

BEGIN
var n:int;
read n;
write fib(n);
END
Programarea dinamică are o abordare complet diferită. Singura asemănare dintre
programarea dinamică şi metoda divide-et-impera este faptul că o problemă este împărţită în
probleme mai mici. Însă, în cazul programării dinamice se rezolvă problemele mai mici, se
stochează rezultatele şi, mai târziu, ori de câte ori este necesar un rezultat, acesta se preia şi nu
se recalculează. Termenul de programare dinamică vine din teoria sistemelor şi aici prin
programare se înţelege utilizarea unui tablou în care se construieşte soluţia.
Un exemplu de calcul a celui de al n-lea termen al şirului lui Fibonacci prin metoda
programării dinamice este prezentat în continuare. Pentru calculul termenului al n-lea al
şirului se construieşte un şir f cu primii n+1 termeni ai şirului indexaţi de la 0 la n.

Intrări: numărul întreg pozitiv n.


Ieşiri: fib2, al n-lea termen al şirului lui Fibonacci.

sirul_Fibonacci;
function fib2(n:int):int;
begin
var i,f(0..n):int;
f(0):=0;
if n>0 then
begin
f(1):=1;
for i:=2 to n
f(i):=f(i-1)+f(i-2);
end;
return f(n)
end;

BEGIN
var n:int;
read n;
write fib2(n);
END

Într-un algoritm realizat prin programare dinamică, construim într-o matrice (sau
succesiune de matrici) o soluţie de jos în sus. Programarea dinamică realizează aşadar o
abordare bottom-up. Uneori, după ce realizăm algoritmul utilizând o matrice (sau secvenţă de
matrici) îl putem optimiza deoarece mare parte din spaţiul de memorie alocat iniţial nu este
necesar.
Paşii în dezvoltarea unui algoritm prin programare dinamică sunt următorii:
1. Stabilirea unei proprietăţi recursive care dă soluţia pentru problemă.
2. Rezolvarea problemei prin metoda bottom-up, rezolvând mai întâi
subproblemele mai mici.

4.1. Algoritmul lui Floyd pentru determinarea celui mai scurt drum
O problemă foarte des întâlnită de cei care călătoresc mult cu avionul este
determinarea celui mai scurt traseu pentru a ajunge dintr-un oraş în altul atunci când nu există
un zbor direct. Vom prezenta în continuare un algoritm care rezolvă această problemă precum
şi altele similare.
Pentru început vom aminti câteva noţiuni din teoria grafurilor. În fig.4.1 se prezintă un
graf orientat şi marcat.

2
Fig.4.1. Un graf orientat şi marcat

În reprezentarea grafică a unui graf se folosesc cercuri pentru noduri şi linii de la un


cerc la altul pentru arcuri. Dacă fiecărui arc îi este asociată o direcţie, graful este denumit un
graf orientat sau digraf. Când se reprezintă grafic un graf orientat, se vor folosi pentru arcuri
săgeţi care arată direcţia. Într-un digraf pot exista două arcuri între oricare două noduri, câte
unul pentru fiecare direcţie. În exemplul din fig.4.1 există un arc de la v1 la v2 şi alt arc de la
v2 la v1. Dacă arcurilor le sunt asociate valori, aceste valori se numesc marcaje şi graful este
numit marcat. Vom presupune în continuare că aceste marcaje sunt numere pozitive. Deşi
aceste valori se numesc în general marcaje, în multe aplicaţii acestea reprezintă distanţe.
Aşadar, vom vorbi de o cale de la un nod la altul. Într-un graf orientat, o cale este o secvenţă
de noduri astfel încţt să existe un arc de la fiecare nod la succesorul său. De exemplu, în
fig.4.1, secvenţa [v1, v4, v3] este o cale deoarece există un arc de la v1 la v4 şi un arc de la v4
la v3. Secvenţa [v3, v4, v1] nu este o cale, deoarece nu există arc de la v4 la v1. O cale de un
nod la el însuşi se numeşte o buclă. Calea [v1, v4, v5, v1] din fig.4.1 este o buclă. Dacă un
graf conţine o buclă acesta este ciclic, altfel este aciclic. O cale este simplă dacă nu trece de
două ori prin acelaşi nod. Calea [v1, v2, v3] din fig.4.1 este simplă, dar calea [v1, v4, v5, v1,
v2] nu este simplă. Se observă că o cale simplă nu conţine niciodată o subcale care să fie o
buclă. Lungimea unei căi dintr-un graf marcat este suma marcajelor din cale, iar într-un graf
nemarcat este numărul de arcuri din cale.

O problemă care are multe aplicaţii este găsirea celor mai scurte căi de la fiecare nod
la toate celelalte. Evident, cea mai scurtă cale trebuie să fie o cale simplă. În fig.4.1 există trei
căi simple de la v1 la v3, şi anume: [v1, v2, v3], [v1, v4, v3] şi [v1, v2, v4, v3]. Deoarece:
lungime[v1, v2, v3]=1+3=4
lungime[v1, v4, v3]=1+2=3
lungime[v1, v2, v4, v3]=1+2+2=5
[v1, v4, v3] este cea mai scurtă cale de la v1 la v3. Aşa cum am mai spus, o aplicaţie uzuală a
celor mai scurte căi este găsirea celor mai scurte drumuri între două oraşe.
Problema celor mai scurte căi este o problemă de optimizare. Într-o problemă de
optimizare pot exista mai multe soluţii candidat. Fiecare soluţie candidat are asociată o
valoare şi o soluţie a problemei este orice soluţie candidat care are o valoare optimă. În
funcţie de problemă, valoarea optimă este fie cea minimă fie cea maximă. În cazul problemi
celor mai scurte căi, o soluţie candidat este o cale de la un nod la altul, valoarea este lungimea
căii şi valoarea optimă este minimul acestor lungimi.
Deoarece pot exista mai multe căi de la un nod la altul care să fie cele mai scurte,
problema este să se găsească oricare dintre cele mai scurte căi. Un algoritm evident pentru
această problemă ar fi să se determine, pentru fiecare nod, lungimile tuturor căilor de la acel
nod la toate celelalte şi să se calculeze apoi minimul acestor lungimi. Dar acest algoritm este
foarte mare consumator de timp. Daca ar exista câte un arc de la fiecare nod la toate celelalte,
am obţine un număr foarte mare de căi posibile. Această metodă de soluţionare nu este
acceptabilă. Trebuie să găsim un algoritm mai eficient.

3
Folosind programarea dinamică, vom crea un algoritm pentru problema celor mai
scurte drumuri. Mai întâi dezvoltăm un algoritm care determină numai lungimile celor mai
scurte drumuri. După aceasta îl modificăm pentru a produce şi cele mai scurte căi. Vom
reprezenta un graf marcat având n noduri printr-o matrice W după cum urmează:
marcajul de pe arc, daca exista un arc de la v i la v j

W (i, j) = ∞, daca nu exista un arc de la v i la v j
0, daca i = j

Deoarece se spune că nodul vi este adiacent nodului vj dacă există un arc de la vi la vj,
această matrice se numeşte matricea adiacenţelor. Graful din fig.4.1 este reprezentat prin
matricea adiacenţelor după cum urmează:
0 1 ∞ 1 5
 9 0 3 2 ∞
 
W = ∞ ∞ 0 4 ∞ 
 
∞ ∞ 2 0 3 
 3 ∞ ∞ ∞ 0 
Vom defini acum o matrice D, de forma de mai jos, care conţine lungimile celor mai
scurte căi din graf.
 0 1 3 1 4
 8 0 3 2 5
 
D = 10 11 0 4 7 
 
 6 7 2 0 3
 3 4 6 4 0
De exemplu, D(3,5)=7 deoarece 7 este lungimea unei celei mai scurte căi de la v3 la
v5. Dacă putem găsi o modalitate de calcul a valorilor din D pe baza celor din W, vom avea un
algoritm pentru probleme celor mai scurte drumuri. Vom realiza aceasta prin calculul unei
serii de n+1 matrici D(k), unde 0<k<n şi unde:
D (k ) (i, j) = lungimea unei celei mai scurte cai de la v i la v j utilizand numai nodurile din
multimea {v1 , v 2 ,..., v k }ca noduri int ermediare
Înainte de a demonstra corectitudinea acestui algoritm, vom determina câteva valori
(k)
prin D (i,j) pentru graful din fig.4.1.
D (0 ) (2,5) = lungimea[v 2 , v 5 ] = ∞
D (1) (2,5) = min (lungimea[v 2 , v 5 ], lungimea[v 2 , v1 , v 5 ]) = min (∞,14 ) = 14
D (2 ) (2,5) = D (1) (2,5) = 14 Pentru orice graf acestea sunt egale deoarece o cea mai
scurtă cale care porneşte din v2 nu poate trece tot prin
v2.
D (2,5) = D (2,5) = 14
(3 ) (2 )
Pentru acest graf acestea sunt egale deoarece
incluzându-l pe v3 nu se produce nici o cale nouă de la
v2 la v5.
Ultima valoare care se calculează, D(5)(2,5) este lungimea celui mai scurt drum de la
v2 la v5 care poate terce prin oricare alte noduri. Adică aceasta este cea mai scurtă cale.

Deoarece D(n)(i,j) este lungimea celei mai scurte căi de la vi la vj care poate trece prin
oricare alte noduri, aceasta va fi lungimea celei mai scurte căi de la vi la vj. Deoarece D(0)(i,j)

4
este lungimea unei cele mai scurte căi de la vi la vj care nu poate trece prin nici un alt nod,
aceasta este de fapt lungimea arcului de la vi la vj. Prin aceste două afirmaţii am stabilit că:
D (0 ) = W şi D (n ) = D .
Aşadar, pentru a determina pe D din W trebuie doar să obţinem D(n) din D(0). Paşii
necesari în programarea dinamică pentru aceasta sunt:
1. Stabilirea unei prorietăţi (proces) recursive cu care putem calcula D(k) din D(k-1).
2. Rezolvarea unei subprobleme în stilul bottom-up (de jos în sus) prin repetarea
procesului (stabilit în pasul 1) pentru k=1 .. n. Aceasta va conduce la secvenţa:
D , D1 , D 2 ,..., D n .
0

↑ ↑
W D

Vom realiza pasul 1 considerând două cazuri:


Cazul 1. Cel puţin o cale dintre cele mai scurte de la vi la vj, care foloseşte ca noduri
intermediare numai cele din mulţimea {v1 , v 2 ,..., v k }, nu foloseşte nodul vk. Atunci:
D (k ) (i, j) = D (k −1) (i, j)
Ca exemplu pentru acest caz, în fig.4.1 avem:
D (5 ) (1,3) = D (4 ) (1,3) = 3 ,
deoarece când includem nodul v5, cea mai scurtă cale de la v1 la v3 este tot (v1,v4,v3).
Cazul 2. Toate căile care sunt cele mai scurte de la vi la vj, care folosesc ca noduri
intermediare numai cele din mulţimea {v1 , v 2 ,..., v k }, utilizează nodul vk. În acest caz toate
cele mai scurte căi sunt ca în fig.4.2.

Fig.4.2. Cea mai scurtă cale foloseşte nodul vk

Deoarece vk nu poate fi nod intermediar în subcalea de la vi la vk, acea subcale


utilizează numai nodurile din {v1 , v 2 ,..., v k −1 } ca noduri intermediare. Aceasta înseamnă că
lungimea subcăii trebuie să fie egală cu D (k−1) (i, k ) din următoarele motive: Mai întâi,
lungimea subcăii nu poate fi mai mică deoarece D (k−1) (i, k ) este lungimea celei mai scurte căi
de la v1 la vk care foloseşte ca noduri intermediare numai nodurile din mulţimea
{v1 , v 2 ,..., v k −1}. Apoi, lungimea subcăii nu poate fi mai mare deoarece dacă ar fi, am putea-o
înlocui în fig.4.2 printr-o altă cale mai scurtă, ceea ce ar contrazice faptul că întreaga cale din
fig.4.2 este o cea mai scurtă cale. Similar, lungimea subcăii de la vk la vj din fig.4.2 trebuie să
fie egală cu D (k−1) (k , j) . Aşadar, în cel de al doilea caz:
D (k ) (i, j) = D (k −1) (i, k ) + D (k −1) (k , j)
Un exemplu pentru cel de al doilea caz în fig.4.1 este:
D (2 ) (5,3) = 7 = 4 + 3 = D (1) (5,2 ) + D (1) (2,3)

5
Deoarece singurele cazuri posibile sunt cele două descrise mai sus, înseamnă că
valoarea lui D (k ) (i, j) este minimul valorilor din cele două cazuri. Aceasta înseamnă că putem
determina pe D(k) din D(k-1) după cum urmează:
[ ]
D (k ) (i, j) = min D (k −1) (i, j), D (k −1) (i, k ) + D (k −1) (k , j)
cazul1 cazul 2
Am realizat astfel pasul 1 din algoritmul dezvoltat prin programare dinamică. Pentru
pasul 2 folosim recursivitarea din pasul 1 pentru a crea secvenţa de matrici. În continuare vom
prezenta cum se calculează fiecare din aceste matrici din precedentele pentru cazul grafului
din fig.4.1.
D (0 ) = W
[ ]
D (1) (2,4) = min D (0 ) (2,4 ), D (0 ) (2,1) + D (0 ) (1,4 ) = min[2,9 + 1] = 2
[ ]
D (1) (5,2 ) = min D (0 ) (5,2 ), D (0 ) (5,1) + D (0 ) (1,2 ) = min[∞,3 + 1] = 4
[ ]
D (1) (5,4 ) = min D (0 ) (5,4 ), D (0 ) (5,1) + D (0 ) (1,4 ) = min[∞,3 + 1] = 4
După ce se calculează întreaga matrice D(1), se poate calcula şi matricea D(2). De
exemplu, pentru matricea D(2) un element este:
[ ]
D (2 ) (5,4 ) = min D (1) (5,4 ), D (1) (5,2 ) + D (1) (2,4 ) = min[4,4 + 2] = 4
După calcularea tuturor elementelor din D(2) vom continua pe acelaşi principiu până
la D(5). Această matrice este matricea D, care conţine lungimile celor mai scurte căi.

În continuare vom prezenta algoritmul lui Floyd, care datează din anul 1962.
Problema: Să se calculeze cele mai scurte căi între oricare două noduri dintr-un graf
marcat. Marcajele sunt numere pozitive.
Intrări: Un graf orientat şi marcat şi n=numărul de noduri din graf. Graful este
reprezentat prin matricea adiacenţelor W, pătratică bidimensională de dimensiune n.
Ieşiri: O matrice pătratică bidimensională D, de dimensiunea n, în care D(i,j)
reprezintă cea mai scurtă cale de la nodul i la nodul j.

Floyd;
procedure Floyd(n:int, W(n,n):const, D(n,n):real);
begin
var i,j,k:int;
D=W;
for k=1 to n
for i=1 to n
for j=1 to n
D(i,j):=min(D(i,j),D(i,k)+D(k,j));
end;

BEGIN
var n:int, W(n,n),D(n,n):real;
read n,W(n,n);
Floyd(n,W,D);
write D(n,n);
END

Putem realiza calculele utilizând numai o matrice D deoarece valorile din linia k şi
cele din coloana k nu se modifică în timpul celei de a k-a iteraţie din buclă. Adică, în iteraţia k
din algoritm se atribuie valorile:

6
D(i, k ) = min[D(i, k ), D(i, k ) + D(k , k )]
care este egală cu D(i,k) şi:
D(k , j) = min[D(k , j), D(k , j) + D(k , k )]
care este egală cu D(k,j). În timpul celei de a k-a iteraţie, D(i,j) se calculează numai din
propria sa valoare şi din valorile din linia k şi din coloana k. Deoarece acestea şi-au păstrat
aceleaşi valori ca în iteraţia k-1, acestea pot fi folosite fără griji. Aşa cum am mai spus, după
dezvoltarea unui algoritm prin programarea dinamică, este posibil să se revizuiască acesta
pentru a-l face mai eficient.

În continuare vom face o modificare a algoritmului, care va produce şi drumurile


(succesiunea de noduri) cele mai scurte.
Problema şi intrările sunt aceleaşi. Apar în plus ieşiri, şi anume, o matrice P, pătratică
de dimensiune n, în care:
cel mai mare index al unui nod int ermediar de pe cea mai scurta cale

P(i, j) =  de la v i la v j , daca exista cel putin un nod int ermediar
0, daca nu exista nici un nod int ermediar

Floyd_2;
procedure Floyd2(n:int, W(n,n):const, D(n,n):real, P(n,n):int);
begin
var i,j,k:int;
for i=1 to n
for j=1 to n
P(i,j):=0;
D=W;
for k=1 to n
for i=1 to n
for j=1 to n
if D(i,k)+D(k,j)<D(i,j) then
begin
P(i,j):=k;
D(i,j):=D(i,k)+D(k,j);
end
end;

BEGIN
var n:int, P(n,n):int, W(n,n),D(n,n):real;
read n,W(n,n);
Floyd(n,W,D,P);
write D(n,n), P(n,n);
END

Pentru graful din fig.4.1 matricea P rezultată prin algoritmul de mai sus este:

7
0 0 4 0 4
5
 0 0 0 4
P = 5 5 0 0 4
 
5 5 0 0 0
0 1 4 1 0

Pentru a găsi cel mai scurt drum de la nodul vq la nodul vr folosind matricea P,
utilizăm algoritmul următor:
Problema: să se afişeze cel mai scurt drum de la un nod la alt nod dintr-un graf marcat.
Intrări: matricea P produsă prin algoritmul Floyd2 şi cei doi indici q şi r ai nodurilor
din graf între care se va afişa cel mai scurt drum.
Ieşiri: nodurile intermediare de pe cel mai scurt drum între vq şi vr.

procedure cale(q,r:int);
begin
if P(q,r)!=0 then
begin
cale(q,P(q,r));
write ”v”&P(q,r);
cale(P(q,r),r);
end;
end

Apelarea procedurii de mai sus se va face prin:


cale(q,r);

Având exemplul din fig.4.1, care are matricea P calculată mai sus, pentru valorile q=5
şi r=3 ieşirea va fi:
v1 v4
Acestea sunt nodurile intermediare de pe cel mai scurt drum de la v5 la v3.

4.2. Programarea dinamică şi problemele de optim


Aşa cum s-a prezentat în paragraful anterior, algoritmul lui Floyd determină nu numai
lungimile celor mai scurte căi ci le şi construieşte. Constuirea soluţiei optime este al treilea
pas în dezvoltarea unui algoritm prin metoda programării dimanice. Aceasta înseamnă că paşii
dezvoltării unui asemenea algoritm sunt:
1. Stabilirea unei recursivităţi care dă soluţia optimă la o problemă.
2. Calculul valorii unei soluţii optime în stilul de jos în sus.
3. Construirea unei soluţii optime în stilul de jos în sus.
Paşii 2 şi 3 se realizează de obicei împreună în cadrul algoritmului. Algoritmii care nu
sunt pentru probleme de optim nu conţin pasul 3.
Deşi ar putea părea că orice problemă de optimizare poate fi rezolvată utilizând
programarea dinamică, nu este aşa. Trebuie ca în problemă să se aplice principiul de optim.
Acest principiu se enunţă prin:
Definiţie
Se spune că într-o problemă se aplică principiul de optim dacă o soluţie optimă la un
caz al unei probleme este soluţie optimă pentru toate subproblemele.
Acest enunţ al principiului de optim poate fi înţeles mai uşor dacă analizăm un
exemplu. În cazul probelemei celor mai scurte căi am arătat că dacă vk este un vârf dintr-o

8
cale optimă de la vk la vj, atunci subcăile de la ui la uk şi de la vk la vj trebuie să fie şi ele
optime. Aşadar, soluţia optimă a unei probleme conţine soluţii optime pentru toate
subproblemele şi se aplică principiul de optim.
Dacă într-o anumită problemă se aplică principiul de optim, putem dezvolta o
recursivitate care dă o soluţie optimă pentru un caz al problemei prin soluţii optime ale
subproblemelor. Motivul cel mai important pentru care putem folosi programarea dinamică în
construirea unei soluţii optime pentru un caz al problemei este că soluţiile optime la
subprobleme pot fi printre soluţiile optime. De exemplu, în cazul problemei celui mai scurt
drum, dacă subcăile sunt cele mai scurte, atunci calea obţinută prin combinarea acestora va fi
optimă. Vom putea aşadar folosi recursivitatea pentru a construi soluţii optime la probleme tot
mai complexe pe principiul de jos în sus. Fiecare soluţie determinată pe această cale va fi
întotdeauna optimă.
Deşi principiul optimului ar putea părea evident, în practică este necesar să
demonstrăm că acest principiu se poate aplica, înainte de a presupune că prin programare
dinamică vom obţine soluţia optimă. Următorul exemplu arată că acest principiu nu se aplică
în toate problemele de optimizare.
Exemplu:
Să considerăm problema celui mai lung drum care presupune găsirea celei mai lungi
căi simple de la orice vârf la toate celelalte ale unui graf. Restricţionăm problema la căi
simple deoarece utilizând un ciclu putem crea o cale oricât de lungă, trecând repetat prin
ciclul respectiv. În fig.4.3 calea simplă cea mai lungă de la v1 la v4 este [v1, v3, v2, v4]. Însă
subcalea [v1, v3] nu este calea optimă (cea mai lungă) de la v1 la v3 deoarece:
lungime[v1, v3]=1 şi lungime[v1, v2, v3]=4.

Fig.4.3. Un graf orientat, marcat având un ciclu

Aşadar, principiul optimului nu se aplică în acest caz. Motivul este că nu pot fi


combinate căile optime dintre v1 şi v3 respectiv v3 şi v4 pentru a obţine o cale optimă de la
v1 la v4. Dacă le-am combina am obţine un ciclu şi nu o cale optimă.
În continuare vom prezenta probleme de optim rezolvate prin programare dinamică.

4.3. Arbori binari de căutare optimală


În acest paragraf vom realiza un algoritm de determinare a modului optim de
organizare a articolelor dintr-un arbore binar de căutare. Înainte de a defini ce înseamnă
organizare optimă, vom prezenat pe scurt noţiunile legate de aceşti arbori. Pentru orice nod
dintr-un arbore binar, subarborele a cărui rădăcină este fiul stâng al nodului respectiv se
numeşte subarborele stâng al nodului. Subarborele stâng al rădăcinii arborelui este denumit
subarborele stâng al arborelui. Subarborele drept este definit în mod analog.
Definiţie:

9
Un arbore de căutare binară este un arbore binar de articole (denumite uzual chei),
care provin dintr-o mulţime ordonată, astfel încât:
1. Fiecare nod conţine o cheie.
2. Cheile din subarborele stâng al unui nod dat sunt mai mici sau egale cu cheia
din acel nod.
3. Cheile din subarborele drept al unui nod dat sunt mai mari sau egale cu cheia
din acel nod.
În fig.4.4 se prezintă doi arbori de căutare binară, fiecare cu aceleaşi chei. În arborele
din fig.4.4.a, dacă analizăm subarborele drept al nodului conţinând articolul „Radu” vedem că
acesta conţine articolele „Toma”, „Ursu” şi „Willy” şi toate aceste articole sunt mai mari
decât „Radu” atunci când sunt ordonate alfabetic. Deşi, în general, o cheie poate apare de mai
multe ori într-un arbore de căutare binară, pentru simplificare vom presupune că toate cheile
sunt distincte.

Fig.4.4. Doi arbori de căutare binară

Adâncimea unui nod dintr-un arbore este numărul de arcuri din calea unică de la
rădăcină la nod. Acesta se mai numeşte şi nivelul nodului în arbore. De obicei spunem că un
nod are o adâncime şi că nodul este la un nivel. De exemplu, în arborele din fig.4.4.a, nodul
cu cheia „Ursu” are o adâncime de 2. Am putea spune şi că acel nod este la nivelul 2.
Rădăcina are o adâncime de 0 şi este la nivelul 0. Adâncimea unui arbore este maximul
adâncimilor tuturor nodurilor acelui arbore. Arborele din fig.4.4.a are adâncimea 3, în timp ce
cel din fig.4.4.b are adâncimea 2. Un arbore binar se spune că este echilibrat dacă adâncimile
celor doi subarbori ai fiecărui nod nu diferă niciodată prin mai mult de 1. Arborele din
fig.4.4.a nu este echilibrat deoarece subarborele stâng al rădăcinii are adâncimea 0 şi
subarborele drept are adâncimea 2. Arborele din fig.4.4.b este echilibrat.
Un arbore binar de căutare conţine înregistrări care sunt preluate în funcţie de valorile
cheilor. Obiectivul nostru este să organizăm cheile dintr-un arbore binar astfel încât timpul
mediu necesar localizării unei chei să fie minimizat. Un arbore care este organizat în acest
mod este denumit optimal. Nu este greu de observat că dacă toate cheile au aceeaşi
probabilitate de a fi cheia de căutare, arborele din fig.4.4.b este optimal. Ne interesează, însă,
cazul în care cheile nu au aceeaşi probabilitate. Un exemplu de asemenea caz ar fi căutarea în
unul din arborii din fig.4.4 după un nume ales aleator. Deoarece „Radu” este un nume mai
comun decât „Willy”, îi vom atribui o probabilitate mai mare lui „Radu”.

10
Vom analiza cazul în care se ştie că cheia de căutare este în arbore. Pentru a minimiza
timpul mediu de căutare, trebuie să ştim complexitatea în timp a localizării unei chei. Aşadar,
mai întâi, vom scrie şi analiza un algoritm care caută o cheie într-un arbore binar de căutare.
Tipul de dată utilizat de acest algoritm este:

struct nodetype
{
keytype key;
nodetype* left;
nodetype* right;
};
typedef nodetype* node_pointer;

Această declaraţie înseamnă că o variabilă node_pointer este un pointer la o


înregistrare de tipul nodetype. Adică, valoarea sa este adresa de memorie unei asemenea
înregistrări.

Problema:
Să se determine nodul care conţine o cheie într-un arbore binar de căutare. Se
presupune că această cheie se află în arbore.
Intrări: un pointer tree la un arbore binar de căutare şi o cheie key.
Ieşiri: un pointer p la nodul care conţine cheia.

procedure search(tree: node_pointer, keyin:keytype, p:node_pointer&);


begin
var found:boolean;
p:=tree;
found:=false;
while !found
if p->key=keyin then found:=true
else if keyin< p->key then p:=p->left merge după fiul stâng
else p:=p->right; merge după fiul drept
end

Numărul de comparări realizate de procedura search pentru localizarea unei chei se


numeşte timpul de căutare. Obiectivul nostru este să determinăm un arbore pentru care timpul
mediu de căutare este minim. Presupunând că operaţiile de comparare sunt bine
implementate, atunci în fiecare iteraţie a buclei While se va realiza numai o comparaţie în
algoritmul de mai sus. Aşadar, timpul de căutare pentru o cheie dată va fi:
adâncime(key)+1
unde adâncime(key) este adâncimea nodului care conţine cheia. De exemplu, deoarece
adâncimea nodului care conţine cheia „Ursu” este 2 in arborele din fig.4.4.a, timpul de căutare
pentru „Ursu” este:
adâncime(„Ursu”)+1=2+1=3.
Să presupunem că avem n chei în ordinea: key1, key2, ..., keyn şi fie pi probabilitatea
ca keyi să fie cheia de căutare. Dacă ci sunt numărul de comparaţii necesare pentru găsirea
cheii keyi într-un arbore dat, timpul mediu de căutare pentru acel arbore este:
n

∑c
i =1
i ⋅ pi

Aceasta este valoare care trebuie minimizată.

11
Exemplu:
În fig.4.5 se prezintă cei cinci arbori diferiţi pentru cazul n=3.

Fig.4.5. Arborii binari de căutare posibili pentru trei chei

Valorile cheilor nu sunt importante. Este necesar numai ca ele să fie ordonate. Dacă
avem: p1=0.7, p2=0.2 şi p3=0.1, atunci timpii medii de căutare pentru arborii din fig.4.5 sunt:
1. 3 ⋅ 0.7 + 2 ⋅ 0.2 + 1 ⋅ 0.1 = 2.6
2. 2 ⋅ 0.7 + 3 ⋅ 0.2 + 1 ⋅ 0.1 = 2.1
3. 2 ⋅ 0.7 + 1 ⋅ 0.2 + 3 ⋅ 0.1 = 1.8
4. 1 ⋅ 0.7 + 3 ⋅ 0.2 + 2 ⋅ 0.1 = 1.5
5. 1 ⋅ 0.7 + 2 ⋅ 0.2 + 3 ⋅ 0.1 = 1.4
Cel de al cincilea arbore este cel optim.

În general, nu putem găsi arborele binar de căutare optim lând în considerare toţi
arborii binari de căutare deoarece numărul acestor arbori este cel puţin exponenţial în n.
Vom folosi programarea dinamică pentru a dezvolta un algoritm mai eficient. Pentru
aceasta vom presupune că avem cheile de la keyi până la keyj aranjate într-un arbore care
minimizează expresia:
j

∑c
m =i
m ⋅ pm

unde cm este numărul de comparaţii necesare pentru a localiza keym în arbore. Vom numi
acest arbore optim pentru acele chei şi vom nota valoarea optimă prin A(i,j). Deoarece este
necesară o comparaţie pentru a localiza o cheie într-un arbore cu o singură cheie, A(i,i)=pi.

Exemplu:
Să presupunem că avem trei chei şi probabilităţile din exemplul anterior. Adică:
p1=0.7, p2=0.2 şi p3=0.1.
Pentru a determina A(2,3) trebuie să considerăm cei doi arbori din fig.4.6. Pentru acei
doi arbori avem următoarele:
1. 1 ⋅ p 2 + 2 ⋅ p 3 = 1 ⋅ 0.2 + 2 ⋅ 0.1 = 0.4
2. 2 ⋅ p 2 + 1 ⋅ p 3 = 2 ⋅ 0.2 + 1 ⋅ 0.1 = 0.5
Primul arbore este optim şi A(2,3)=0.4.

12
Fig.4.6. Arborii binari de căutare compuşi din key2 şi key3

Se observă că arborele optim obţinut în exemplul de mai sus este subarborele drept al
rădăcinii arborelui optim obţinut în exemplul anterior. Chiar dacă acest subarbore nu ar fi fost
exact acelaşi cu subarborele drept, timpul mediu de căutare în el ar fi trebuit să fie acelaşi.
Altfel, l-am fi putut înlocui în acel subarbore, obţinând un arbore cu un timp mediu de căutare
mai mic. În general, orice subarbore al unui arbore optim trebuie să fie optim pentru cheile
din acel subarbore. Aşadar, se aplică principiul optimului.
Acum, să presupunem că arborele 1 este un arbore optim pentru resticţia ca key1 să fie
rădăcina, arborele 2 este un arbore optim pentru restricţia ca key2 să fie rădăcina, ..., arborele
n este un arbore optim pentru restricţia ca keyn să fie rădăcina. Pentru 1 ≤ k ≤ n subarborii
arborelui k trebuie să fie optimi şi aşadar timpul mediu de căutare în aceşti subarbori sunt aşa
cum se vede în fig.4.7. Această figură prezintă şi că pentru a localiza keym în arborele k
pentru fiecare m ≠ k este necesară exact o comparaţie în plus (cea din rădăcină) faţă de cele
necesare localizării acelei chei în subarborele care o conţine. Această comparaţie adaugă 1xpm
la timpul mediu de căutare pentru keym în arborele k. Am stabilit că timpul mediu de căutare
pentru arborele k este dat de:

Fig.4.7. Arbore binar de căutare optim având keyk în rădăcină

ceea ce conduce la:


n
A(1, k − 1) + A(k + 1, n ) + ∑ p m .
m =1
Deoarece unul din cei k arbori trebuie să fie optim, timpul mediu de căutare pentru
arborele optim este dat de:

13
n
A(1, n ) = min (A(1, k − 1) + A(k + 1, n )) + ∑ p m ,
1≤ k ≤ n
m =1
unde A(1,0) şi A(n+1,n) sunt definite ca fiind 0. Deşi suma probabilităţilor din această ultimă
expresie este evident 1, am scris-o ca o sumă pentru că dorim să generalizăm rezultatul. Până
acum nu a existat nici o necesitate în demonstraţia de mai sus să avem cheile de la key1 la
keyn. Deci, în general, expresiile de mai sus se păstrează şi pentru domeniul keyi..keyj, unde
i<j. Am obţinut aşadar următoarele:
j
A(i, j) = min(A(i, k − 1) + A(k + 1, j)) + ∑ p m , i < j
i≤ k ≤ j
m =i

A(i, i ) = p i
A(i, i − 1) = A( j + 1, j) = 0 .
def

Utilizând egalităţile de mai sus putem scrie un algoritm care determină un arbore binar
de căutare optim. Deoarece A(i,j) este calculat din intrările din rândul i de la dreapta lui A(i,j)
şi din intrările din coloana j de sub A(i,j), vom calcula secvenţial valorile de pe fiecare
diagonală. Deoarece paşii algoritmului sunt destul de simpli, vom prezenta mai întâi
algoritmul şi apoi un exemplu sugestiv. Matricea R produsă de algoritm conţine indicii cheilor
alese ca rădăcini la fiecare pas. De exemplu, R(1,2) este indexul cheii din rădăcina unui
arbore optim conţinând primele două chei şi R(2,4) este indexul cheii din rădăcina unui arbore
optim conţinând a doua, a treia şi a patra cheie. După analizarea algoritmului, vom prezenta
modul de construire a unui arbore optim din matricea R.

Problema: Să se determine un arbore binar de căutare optim pentru un set de chei,


fiecare cu o probabilitate dată de a fi cheia de căutare.
Intrări: n=numărul de chei şi o matrice de numere reale p indexate de la 1 la n, unde
p(i) este probabilitatea de căutare după cheia i.
Ieşiri: o variabilă minavg, a cărei valoare este timpul mediu de căutare pentru un
arbore binar de căutare optim şi o matrice bidimensională R din care se poate construi un
arbore optim. R are rândurile indexate de la 1 la n+1 şi coloanele indexate de la 0 la n. R(i,j)
este indexul cheii din rădăcina unui arbore optim conţinând cheile de la i la j.

procedure optsearchtree(i:int, p:real, minavg:real&, R(1..n+1,0..n):int);


begin
var i,j,k,diagonal:int, A(1..n+1,0..n):real;
for i:=1 to n
begin
A(i,i-1):=0;
A(i,i):=p(i);
R(i,i):=i;
R(i,i-1):=0;
end;
A(n+1,n):=0;
R(n+1,n):=0;
for diagonal:=1 to n-1
for i:=1 to n-diagonal
begin
j:=i+diagonal;
j
A(i, j) := min (A(i, k − 1) + A(k + 1, j)) + ∑ p m , i < j ;
i≤ k ≤ j
m =i

14
R(i,j):=o valoare a lui k care a dat minimul;
end;
minavg:=A(1,n);
end

Problema: Să se construiască un arbore binar de căutare optim.


Intrări: n=numărul de chei, o matrice key conţinând cele n chei în ordine şi matricea R
produsă de algoritmul de mai sus. R(i,j) este indexul cheii din rădăcina unui arbore optim
conţinând cheile de la i la j.
Ieşiri: un pointer tree la un arbore binar de căutare optim conţinând cele n chei.

function tree(i,j:int):node_pointer;
begin
var k:int, p:node_pointer;
k:=R(i,j);
if k=0 then return NULL
else
begin
p:=new nodetype;
p->key:=key(k);
p->left:=tree(i,k-1);
p->right:=tree(k+1,j);
return p;
end;
end

Instrucţiune p:=new nodetype realizează un nou nod şi pune adresa sa în p. Fiind un


algoritm recursiv, parametrii n, key şi R nu sunt intrări pentru funcţia tree. Dacă algoritmul ar
fi fost implementat definind n, key şi R global, se obţine un pointer root la rădăcina arborelui
binar de căutare optim apelând tree după cum urmează:
root:=tree(1,n);
Vom prezenta în continuare un exemplu arătând rezultatele obţinute după aplicarea
celor doi algoritmi de mai sus.

Exemplu:
Presupunem că avem următoarele valori pentru matricea key:

Şi
3 3 1 1
p1 = p2 = p3 = p4 = .
8 8 8 8
Matricile A şi R produse de algoritmii de mai sus sunt:

15
 3 9 11 7
0 8 8 8 8
  0 1 1 2 2 
3 5  0 2 2 2
 0 1
 8 8   
A= 1 3 R= 0 3 3

0
8 8  
 0 4
 1
 0   0
 8
 0
7
Arborele creat pe baza matricei R este dat în fig.4.8. Timpul mediu de căutare este .
4

Fig.4.8. Arborele binar de căutare optim

Se observă că R(1,2) ar putea fi 1 sau 2. Motivul este că oricare din aceşti indexi pot fi
rădăcina unui arbore optim care să conţină numai primele două chei. Aşadar, ambii indexi dau
valoarea minimă a lui A(1,2) din algoritmul de mai sus, ceea ce înseamnă că oricare ar putea
fi ales pentru R(1,2).

4.4. Problema comisului voiajor


Să presupunem că un comis voiajor îşi planifică o călătorie care include 20 de oraşe.
Fiecare oraş este conectat cu unele dintre celelalte oraşe printr-un drum. Pentru a minimiza
durata călătoriei, vrem să determinăm cel mai scurt drum care începe din oraşul în care
locuieşte comisul voiajor, trece prin toate oraşele o singură dată şi se termină înapoi în oraşul
comisului voiajor. Această problemă se numeşte problema comisului voiajor.
Această problemă poate fi reprezentată printr-un graf marcat, în care fiecare vârf
reprezintă un oraş. Vom generaliza problema incluzând cazul în care distanţa (marcajul) într-o
direcţie poate fi diferită de distanţa în direcţia opusă. Din nou presupunem că marcajele sunt
numere pozitive. În fig.4.9 se prezintă un asemenea graf marcat. Un tur (denumit şi un circuit
Hamiltonian) într-un graf orientat este un drum de la un vârf la el însuşi şi care trece o singură
dată prin fiecare dintre celelalte vârfuri. Un tur optimal într-un graf marcat şi orientat este o
asemenea cale de lungime minimă. Problema comisului voiajor este să se găsească un tur
optim într-un graf orientat şi marcat atunci când există cel puţin un tur. Deoarece vârful de
pornire nu este relevant pentru lungimea unui tur optim, vom considera v1 ca vârf de pornire.
Cele trei tururi posibile şi lungimile lor în graful din fig.4.9 sunt următoarele:
Lungime(v1, v2, v3, v4, v1)=22
Lungime(v1, v3, v2, v4, v1)=26
Lungime(v1, v3, v4, v2, v1)=21

16
Fig.4.9. Turul optimal este (v1, v3, v4, v2, v1)

Ultimul tur este cel optim. Am rezolvat problema luând în considerare toate tururile
posibile. În general, poate exista un arc de la fiecare vârf la fiecare alt vârf. Dacă luăm în
considerare toate tururile posibile pentru grafuri cu multe vârfuri şi arce timpul necesar
rezolvării problemei devine foarte mare.
Se observă că dacă vk este primul nod după v1 în turul optim, subcalea acelui tur de la
vk la v1 trebuie să fie cea mai scură cale de la vk la v1 care trece prin fiecare din celelalte
vârfuri exact o dată. Aceasta înseamnă că se aplică principiul optimului şi putem, deci, folosi
programarea dinamică. Pentru aceasta, reprezentăm graful printr-o matrice de adiacenţă W, ca
în secţiunea precedentă. Matricea de adiacenţă pentru graful din fig.4.9 este:
 0 2 9 ∞
1 0 6 4
W= 
∞ 7 0 8 
 
6 3 ∞ 0
Notăm cu V=mulţimea tuturor vârdurilor
A=o submulţime a lui V
D[v i , A ] =lungimea celei mai scurte căi de la vi la v1 care trece prin fiecare vârf din A
exact o dată.
De exemplu, pentru graful fin fig.4.9 avem: V = {v1 , v 2 , v 3 , v 4 }. Se observă că se
folosesc acoladele pentru a reprezenta o mulţime si parantezele drepte pentru a reprezenta o
cale. Dacă A = {v 3 } , atunci avem:
D[v 2 , A ] = length[v 2 , v 3 , v1 ] = ∞
Dacă A = {v 3 , v 4 }, atunci avem:
D[v 2 , A ] = min (length[v 2 , v 3 , v 4 , v1 ], length[v 2 , v 4 , v 3 , v1 ]) = min(20, ∞ ) = 20
Deoarece V − {v1 , v j } conţine toate arcele cu excepţia lui v1 şi vj şi se aplică principiul
de optim, vom avea:
[
Lungimea unui tur optim= min (W[1, j] + D v j , V − {v1 , v j } ) ]
2≤ j≤ n

Şi, în general, pentru i≠1 şi vi nu aparţine lui A, vom avea:


[ ]
D[v i , A ] = min (W[i, j] + D v j , A − {v j } ), daca A ≠ Φ
j:v j∈A

D[v i , Φ ] = W[i,1]

17
Folosind formula de mai sus putem realiza un algoritm bazat pe programarea dinamică
pentru problema comisului voiajor. Înainte vom prezenta pe scurt modul de operare al
algoritmului propus.
Ne propunem să determinăm un tur optim pentru graful din fig.4.9. Mai întâi
considerăm mulţimea vidă şi obţinem:
D[v 2 , Φ ] = 1
D[v 3 , Φ ] = ∞
D[v 4 , Φ ] = 6
Apoi considerăm toate mulţimile care conţin un singur element:
[ ]
D[v 3 , {v 2 }] = min (W[3, j] + D v j , {v 2 } − {v j } ) = W[3,2] + D[v 2 , Φ ] = 7 + 1 = 8
j:v j∈{v 2 }

Similar:
D[v 4 , {v 2 }] = 3 + 1 = 4
D[v 2 , {v 3 }] = 6 + ∞ = ∞
D[v 4 , {v 2 }] = ∞ + ∞ = ∞
D[v 2 , {v 4 }] = 4 + 6 = 10
D[v 3 , {v 4 }] = 8 + 6 = 14
Apoi, să considerăm toate mulţimile cu două elemente:
[
D[v 4 , {v 2 , v 3 }] = min (W[4, j] + D v j , {v 2 , v 3 } − {v j } ) = ]
j:v j∈{v 2 , v3 }

= min (W[4,2] + D[v 2 , {v 3 }], W[4,3] + D[v 3 , {v 2 }]) =


= min (3 + ∞, ∞ + 8) = ∞
Similar:
D[v 3 , {v 2 , v 4 }] = min(7 + 10,8 + 4 ) = 12
D[v 2 , {v 3 , v 4 }] = min (6 + 14,4 + ∞ ) = 20
În final, se calculează lungimea turului optim prin:
[ ]
D[v1 , {v 2 , v 3 , v 4 }] = min (W[1, j] + D v j , {v 2 , v 3 , v 4 } − {v j } ) =
j:v j∈{v 2 , v3 , v 4 }

= min (W[1,2] + D[v 2 , {v 3 , v 4 }], W[1,3] + D[v 3 , {v 2 , v 4 }], W[1,4] + D[v 4 , {v 2 , v 3 }]) =
= min(2 + 20,9 + 12, ∞ + ∞ ) = 21
Algoritmul prin programare dinamică pentru problema comisului voiajor este:
Problema: Să se determine un tur optim într-un graf orientat şi marcat. Marcajele sunt
numere întregi pozitive.
Intrări: un graf orientat şi marcat; n=numărul de noduri din graf. Graful este
reprezentat printr-o matrice bidimensională W, pătratică de ordinul n, unde W[i,j] este
marcajul de pe arcul de la nodul i la nodul j.
Ieşiri: o variabilă minlength, a cărei valoare este lungimea unui tur optim şi o matrice
bidimensională P din care se poate constui un tur optim. P are n linii şi ca număr de coloane
numărul tuturor submulţimilor lui V-{v1}. P[i,A] este indexul primului nod după vi pe o cea
mai scurtă cale de la vi la v1 care trece prin toate vârfurile din A o singură dată.

Comis_voiajor;
procedure Travel(n:int, W(n,n):const, P(n, ):int, minlenght:int);
begin
var i,j,k:int;
var D(n, [submulţimile lui V-{v1}]):int;

18
for i=2 to n
D(i,?)=W(i,1);
for k=1 to n-2
for (toate submulţimile lui A ⊆ V-{v1} conţinând k noduri)
for (i astfel încât i ≠ 1 şi vi nu este în A)
begin
D(i,A):= min (W(i,j)+D(j,A-{vj}));
[
j: v j ∈ A ]
P(i,A):=valoarea lui j care a dat minimul de mai sus;
end;
D(1,V-{v1}):= min (W(1,j)+D(j,V-{v1,vj}));
2≤ j≤ n
P(1,V-{v1}):=valoarea lui j care a dat minimul de mai sus;
minlenght:=D(1,V-{v1});
end;

BEGIN
var n:int, P(n, ):int, W(n,n):int,lungime:int;
read n,W(n,n);
Floyd(n,W,P,lungime);
write lungime;
END

În continuare vom prezenta modalitatea de extragere a unui tur optim din matricea P.
Elementele din matricea P necesare pentru a determina un tur optim pentru graful din fig.4.9
sunt date mai jos:

Obţinerea turului optim se face prin:


Indexul primului nod = P(1, {v 2 , v 3 , v 4 }) = 3

Indexul celui de al doilea nod = P(3, {v 2 , v 4 }) = 4

Indexul celui de al treilea nod = P(4, {v 2 }) = 2


Aşadar, turul optim este: [v1 , v 3 , v 4 , v 2 , v1 ] .

19
PROBLEME PROPUSE

20
Capitolul 5
METODA GREEDY

În povestirile lui Charles Dickens există un personaj deja clasic şi anume Ebenezer
Scrooge. Acesta este probabil cea mai lacomă persoană care a existat vreodată, în ficţiune sau
în realitate. Acest Scrooge nu ţinea niciodată seama de trecut sau de viitor. Singurul său
obiectiv zilnic era să strângă cu lăcomie cât mai mult aur. După ce Spiritul Crăciunurilor
Trecute i-a amintit de trecut şi l-a avertizat în privinţa viitorului, el şi-a schimbat modul lacom
de a se purta.
Un algoritm Greedy procedează la fel ca personajul de mai sus. Adică, culege succesiv
articole de date, de fiecare dată luând cel care este considerat „cel mai bun” pe baza unor
criterii, fără a ţine seama de alegerile pe care le-a făcut înainte sau de cele pe care le va face în
viitor. Nu trebuie să credem că algoritmii Greedy nu sunt buni, datorită analogiei cu
personajul negativ Scrooge sau cu cuvântul „Greedy” (lacom). Aceşti algoritmi adesea
conduc la soluţii foarte eficiente şi simple.
La fel ca programarea dinamică, algoritmii Greedy sunt utilizaţi adesea pentru a
rezolva probleme de optimizare. Metoda Greedy are avantajul că este mai directă.
La programarea dinamică se foloseşte recursivitatea pentru a împărţi o problemă în
probleme mai mici. La metoda Greedy nu apare împărţirea în probleme mai mici. Un algoritm
Greedy găseşte o soluţie făcând o succesiune de alegeri, fiecare dintre acestea pare cea mai
bună în momentul alegerii. Adică, fiecare alegere este un optim local. Prin aceasta se speră să
se obţină o soluţie care să fie un optim global, dar nu este întotdeauna aşa. Pentru un anumit
algoritm, trebuie să determinăm dacă soluţia este întotdeauna optimă.
În continuare vom prezenta un exemplu deja clasic pentru metoda Greedy.
George, un vânzător, trebuie să dea restul la cumpărături. Clienţii de obicei nu doresc
să primească multe monede. De exemplu, majoritatea clienţilor ar fi nervoşi dacă ar primi 87
de monezi de un ban atunci când restul ar fi 87 de bani. Aşadar, obiectivul este nu numai să se
dea restul corect, ci şi cu cât mai puţine monede posibil. O soluţie la o asemenea problemă a
restului este un set de monede a căror valoare totală este egală cu suma datorată
cumpărătorului, iar o soluţie optimă este un asemenea set cu număr minim de monede.
O abordare Greedy la această problemă ar putea fi după cum urmează:
• George începe să caute cea mai mare monedă (ca valoare) pe care o are. Adică,
criteriul său de decidere a celei mai bune monede (optim local) este valoarea
monedei. Aceasta, într-un algoritm Greedy, se numeşte procedura de selecţie.
• Apoi, verifică dacă adunând această monedă la rest valoarea rezultată nu
depăşeşte valoarea datorată. Aceasta, într-un algoritm Greedy, se numeşte
procedura de fezabilitate. Dacă adăugarea monedei nu depăşeşte suma
datorată, atunci adaugă moneda la rest.
• Apoi, verifică dacă valoarea totală a monedelor strânse până acum este egală
cu suma datorată. Aceasta, într-un algoritm Greedy, se numeşte verificarea
soluţiei. Dacă nu, George alege o nouă monedă după acelaşi criteriu şi repetă
întregul proces până când valoarea restului este egală cu suma datorată sau nu
mai are monede din care să aleagă. În cazul în care i se termină monedele el nu
va putea returna exact suma datorată.
În continuare se prezintă algoritmul Greedy scris în pseudocod pentru etapele descrise
mai sus.
While mai există monede şi problema nu este soluţionată
Begin
Ia cea mai mare monedă dintre cele rămase; procedura de selecţie
If adăugarea monedei face restul să depăşească suma datorată
Respinge moneda; testul de fezabilitate
Else
Adaugă moneda la rest;
If valoarea totală a restului este egală cu suma datorată
Problema este rezolvată;
End

Algoritmul are următoarea forma :

monede(v[1..n],C)
v[1..n] sortare descrescătoare(v[1..n])
FOR i ←1, n DO S[i] ← 0
i←1
WHILE C > 0 AND i ≤ n DO
S[i]← C DIV v[i]
C ← C MOD v[i]
i←i+1
IF C = 0 THEN RETURN S[1..n]
ELSE "nu s-a gasit solutie"

Observaţie. Tehnica greedy nu conduce pentru orice instanţă a problemei monedelor


la soluţia optimă. De exemplu pentru cazul monedelor cu valorile (25, 20, 10, 5, 1) şi a valorii
C = 40 strategia greedy ar conduce la soluţia (1, 0, 1, 1, 0) în timp ce varianta (0, 2, 0, 0, 0)
este optimă. Pe de altă parte există situaţii în care problema nu are soluţii. De exemplu pentru
monede având valorile (20, 10,5) şi C = 17 problema nu are soluţie. Se observă uşor că dacă
există monedă cu valoarea 1 atunci pentru orice sumă C se poate determina o soluţie. Aceasta
nu este însă neapărat optimală. Tehnica greedy conduce la o soluţie optimă pentru problema
monedelor doar dacă valorile acestora satisfac anumite restricţii. Un exemplu de valori pentru
care se poate demonstra optimalitatea soluţiei este: vi-1 este multiplu al lui vi pentru fiecare i ≥
2 (în condiţiile în care v[1..n] este ordonat crescător).

Metoda greedy este alcătuită din următorii paşii :


• mulţime de candidaţi (lucrări de executat, vârfuri ale grafului etc.)
• funcţie care verifica daca o anumita mulţime de candidaţi constituie o soluţie posibila,
nu neapărat optima, a problemei
• funcţie care verifica daca o mulţime de candidaţi este fezabila, adică daca este posibil
sa completam aceasta mulţime astfel încât sa obţinem o soluţie posibila, nu neapărat
optima, a problemei
• funcţie de selecţie care indica la orice moment care este cel mai promiţător dintre
candidaţii încă nefolosiţi
• funcţie obiectiv care da valoarea unei soluţii (timpul necesar executării tuturor
lucrărilor intr-o anumita ordine, lungimea drumului pe care l-am găsit etc.); aceasta
este funcţia pe care urmărim sa o optimizăm (minimizăm/maximizăm)

Un algoritm greedy construieşte soluţia pas cu pas. Iniţial, mulţimea candidaţilor


selectaţi este vidă. La fiecare pas, încercam sa adăugam acestei mulţimi cel mai promiţător
candidat, conform funcţiei de selecţie. Dacă, după o astfel de adăugare, mulţimea de candidaţi
selectaţi nu mai este fezabilă, eliminăm ultimul candidat adăugat; acesta nu va mai fi
niciodată considerat. Dacă, după adăugare, mulţimea de candidaţi selectaţi este fezabilă
ultimul candidat adăugat va rămâne de acum încolo în ea. De fiecare dată când lărgim

2
mulţimea candidaţilor selectaţi, verificăm dacă această mulţime nu constituie o soluţie
posibilă a problemei noastre. Daca algoritmul greedy funcţionează corect, prima soluţie găsită
va fi totodată o soluţie optimă a problemei. Soluţia optimă nu este in mod necesar unică: se
poate ca funcţia obiectiv sa aibă aceeaşi valoare optimă pentru mai multe soluţii posibile.

Descrierea formală 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 U {x}) then
S ← S U {x}
if solutie(S) then
return S
else
return “nu exista solutie”

5.1. Aplicaţii
O clasa frecvent întâlnită în practică este cea a problemelor de optimizare de tipul:
Să se determine x care aparţine lui X astfel încât :
i) x satisface anumite condiţii (restricţii);
ii) x optimizează (minimizează sau maximizează) un criteriu.
O subclasa importanta în informatică o reprezintă cea în care X este o mulţime finita,
problema de optimizare fiind numită în acest caz discretă sau combinatoria. La o primă
vedere problema pare foarte simplă întrucât este suficient să se parcurgă X şi să se aleagă
elementul care satisface restricţiile şi optimizează criteriul. O astfel de abordare (numită şi
metoda forţei brute) devine total ineficientă (sau chiar impracticabilă) atunci când numărul de
elemente din X este foarte mare.

Exemplu (Problema submulţimii de cardinal dat şi sumă maximă.) Se cere


determinarea unui subset cu m elemente, S, dintr-un set A având n > m elemente astfel încât
suma elementelor selectate să fie cât mai mare. În acest caz este suficient să ordonăm
descrescător elementele din A şi să reţinem primele m elemente:
In acest caz este suficient să ordonăm descrescător elementele din A şi să reţinem
primele m elemente:

submulţime(A[1..n],m)
A[1..n] sortare_descrescătoare(A[1..n])
FOR i←1,m DO S[i] ←A[i]
RETURN S[1..m]

A1. Problema rucsacului.


Este o problemă clasică ce are multe aplicaţii şi diverse variante. Se consideră un set
de obiecte caracterizate prin profitul (p) şi dimensiunea lor (d): A = ((p1, d1) (pn , dn)).
Se mai consideră un rucsac de capacitate maximă C şi se pune problema selectării
unui subset de obiecte, ((pi1 , di1) … (pik , dik )) care să nu depăşească capacitatea rucsacului
şi să asigure un profit maxim.

3
Două dintre cele mai cunoscute variante ale problemei sunt:
- Varianta discretă (0-1). Obiectele nu pot fi divizate: un obiect este fie preluat în
întregime fie nu este preluat.
- Varianta continuă (fracţionară). Este posibil să fie transferate şi fracţiuni din
obiecte, profitul asigurat fiind proporţional cu fracţiunea.
Doar pentru varianta fracţionară se poate obţine o soluţie optimă aplicând strategia
greedy. În acest caz alegerea local optimală este cea care corespunde profitului relativ maxim
(profitul relativ al obiectului i este pi / di). Aplicând strategia greedy se ajunge la a efectua
următoarele prelucrări:
(i) se ordonează A descrescător după profitul relativ;
(ii) se transferă obiectele în ordinea în care apar în A, în întregime cu excepţia
ultimului obiect din care se ia doar o fracţiune, atât cât să se umple rucsacul.
Pentru a descrie algoritmul facem următoarele convenţii: A[i].p, A[i].d reprezintă
profitul respectiv dimensiunea obiectului i. Soluţia va fi de forma: S = (S[1] , … , S[n]) cu S[i]
aparţine lui [0; 1], iar elementele sale vor fi interpretate astfel: S[i] = 0 - obiectul i nu este
selectat, S[i] = 1 – obiectul i este selectat în întregime, S[i] aparţine lui (0; 1) - este selectată
doar o fracţiune din obiectul i. Cu aceste convenţii algoritmul poate fi descris astfel:

rucsac fractionar(A[1::n],C)
A[1 .. n] sortare descrescătoare(A[1..n]) //sortare după profitul relativ
FOR i ← 1, n DO S[i] ← 0
i ←1
WHILE C > 0 AND i ≤ n DO
IF S[i].d ≤ C
THEN S[i]←1; C ← C - A[i].d
ELSE S[i]←C/A[i].d; C←0
i← i + 1
RETURN S[1..n]

A2. Problema selectării activităţilor


Se consideră un set de activităţi care au nevoie de o anumită resursă şi la un moment
dat o singură activitate poate beneficia de resursa respectivă (de exemplu activitatea poate fi
un examen, iar resursa o sală de examen). Presupunem că pentru fiecare activitate, Ai, se
cunoaşte momentul de începere, pi şi cel de finalizare ti (ti > pi). Presupunem că activitatea se
desfăşoară în intervalul [pi; ti). Două activităţi Ai şi Aj se consideră compatibile dacă
intervalele asociate sunt disjuncte. Se cere să se selecteze un număr cât mai mare de activităţi
compatibile.
O soluţie a acestei probleme constă într-un subset de activităţi S = (ai1 ,… , aim) care
satisfac [pij , tij ) ∩ [pik , tik ) = 0 pentru orice j ≠ k.
Criteriul de selecţie ar putea fi:
(i) cea mai mică durată;
(ii) cel mai mic moment de începere;
(iii) cel mai mic moment de sfârşit;
(iv) intervalul care se intersectează cu cele mai puţine alte intervale.
Dintre aceste variante cea pentru care se poate demonstra că asigură obţinerea soluţiei
optime este a treia: la fiecare etapă se alege activitatea care se termină cel mai devreme.
Notând cu A[i].p, A[i].t momentul de începere respectiv cel de final al activităţii A[i]
algoritmul bazat pe strategia greedy poate fi descris astfel:

4
selecţie activităţi(A[1..n])
A[1..n] ← sortare crescătoare(A[1..n]) // sortare după timpul de finalizare
S[1]← A[1]
i←2; k←1
WHILE i≤ n DO
IF S[k].t ≤ A[i].p
THEN k←k + 1; S[k] ← A[i];
i←i + 1
RETURN S[1..k]

A3. Minimizarea timpului mediu de aşteptare


O singura staţie de servire (procesor, pompa de benzina etc.) trebuie sa satisfacă
cererile a n clienţi. Timpul de servire necesar fiecărui client este cunoscut in prealabil: pentru
clientul i este necesar un timp ti, 1 ≤ i ≤ n. Dorim sa minimizam timpul total de aşteptare
n
T = ∑ ( (timpul de asteptare pentru clientul i))
i
ceea ce este acelaşi lucru cu a minimiza timpul mediu de aşteptare, care este T/n.
De exemplu, daca avem trei clienţi cu t1 = 5, t2 = 10, t3 = 3, sunt posibile sase ordini
de servire. In primul caz, clientul 1 este servit primul, clientul 2 aşteaptă pana este servit
clientul 1 si apoi este servit, clientul 3 aşteaptă pana sunt serviţi clienţii 1, 2 si apoi este servit.
Timpul total de aşteptare a celor trei clienţi este 38.

Ordinea T

123 5+(5+10)+(5+10+3) = 38
132 5+(5+3)+(5+3+10) = 31
213 10+(10+5)+(10+5+3) = 43
231 10+(10+3)+(10+3+5) = 41
312 3+(3+5)+(3+5+10) = 29 ← optim
321 3+(3+10)+(3+10+5) = 34

Algoritmul greedy este foarte simplu: la fiecare pas se selectează clientul cu timpul
minim de servire din mulţimea de clienţi rămasă.
Prin metoda greedy obţinem deci întotdeauna planificarea optima a clienţilor.
Problema poate fi generalizata pentru un sistem cu mai multe staţii de servire.

A4. Coduri Huffman


O alta aplicaţie a strategiei greedy si a arborilor binari cu lungime externa ponderata
minima este obţinerea unei codificări cat mai compacte a unui text.
Un principiu general de codificare a unui sir de caractere este următorul: se măsoară
frecventa de apariţie a diferitelor caractere dintr-un eşantion de text si se atribuie cele mai
scurte coduri, celor mai frecvente caractere, si cele mai lungi coduri, celor mai puţin frecvente
caractere. Acest principiu sta, de exemplu, la baza codului Morse. Pentru situaţia in care
codificarea este binara, exista o metoda eleganta pentru a obţine codul respectiv. Aceasta
metoda, descoperita de Huffman (1952) foloseşte o strategie greedy si se numeşte codificarea
Huffman. O vom descrie pe baza unui exemplu. Fie un text compus din următoarele litere (in
paranteze figurează frecventele lor de apariţie):
S (10), I (29), P (4), O (9), T (5)

5
Conform metodei greedy, construim un arbore binar fuzionând cele doua litere cu
frecventele cele mai mici. Valoarea fiecărui vârf este data de frecventa pe care o reprezintă.

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


S (10), I (29), O (9), {P, T} (4+5 = 9)
Multimea {P, T} semnifica evenimentul reuniune a celor doua evenimente
independente corespunzătoare apariţiei literelor P si T. Continuam procesul, obţinând arborele

In final, ajungem la arborele din Figura 5.1, in care fiecare vârf terminal corespunde
unei litere din text.
Pentru a obţine codificarea binara a literei P, nu avem decât sa scriem secvenţa de 0-
uri si 1-uri in ordinea apariţiei lor pe drumul de la rădăcina către vârful corespunzător 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 vârfurile terminale având valorile f1, f2, …, fn, prin care se
obţine o codificare binara a textului. Un arbore de codificare nu trebuie in mod necesar sa fie
construit după metoda greedy a lui Huffman, alegerea vârfurilor care sunt fuzionate la fiecare
pas putându-se face după diverse criterii. Lungimea externa ponderata a unui arbore de
codificare este:
n

∑a i =1
i fi

Fig. 5.1 Arborele de codificare Huffman.

unde ai este adâncimea vârfului terminal corespunzător literei i. Se observa ca lungimea


externa ponderata este egala cu numărul 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
minimizează lungimea externa ponderata pentru toţi arborii de codificare cu vârfurile
terminale având valorile f1, f2, …, fn. Prin strategia greedy se obţine deci întotdeauna
codificarea binara cea mai compacta a unui text.

6
Arborii de codificare pe care i-am considerat in acesta secţiune corespund unei
codificări de tip special: codificarea unei litere nu este prefixul codificării 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 întotdeauna obtinută printr-un
cod de tip prefix. Deci, concentrându-ne atenţia asupra acestei categorii de coduri, nu am
pierdut nimic din generalitate.

A5. Arbori parţiali de cost minim


Fie G = <V, M> un graf neorientat conex, unde V este multimea vârfurilor si M este
multimea muchiilor. Fiecare muchie are un cost nenegativ (sau o lungime nenegativa).
Problema este sa găsim o submulţime A ∈ M , astfel încât toate vârfurile din V sa rămână
conectate atunci când sunt folosite doar muchii din A, iar suma lungimilor muchiilor din A sa
fie cat mai mica. Căutam deci o submulţime A de cost total minim. Aceasta problema se mai
numeşte si problema conectării oraşelor cu cost minim, având numeroase aplicaţii. Graful
parţial <V, A> este un arbore si este numit arbore parţial de cost minim al grafului G (minimal
spanning tree). Un graf poate avea mai mulţi arbori parţiali de cost minim si acest lucru se
poate verifica pe un exemplu. Vom prezenta doi algoritmi greedy care determina arborele
parţial de cost minim al unui graf. In terminologia metodei greedy, vom spune ca o mulţime
de muchii este o soluţie, daca constituie un arbore parţial al grafului G, si este fezabila, daca
nu conţine cicluri. O mulţime fezabila de muchii este promiţătoare, daca poate fi completata
pentru a forma soluţia optima. O muchie atinge o mulţime data de vârfuri, daca exact un capăt
al muchiei este in mulţime. Următoarea proprietate va fi folosita pentru a demonstra
corectitudinea celor doi algoritmi.
Proprietatea 5.1 Fie G = <V, M> un graf neorientat conex in care fiecare muchie are
un cost nenegativ. Fie W ∈ V o submulţime stricta a vârfurilor lui G si fie A ∈ M o mulţime
promiţătoare de muchii, astfel încât nici o muchie din A nu atinge W. Fie m muchia de cost
minim care atinge W. Atunci, A ∈{m} este promiţătoare.

A.5.1 Algoritmul lui Kruskal


Arborele parţial de cost minim poate fi construit muchie, cu muchie, după următoarea
metoda a lui Kruskal (1956): se alege întâi muchia de cost minim, iar apoi se adăuga repetat
muchia de cost minim nealeasa anterior si care nu formează cu precedentele un ciclu. Alegem
astfel V–1 muchii. Este uşor de dedus ca obţinem in final un arbore. Este insa acesta chiar
arborele parţial de cost minim căutat?
Înainte de a răspunde la întrebare, sa consideram, de exemplu, graful din Figura 5.2a.

Fig.5.2. Un graf si arborele sau parţial de cost minim.

7
Ordonam crescător (in funcţie 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 6.1.
Multimea A este iniţial vida si se completează pe parcurs cu muchii acceptate (care nu
formează un ciclu cu muchiile deja existente in A). In final, multimea A va conţine muchiile
{1, 2}, {2, 3}, {4, 5}, {6, 7}, {1, 4}, {4, 7}. La fiecare pas, graful parţial <V, A> formează o
pădure de componente conexe, obţinuta din pădurea precedenta unind doua componente.
Fiecare componenta conexa este la rândul ei un arbore parţial de cost minim pentru
vârfurile pe care le conectează.
Iniţial, fiecare vârf formează o componenta conexa. La sfârşit, vom avea o singura
componenta conexa, care este arborele parţial de cost minim căutat (Figura 5.2b).

Pasul Muchia Componentele conexe ale


considerata subgrafului <V, A>
iniţializare — {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 (formează ciclu)
7 {4, 7} {1, 2, 3, 4, 5, 6, 7}
Tabelul 5.1 Algoritmul lui Kruskal aplicat grafului din Figura 5.2a.

Ceea ce am observat in acest caz particular este valabil si pentru cazul general, din
Proprietatea 5.1 rezultând:
Proprietatea 5.2 In algoritmul lui Kruskal, la fiecare pas, graful parţial <V, A>
formează o pădure de componente conexe, in care fiecare componenta conexa este la rândul ei
un arbore parţial de cost minim pentru vârfurile pe care le conectează.
In final, se obţine arborele parţial de cost minim al grafului G. Pentru a implementa
algoritmul, trebuie sa putem manipula submulţimile formate din vârfurile componentelor
conexe. Folosim pentru aceasta o structura de mulţimi 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 încât sa putem ordona aceasta lista in funcţie de cost. Iată algoritmul:

function Kruskal(G = <V, M>)


{initializare}
sorteaza M crescător in funcţie de cost
n ← #V
A ← ∅ {va conţine muchiile arborelui parţial de cost minim}
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 considerata
ucomp ← find(u)
vcomp ← find(v)
if ucomp ≠ vcomp then merge(ucomp, vcomp)

8
A ← A U {{u, v}}
until #A = n–1
return A

Pasul Muchia considerata U


iniţializare — {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}
5 {7, 4} {1, 2, 3, 4, 5, 6}
6 {6, 7} {1, 2, 3, 4, 5, 6, 7}
Tabelul 5.2 Algoritmul lui Prim aplicat grafului din Figura 5.2a.

A.5.2 Algoritmul lui Prim


Cel de-al doilea algoritm greedy pentru determinarea arborelui parţial de cost minim al
unui graf se datorează lui Prim (1957). In acest algoritm, la fiecare pas, multimea A de muchii
alese împreuna cu multimea U a vârfurilor pe care le conectează formează un arbore parţial de
cost minim pentru subgraful <U, A> al lui G. Iniţial, multimea U a vârfurilor acestui arbore
conţine un singur vârf oarecare din V, care va fi rădăcina, iar multimea A a muchiilor este
vida. La fiecare pas, se alege o muchie de cost minim, care se adăuga la arborele precedent,
dând naştere unui nou arbore parţial de cost minim (deci, exact una dintre extremităţile acestei
muchii este un vârf in arborele precedent). Arborele parţial de cost minim creste “natural”, cu
cate o ramura, până când va atinge toate vârfurile din V, adică până când U = V. Funcţionarea
algoritmului, pentru exemplul din Figura5.2a, este ilustrata in Tabelul 5.3. La sfârşit, A va
conţine aceleaşi muchii ca si in cazul algoritmului lui Kruskal. Faptul ca algoritmul
funcţionează întotdeauna corect este exprimat de următoarea proprietate, pe care o puteţi
demonstra folosind Proprietatea 5.2.

Pasul Muchia considerata U


iniţializare — {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}
5 {7, 4} {1, 2, 3, 4, 5, 6}
6 {6, 7} {1, 2, 3, 4, 5, 6, 7}
Tabelul 5.3 Algoritmul lui Prim aplicat grafului din Figura 5.4a.

Proprietatea 5.3 In algoritmul lui Prim, la fiecare pas, <U, A> formează un arbore
parţial de cost minim pentru subgraful <U, A> al lui G. In final, se obţine arborele parţial de
cost minim al grafului G.
Descrierea formala a algoritmului este data in continuare.

function Prim-formal(G = <V, M>)


{iniţializare}
A ← ∅ {va conţine muchiile arborelui parţial de cost minim}

9
U ← {un vârf 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 {{u, v}}
U ← U U {u}
return A

Pentru a obţine o implementare simpla, presupunem ca: vârfurile din V sunt


numerotate de la 1 la n, V = {1, 2, …, n}; matricea simetrica C da costul fiecărei muchii, cu
C[i, j] = +∞, daca muchia { i, j} nu exista. Folosim doua tablouri paralele. Pentru fiecare
i∈ V \ U , vecin[i] conţine vârful 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
iniţializata cu {1}, nu este reprezentata explicit. Elementele vecin[1] si mincost[1] nu se
folosesc.

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


{iniţializare; numai vârful 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 U {{k, vecin[k]}}
mincost[k] ← –1 {adăuga vârful 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

A.5.3 Cele mai scurte drumuri care pleacă din acelaşi punct
Fie G = <V, M> un graf orientat, unde V este multimea vârfurilor si M este multimea
muchiilor. Fiecare muchie are o lungime nenegativa. Unul din vârfuri este desemnat ca vârf
sursa. Problema este sa determinam lungimea celui mai scurt drum de la sursa către fiecare
vârf din graf.
Vom folosi un algoritm greedy, datorat lui Dijkstra (1959). Notam cu C multimea
vârfurilor disponibile (candidaţii) si cu S multimea vârfurilor deja selectate. In fiecare
moment, S conţine acele vârfuri a căror distanta minima de la sursa este deja cunoscuta, in
timp ce multimea C conţine toate celelalte vârfuri. La început, S conţine doar vârful sursa, iar
in final S conţine toate vârfurile grafului. La fiecare pas, adăugam in S acel vârf din C a cărui
distanta de la sursa este cea mai mica.
Spunem ca un drum de la sursa către un alt vârf este special, daca toate vârfurile
intermediare de-a lungul drumului aparţin lui S. Algoritmul lui Dijkstra lucrează in felul
următor. La fiecare pas al algoritmului, un tablou D conţine lungimea celui mai scurt drum
special către fiecare vârf al grafului. După ce adăugam un nou vârf v la S, cel mai scurt drum
special către v va fi, de asemenea, cel mai scurt dintre toate drumurile către v. Când algoritmul

10
se termina, toate vârfurile din graf sunt in S, deci toate drumurile de la sursa către celelalte
vârfuri sunt speciale si valorile din D reprezintă soluţia problemei. Presupunem, pentru
simplificare, ca vârfurile sunt numerotate, V = {1, 2, …, n}, vârful 1 fiind sursa, si ca matricea
L da lungimea fiecărei muchii, cu L[i, j] = +∞, daca muchia (i, j) nu exista. Soluţia se va
construi in tabloul D[2 .. n].
Algoritmul este:

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


{iniţializare}
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 ← vârful din C care minimizează D[v]
C ← C \ {v} {si, implicit, S ← S U{v}}
for fiecare w ∈ C do
D[w] ← min(D[w], D[v]+L[v, w])
return D

Pentru graful din Figura 5.3, paşii algoritmului sunt prezentaţi in Tabelul 5.4.
Observam ca D nu se schimba daca mai efectuam o iteraţie pentru a-l scoate si pe {2} din C.
De aceea, bucla greedy se repeta de doar n−2 ori. Se poate demonstra următoarea proprietate:
Proprietatea 5.4. In algoritmul lui Dijkstra, daca un vârf i
i) este in S, atunci D[i] da lungimea celui mai scurt drum de la sursa către i;
ii) nu este in S, atunci D[i] da lungimea celui mai scurt drum special de la sursa către i.
La terminarea algoritmului, toate vârfurile grafului, cu excepţia unuia, sunt in S. Din
proprietatea precedenta, rezulta ca algoritmul lui Dijkstra funcţionează corect. Daca dorim sa
aflam nu numai lungimea celor mai scurte drumuri, dar si pe unde trec ele, este suficient sa
adăugam un tablou P[2 .. n], unde P[v] conţine numărul nodului care îl precede pe v in cel mai
scurt drum.

Fig. 5.3. Un graf orientat

Pasul v C D
iniţializare — {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 5.4 Algoritmul lui Dijkstra aplicat grafului din Figura 5.3.

Pentru a găsi drumul complet,nu avem decât sa urmărim, in tabloul P, vârfurile prin
care trece acest drum, de la destinaţie la sursa.

11
Modificările in algoritm sunt simple:
• iniţializează P[i] cu 1, pentru 2 ≤ i ≤ n
• conţinutul buclei for cea mai interioara se înlocuieşte 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

Euristica greedy
Pentru anumite probleme, se poate accepta utilizarea unor algoritmi despre care nu se
ştie daca furnizează soluţia optima, dar care furnizează rezultate “acceptabile”, sunt mai uşor
de implementat si mai eficienţi decât algoritmii care dau soluţia optima. Un astfel de algoritm
se numeşte euristic.
Una din ideile frecvent utilizate in elaborarea algoritmilor euristici consta in
descompunerea procesului de căutare a soluţiei optime in mai multe subprocese succesive,
fiecare din aceste subprocese constând dintr-o optimizare. O astfel de strategie nu poate
conduce întotdeauna la o soluţie optima, deoarece alegerea unei soluţii optime la o anumita
etapa poate împiedica atingerea in final a unei soluţii optime a întregii probleme; cu alte
cuvinte, optimizarea locala nu implica, in general, optimizarea globala. Regăsim, de fapt,
principiul care sta la baza metodei greedy. Un algoritm greedy, despre care nu se poate
demonstra ca furnizează soluţia 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 cărui vârfuri trebuie colorate astfel incot
oricare doua vârfuri adiacente sa fie colorate diferit. Problema este de a obţine o colorare cu
un număr minim de culori.
Folosim următorul algoritm greedy: alegem o culoare si un vârf arbitrar de pornire,
apoi consideram vârfurile ramase, încercând sa le coloram, fără a schimba culoarea. Când nici
un vârf nu mai poate fi colorat, schimbam culoarea si vârful de start, repetând procedeul.
Daca in graful din fig.5.4 pornim cu vârful 1 si îl coloram in roşu, mai putem colora
tot in roşu vârfurile 3 si 4. Apoi, schimbam culoarea si pornim cu vârful 2, colorându-l in
albastru.

Fig. 5.4. Graf care va fi colorat

Mai putem colora cu albastru si vârful 5. Deci, ne-au fost suficiente doua culori. Daca
coloram vârfurile in ordinea 1, 5, 2, 3, 4, atunci se obţine o colorare cu trei culori. Rezulta ca,
prin metoda greedy, nu obţinem decât o soluţie euristica, care nu este in mod necesar soluţia
optima a problemei. De ce suntem atunci interesaţi intr-o astfel de rezolvare? Toţi algoritmii
cunoscuţi, care rezolva optim aceasta problema, sunt exponenţiali, deci, practic, nu pot fi
folosiţi pentru cazuri mari. Algoritmul greedy euristic propus furnizează doar o soluţie
“acceptabila”, dar este simplu si eficient.
Un caz particular al problemei colorării unui graf corespunde celebrei probleme a

12
colorării harţilor: o harta oarecare trebuie colorata cu un număr minim de culori, astfel încât
doua tari cu frontiera comuna sa fie colorate diferit. Daca fiecărui vârf ii corespunde o tara, iar
doua vârfuri adiacente reprezintă tari cu frontiera comuna, atunci harţii ii corespunde un graf
planar, adică un graf care poate fi desenat in plan fără ca doua muchii sa se intersecteze.
Celebritatea problemei consta in faptul ca, in toate exemplele întâlnite, 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. Recent s-a demonstrat pe calculator faptul ca orice
harta poate fi colorata cu cel mult 4 culori. Este prima demonstrare pe calculator a unei
teoreme importante.
Problema colorării unui graf poate fi interpretata si in contextul planificării unor
activitatea. De exemplu, sa presupunem ca dorim sa executam simultan o mulţime de
activitatea, in cadrul unor săli de clasa. In acest caz, vârfurile grafului reprezintă activitatea,
iar muchiile unesc activităţile incompatibile. Numărul minim de culori necesare pentru a
colora graful corespunde numărului minim de săli necesare.

PROBLEME PROPUSE

1. Presupunând ca exista monezi de:


i) 1, 5, 12 si 25 de unităţi, găsiţi un contraexemplu pentru care algoritmul
greedy nu găseşte soluţia optima;
ii) 10 si 25 de unităţi, găsiţi un contraexemplu pentru care algoritmul greedy nu
găseşte nici o soluţie cu toate ca exista soluţie.

2. Scrieţi algoritmul greedy pentru colorarea unui graf si analizaţi eficienta lui.

3. Problema împachetării. Se consideră un set de numere a1, a2, … , an cu proprietatea


că ai aparţine lui (0,1]. Se cere să se grupeze în cât mai puţine subseturi (k) astfel încât suma
elementelor din fiecare subset să nu depăşească valoarea 1.
Indicaţie. Elementele setului iniţial pot fi în orice ordine. Varianta 1. Se construiesc
succesiv submulţimile: se transferă (în ordinea în care se afla în set) în primul subset atâtea
elemente cât este posibil, din elementele rămase se transferă elemente în al doilea subset etc.
(nu e garantată optimalitatea soluţiei însă s-a demonstrat că k < 2kopt, k fiind numărul de
subseturi generat prin strategia greedy de mai sus, iar kopt este numărul optim de subseturi).
Varianta 2. Se iniţializează n subseturi vide. Se parcurge setul de numere şi fiecare număr se
transferă în primul subset în care "încape". Numărul de subseturi nevide astfel constituite k,
are proprietatea k < 1.7kopt. Varianta 3. Dacă se ordonează descrescător setul iniţial şi se
aplică varianta 2 se obţine o îmbunătăţire: k < 11/9kopt + 4.
4. Să se aplice strategia greedy pentru a rezolva problema discretă a rucsacului.
Indicaţie. Se ordonează obiectele descrescător după profitul relativ şi se selectează în
această ordine. Obiectele care nu încap în rucsac în etapa selectării lor sunt ignorate.
5. Se consideră o variantă a problemei rucsacului în care ordonarea obiectelor după
profit coincide cu ordonarea obiectelor după dimensiune (un caz particular este acela în care
sunt egale). Să se elaboreze un algoritm bazat pe strategia greedy în care selecţia se face în
ordinea profiturilor absolute. Conduce algoritmul la soluţia optimă ?
6. Se consideră o variantă a problemei rucsacului în care toate obiectele au aceeaşi
dimensiune (dar profituri diferite). Să se elaboreze un algoritm bazat pe strategia greedy care

13
să rezolve problema. Se obţine soluţia optimă ? Aceeaşi problemă pentru cazul în care toate
profiturile sunt identice (dar dimensiunile sunt diferite).

Pentru un text format din n litere care apar cu frecventele f1, f2, …, fn,
demonstraţi ca arborele de codificare Huffman minimizează lungimea externa
ponderata pentru toţi arborii de codificare cu vârfurile terminale având valorile
f1, f2, …, fn.

Căţi biţi ocupa textul “ABRACADABRA” după codificarea Huffman?

14
Capitolul 6
METODA BACKTRACKING

Dacă am încerca să căutăm drumul prin bine cunoscutul labirint făcut din gard viu
aflat în Hampton Court Palace în Anglia, nu am avea altă soluţie decât să urmăm orbeşte o
cale până când găsim un capăt de drum. Când se întâmplă aceasta, trebuie să ne întoarcem la o
bifurcaţie şi să încercăm o altă cale. Oricine a încercat vreodată să rezolve un labirint a trăit
frustrarea ajungerii la capete de drum. Ce bine ar fi dacă ar exista semne care să indice că un
anumit drum nu conducere la nimic altceva decât un capăt. Dacă semnul ar fi poziţionat
aproape de începutul unei căi, timpul economisit ar fi foarte mare, deoarece toate bifurcaţiile
de după acel semn nu ar mai fi luate în considerare. Aceasta înseamnă că ar fi evitate nu unul
ci mai multe capete de drum. Dar în celebrul labirint din gard viu, ca şi în majoritatea
labirinturilor, nu există asemenea semne. Însă, aşa cum vom vedea, în algoritmii de tip
backtracking aceste semne există.

6.1. Tehnica Backtracking


Metoda Backtracking este folosită pentru a rezolva probleme în care este aleasă o
secvenţă de obiecte dintr-un set specificat astfel încât acea secvenţă să satisfacă unele criterii.
Exemplul clasic de utilizare a metodei backtracking este cel al problemei n-Reginelor.
Obiectivul acestei probleme este să se poziţioneze n regine pe o tablă de şah de dimensiune n
x n în aşa fel încât nici o regină să nu o ameninţe pe alta. Adică, nu pot exista două regine pe
acelaşi rând, coloană sau diagonală. Secvenţa în această problemă constă din cele n poziţii în
care sunt plasate reginele, setul pentru fiecare alegere constă din cele n2 poziţii posibile pe
tabla de şah şi criteriul este ca nici o regină să nu o ameninţe pe alta. Problema n-reginelor
este o generalizare a cazului n=8, care corespunde utilizării unei table de şah standard. Pentru
a merge mai repede explicarea algoritmului, vom considera cazul în care n=4.
Metoda backtracking este o modificare a căutării în adâncime Depth-First-Search a
unui arbore. Mai întâi vom recapitula procedura de căutare în adâncime Depth-First-Search.
Deşi această căutare este definită pentru grafuri în general, vom discuta numai căutarea în
arbori, deoarece backtracking-ul presupune numai căutarea în arbori. O căutare în adâncime
Depth-First-Search a unui arbore este de fapt traversarea arborelui în preordine. Aceasta
înseamnă că mai întâi este vizitată rădăcina şi o vizită a unui nod este urmată imediat de
vizitarea tuturor descendenţilor nodului. Deşi căutarea în adâncime Depth-First-Search nu
necesită vizitarea fiilor într-o anumită ordine, vom considera ordinea de la stânga la dreapta în
toate aplicaţiile din acest capitol.
În fig.6.1 se prezintă o căutare în adâncime Depth-First-Search a unui copac realizată
prin parcurgerea fiilor de la stânga la dreapta. Nodurile sunt numerotate în ordinea în care
sunt vizitate. Se observă că la căutarea Depth-First-Search o cale este urmată cât de adânc se
poate până se găseşte un capăt. La capăt de drum ne întoarcem până găsim un nod cu un fiu
nevizitat şi apoi încercăm din nou să coborâm cât se poate de adânc.

Fig.6.1. Un arbore cu nodurile numerotate conform căutării Depth-First-Search


Există un algoritm recursiv simplu pentru a face o căutare Depth-First-Search.
Deoarece momentan ne interesează numai parcurgerea arborilor în preordine, vom prezenta o
variantă care realizează această parcurgere în mod explicit. Procedura se apelează cu rădăcina
la nivelul cel mai înalt.

procedure depth_first_tree_search (v:node);


begin
u:node;
viziteaza v;
for fiecare fiu u al lui v
depth_first_tree_search(u);
end

Acest algoritm de uz general nu cere ca vizitarea fiilor să se facă într-o anumită


ordine. Oricum, aşa cum s-a menţionat anterior, vizitarea se face de la stânga la dreapta.
Acum, vom prezenta tehnica backtracking pentru problema n-Reginelor în cazul n=4.
Scopul este să poziţionăm patru regine pe o tablă de şah de dimensiune 4 x 4 astfel încât nici o
regină să nu o ameninţe pe alta. Putem simplifica imediat lucrurile prin observarea că nu se
pot plasa două regine pe acelaşi rând. Deci, primul pas este asignarea fiecărei regine un rând
diferit şi verificarea combinaţiei de coloane care conduce la soluţie. Deoarece fiecare regină
poate fi plasată în una din cele patru coloane, există 4 x 4 x 4 x 4 = 256 de soluţii candidat.
Putem crea soluţiile candidat construind un arbore în care alegerile coloanei pentru
prima regină (cea de pe rândul 1) sunt stocate în nodurile de pe nivelul 1 din arbore (rădăcina
este la nivelul 0), alegerile coloanei pentru a doua regină (cea din rândul 2) sunt stocate în
nodurile de pe nivelul 2 şi aşa mai departe.
O cale de la rădăcină la o frunză reprezintă o soluţie candidat (o frunză din arbore este
un nod fără fii). Acest arbore se numeşte un arbore al spaţiului de stare. O parte din acest
arbore se poate vedea în fig.6.2. Întregul arbore are 256 de frunze, căte una pentru fiecare
soluţie candidat. Se observă că în fiecare nod este stocată o pereche ordonată (i,j). Această
pereche ordonată înseamnă că regina din rândul i este plasară în coloana j.

Fig.6.2. O parte din arborele spaţiului de stare


pentru problema n-Reginelor în cazul n=4

Pentru a determina soluţiile, verificăm fiecare soluţie candidat (fiecare cale de la


rădăcină la o frunză) secvenţial, începând de la calea cea mai din stânga. Primele câteva căi
sunt după cum urmează:
[< 1, 1 >, < 2, 1 >, < 3, 1 >, < 4, 1 >]
[< 1, 1 >, < 2, 1 >, < 3, 1 >, < 4, 2 >]
[< 1, 1 >, < 2, 1 >, < 3, 1 >, < 4, 3 >]

2
[< 1, 1 >, < 2, 1 >, < 3, 1 >, < 4, 4 >]
[< 1, 1 >, < 2, 1 >, < 3, 2 >, < 4, 1 >]
Se observă că nodurile sunt vizitate conform unei căutări Depth-First-Search în care fii
unui nod sunt vizitaţi de la stânga la dreapta. O căutare Depth-First-Search simplă într-un
arbore al spaţiului de stare este asemenea urmării fiecărei căi dintr-un labirint până la
atingerea unui capăt de drum. Nu profită de nici un semn aflat pe drum. Putem face căutarea
mai eficientă prin căutarea acestor semne. De exemplu, aşa cum se vede în fig.6.3.a, nu pot fi
puse două regine în aceeaşi coloană. Aşadar, nu are sens construirea şi verificarea căilor din
ramura care porneşte din nodul care conţine perechea < 2, 1 > în fig.6.2 (deoarece am pus deja
regina 1 în coloana 1, nu mai putem pune şi regina 2 în aceeaşi coloană). Acest semn ne spune
că acest nod nu poate conduce decât la capete de drum. Similar, aşa cum se vede în fig.6.3.b,
nu pot exista două regine pe aceeaşi diagonală. Aşadar, nu are sens construirea şi verificarea
ramurii care porneşte din nodul care conţine perechea < 2, 2 > în fig.6.2.

Fig.6.3. Diagrama care arată faptul că dacă prima regină este plasată în coloana 1, a
doua regină nu poate fi plasată în coloana 1 (a) sau în coloana 2(b)

PROBLEME PROPUSE

3
4

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