Sunteți pe pagina 1din 60

Ll.

7,8_Sda2020 Tema “Analiza algoritmilor ale TEHNICII PROGRAMĂRII”

Sarcina şi obiectivele:

- 1. De studiat şi însuşit materialul teoretic pentru evidenţierea esenţialului tehnicilor de programare în elaborarea
modelelor soluţiei problemelor: esenţa metodelor (strategiilor tehnicilor de programare) şi specificul realizării;
- 2.Să se analizeze şi să se descrie tehnica modelării şi scenariile programării eficiente pentru diverse
compartimente (1.3) ale diferitor situaţii cu argumentări şi modele de structuri abstracte;
- 3. Pentru studiu detailat fiecare student alege cate 2 probleme din 1.3 dupa numarul de ordine in subgrupa (de
exemplu primul alege din 1.3.1. si prima litera a numelui de familie) să se ia “exemplul-program pentru
însuşire, analiza, modificari şi rulare în limbajul C”.
- 4.In partea a doua să se elaboreze scenariile succinte de implementare a ceasului de timp real al sistemului (4.)
pentru a estima performanţele algoritmului (#include <time.h>), incluzând subprograme şi fişiere cu teste de
verificare şi vizualizări şi explicaţii la principalele subprograme prin schemele logice.
- Faceţi o concluzie concisa asupra eficienţei.

Consideraţii teoretice:

1. Importanţa implementării algoritmilor eficienţi

1.1. Noţiuni aprofundate de programare cu soluţie surprinzătoare

Structurarea procesului software este impusă de doua motive principale. Primul este necesitatea de
uniformizare structurală ierarhica a activitatilor faciliteaza aplicarea recursiva a aceluiasi procedeu pentru
fiecare activitate. Al doilea este nevoia de simplificare in majoritatea cazurilor, activitatea software si se
bazeaza pe o metoda, pe un ghid sistemic si realizatorului produsului software. Avind o metoda, activitatea
software este inerent structurata in subactivitati mai simple, interconectate in acelasi context.

Oriece activitate software (AS) are o evolutie discreta, orientata pe stari. Starea, in acest caz este strins
legata de situatia momentara a proiectului: obiectele existente, activitatile realizate, activitatile in
desfasurare.

O AS este inglobate intr-un mediu si isi ofera serviciile altor AS mai complexe. Activitatea apartine
unei ierarhii functionale si unei ierarhii de alocare de resurse si suport de servicii tehnici de elaborare
(strategie, reguli) + creativitate (cunoştinţe, intuitie) = soluţie eficientă.

Dezvoltarea unui program Indiferent de tpul de probleme elementare (de la simple, naive, algoritmi liniari,
cu ramificaţii, ciclici...), fie complexe poate fi văzută ca stabilirea unui şir de descrieri din ce în ce mai
precise şi din ce în ce mai apropiate de un program executabil şi de documentaţia sa. Trecerile de la o
descriere la alta sunt deseori numite rafinări succesive. Aceasta este o vedere simplificată, pentru ca nu ia în
considerare natura iterativă a procesului de dezvoltare.

Timoty Budd ( profesor la Oregon State University ) care dă următoarea definiţie: “Un bun
programator trebuie să fie înzestrat cu tehnică, experienţă, capacitate de abstractizare, logică, inteligenţă,
creativitate şi talent”.

Mai intai vom deduce următoarea consecinţă imediată - deosebit de importantă - ce rezultă din definiţia de
mai sus: cele şapte calităţi trebuie să fie prezente toate pentru a se obţine calificativul de bun programator.
Deci, prin lipsa sau prin prezenţa “atrofiată” a uneia , sau a mai multe din “ingredientele reţetei” de mai sus,
acest calificativ nu mai poate fi atins.

În rezolvarea sa cu ajutorul calculatorului orice problemă trece prin trei etape obligatorii: Analiza
problemei, Proiectarea algoritmului de soluţionare şi Implementarea algoritmului într-un program pe
calculator. În ultima etapă, sub acelaşi nume, au fost incluse în plus două subetape cunoscute sub numele de
Testarea şi Întreţinerea programului. Aceste subetape nu lipsesc din “ciclul de viaţă” a oricărui produs-
program ce “se respectă”

1.2.TEORIA REZOLVĂRII PROBLEMELOR

Tehnicile de rezolvare a problemelor sunt subiecte de interes în aproape orice domeniu de activitate, nu
numai în informatică.

Legătura dintre procesul de dezvoltare al unui algoritm şi cel de rezolvare a unei probleme (mai general) i-a
determinat pe informaticieni să caute metode mai bune de rezolvare a problemelor.

Ideal ar fi ca procesul de rezolvare a problemelor să poată fi redus la rândul lui la un algoritm, dar s-a
demonstrat că acest lucru nu este posibil.

Astfel capacitatea de a rezolva anumite probleme rămâne mai degrabă un talent care trebuie dezvoltat decât
o ştiinţă care poate fi învăţată.

Etapele de bază în ceea ce priveşte rezolvarea problemelor:

Faza 1 – Înţelegerea problemei;

Faza 2 – Conceperea unui plan de rezolvare;

Faza 3 – Punerea în practică a planului respectiv;

Faza 4 – Evaluarea soluţiei din punct de vedere al corectitudinii şi ca potenţial instrument pentru rezolvarea
altor probleme.

În contextul dezvoltării programelor acestea devin:

Faza 1 – Înţelegerea problemei;

Faza 2 – Conceperea unei metode de rezolvare a problemei printr-o procedură algoritmică;

Faza 3 – Formularea algoritmului şi reprezentarea lui ca program;

Faza 4 – Evaluarea programului din punct de vedere al corectitudinii şi ca potenţial instrument pentru
rezolvarea altor probleme

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. Este adevărat că ultima etapă în rezolvarea
unei probleme – implementarea – este decisivă şi doveditoare, 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ă ?

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 16 corectitudinii soluţiei, iar etapa
de proiectare este singura care poate oferi argumente precise în favoarea eficienţei soluţiei propuse
Dificultatea problemelor de programare a căror enunţuri urmează este considerată maximă de teoreticienii
informaticii (ele se numesc probleme NP-complete). Nu vă lăsaţi păcăliţi de faptul că le-aţi întîlnit în unele
culegeri de programare. Ele sînt depăşite în dificultate doar de problemele insolvabile algoritmic ! Dar în ce
constă dificultatea lor ?

Dintr-un anumit punct de vedere, teoria complexităţii este exact opusul teoriei algoritmilor, care probabil
partea cea mai dezvoltată a informaticii teoretice: dacă teoria algoritmilor ia o problemă şi oferă o soluţie a
problemei în limitele unor resurse, teoria complexităţii încearcă să arate cînd resursele sunt insuficiente
pentru a rezolva o anumită problemă. Teoria complexităţii oferă astfel demonstraţii că anumite lucruri nu
pot fi făcute, pe cînd teoria algoritmilor arată cum lucrurile pot fi făcute.

De exemplu, atunci cînd învăţăm un algoritm de sortare ca quicksort, demonstrăm că problema sortării a n
numere se poate rezolva în timp proporţional cu n log n. Ştim deci că sortarea se poate face în timp cel mult
n log n, sau mai puţin.

Pe de altă parte, teoria complexităţii ne arată că pentru a sorta n numere oarecare ne trebuie cel puţin timp n
log n, şi că este imposibil să sortăm mai repede, dacă nu avem informaţii suplimentare despre valorile de
sortat. Combinînd aceste două rezultate, deducem că problema sortării are complexitatea exact n log n,
pentru că:

1. Avem un algoritm de timp n log n;


2. Am demonstrat că nu se poate mai bine de atît.
Această stare de fapt este una extrem de fericită, şi din păcate foarte rară: pentru majoritatea problemelor pe
care le cunoaştem, există o distanţă mare între cea mai bună posibilitate de rezolvare pe care o cunoaştem şi
limita inferioară cea mai ridicată pe care o putem demonstra. Insa există o clasă de probleme (numite NP-
complete) pentru care nici una din tehnici nu produce soluţii eficiente. Situaţia este ca în figura 1.

Figura 1: Existenţa unui algoritm pentru a rezolva o problemă oferă o limită superioară pentru complexitatea
problemei: în regiunea albastră, din dreapta, putem rezolva problema. Teoria complexităţii găseşte limite inferioare:
dacă ne plasăm în regiunea roşie, cu certitudine problema este insolvabilă. Foarte adesea, între cele două regiuni există
o zonă (haşurată cu negru) despre care nu putem spune nimic. Din păcate, această zonă este adesea foarte mare ca
întindere.

Figura 1

Chiar pentru probleme aparent banale, complexitatea minimă este adesea necunoscută. De exemplu,
algoritmul cel mai bun cunoscut pentru a înmulţi două numere de n cifre are complexitatea n log log n, dar
limita inferioară oferită de teoria complexităţii este de n. Algoritmul învăţat în şcoala primară pentru
înmulţire are complexitatea n2. Nimeni nu ştie cît de jos poate fi împinsă această limită, dar teoria
complexităţii ne garantează ca nu mai jos de n (de fapt în cazul de faţă nu e nevoie de nici o teorie pentru a
ne spune asta: în timp mai puţin de n nu poţi nici măcar să citeşti toate cifrele numerelor de înmulţit).

Pentru foarte multe probleme importante, distanţa între limitele inferioară şi superioară cunoscute este
enormă: de exemplu pentru problema satisfiabilităţii, limita superioară este o funcţie exponenţială (2 n) iar
cea inferioară este una polinomială (nk), pentru un k constant, independent de problemă.
Într-un anume sens, teoria complexităţii are o sarcină mult mai grea decît cea a algoritmilor: teoria
algoritmilor demonstrează propoziţii de genul: ``există un algoritm de complexitate n log n care înmulţeşte
două numere''. Acest lucru este de obicei făcut chiar construind acel algoritm. Pe de altă parte, teoria
complexităţii trebuie să demonstreze teoreme de genul ``Nu există nici un algoritm care rezolvă această
problemă în mai puţin de n log n paşi''. Ori aşa ceva este nu se poate demonstra în mod direct: există un
număr infinit de algoritmi, deci nu putem pur şi simplu să-i verificăm pe toţi.

1.2.1. Modele de calcul şi resurse Am tot menţionat tot felul de limite (ex. n 2), dar nu am spus ce
înseamnă acestea. Atît teoria complexităţii, cît şi teoria algoritmilor, măsoară complexitatea în acelaşi fel, şi
anume asimptotic. Asta înseamnă că măsurăm numărul de operaţii făcute ca o funcţie de cantitatea de date
care ne este oferită spre prelucrare, şi că împingem această cantitate spre infinit. Atunci complexitatea este
exprimată ca o funcţie pe mulţimea numerelor naturale: pentru fiecare mărime de intrare, avem o
complexitate. Dacă putem avea mai multe date de intrare cu aceeaşi mărime (de exemplu, cînd sortăm
putem avea mai mulţi vectori de lungime n), atunci complexitatea pentru intrarea n este luată ca fiind
maximumul dintre toate valorile: max(|i|=n) f(i), unde f este complexitatea şi i sunt datele de intrare (simbolul |
| reprezintă mărimea datelor).

Dacă am doi algoritmi pentru aceeaşi problemă, atunci poate pentru anumite instanţe ale problemei unul este
mai rapid, iar pentru alte instanţe celălalt. Dintre algoritmii de sortare sortarea prin selecţie este preferată
pentru vectori mici, iar quicksort sau heapsort pentru vectori mari. Dacă valorile din vector sunt mici, atunci
le bate pe amândouă radixsort. Şi atunci, cum comparăm doi algoritmi?

Există un răspuns relativ unanim acceptat la această întrebare, dar, înainte de a-l prezenta, trebuie încă odată
să spunem că acesta este doar un punct de vedere în comparaţie, şi că în practică se pot prefera algoritmii şi
din alte motive. Cel mai interesant atribut al performanţei a fost judecat a fi timpul de execuţie al unui
algoritm. Timpul este apoi asimilat cu numărul de operaţii elementare pe care le efectuează un algoritm
pentru a rezolva o problemă.

Notaţii asimptotice. Fie N mulţimea numerelor naturale (pozitive său zero) şi R mulţimea
numerelor reale. Notam prin N+ şi R+ mulţimea numerelor naturale, respectiv reale, strict pozitive, şi prin R
mulţimea numerelor reale nenegative. Mulţimea {true, false} de constante booleene o notăm cu B. Fie f : N
 R o funcţie arbitrară. Definim mulţimea O( f ) = {t : N  R | (c  R+) (n0  N) (n  n0) [t(n)  cf
(n)]}

Cu alte cuvinte, O( f ) (se citeşte “ordinul lui f ”) este mulţimea tuturor funcţiilor t mărginite superior de un
multiplu real pozitiv al lui f, pentru valori suficient de mari ale argumentului. Vom conveni să spunem că t
este în ordinul lui f (sau, echivalent, t este în O( f ), sau t  O( f )) chiar şi atunci când valoarea f (n) este
negativă sau nedefinită pentru anumite valori n < n0. În mod similar, vom vorbi despre ordinul lui f chiar şi
atunci când valoarea t(n) este negativă sau nedefinită pentru un număr finit de valori ale lui n; în acest caz,
vom alege n0 suficient de mare, astfel încât, pentru n  n0, acest lucru să nu mai apară. De exemplu, vom
vorbi despre ordinul lui n/log n, chiar dacă pentru n = 0 şi n = 1 funcţia nu este definită. În loc de t  O( f ),
uneori este mai convenabil să folosim notaţia t(n)  O( f (n)), subînţelegând aici că t(n) şi f (n) sunt funcţii.

Operaţii asupra funcţiilor asimptotice. Dacă T1(n) şi T2(n) sunt timpii de execuţie a două secvenţe de
program P1 şi P2, T1(n) fiind O(f(n)), iar T2(n) fiind O(g(n)), atunci timpul de execuţie T1(n)+T2(n), al
secvenţei P1 urmată de P2, va fi O(max(f(n),g(n))). Dacă T1(n) este O(f(n)) şi T2(n) este O(g(n)), atunci
T1(n)*T2(n) este O(f(n)*g(n)).

Etapele analizei complexităţii. În analiza complexităţii unui algoritm se parcurg următoarele etape:

1. Se stabileşte dimensiunea problemei.


2. Se identifică operaţia de bază.
3. Se verifică dacă numărul de execuţii ale operaţiei de bază depinde doar de dimensiunea problemei.
Dacă da, se determină acest număr. Dacă nu, se analizează cazul cel mai favorabil, cazul cel mai defavorabil
şi (dacă este posibil) cazul mediu.
4. Se stabileşte clasa de complexitate căruia îi aparţine algoritmul.
Câteva reguli generale pentru evaluarea timpului de execuţie în funcţie de mărimea n a datelor de
intrare, sunt:

1. Timpul de execuţie a unei instrucţiuni de asignare, citire sau scriere, este constant şi se notează cu
O(1).
2. Timpul de rulare a unei secvenţe de instrucţiuni e determinat de regula de însumare, fiind
proporţional cu cel mai lung timp din cei ai instrucţiunilor secvenţei.
3. Timpul de execuţie a unei instrucţiuni if - then - else este suma dintre timpul de evaluare a condiţiei
(O(1)) şi cel mai mare dintre timpii de execuţie ai instrucţiunilor pentru condiţia adevărată sau falsă.
4. Timpul de execuţie a unei instrucţiuni de ciclare este suma, pentru toate iteraţiile, dintre timpul de
execuţie a corpului instrucţiunii şi cel de evaluare a condiţiei de terminare (O(1)).
5. Pentru evaluarea timpului de execuţie a unei proceduri recursive, se asociază fiecărei proceduri
recursive un timp necunoscut T(n), unde n măsoară argumentele procedurii; se poate obţine o relaţie
recurentă pentru T(n), adică o ecuaţie pentru T(n), în termeni T(k), pentru diferite valori ale lui k.
6. Timpul de execuţie poate fi analizat chiar pentru programele scrise în pseudocod; pentru secvenţele
care cuprind operaţii asupra unor structuri de date, se pot alege câteva implementări şi astfel se poate face
comparaţie între performanţele implementărilor, în contextul aplicaţiei respective.
Analiza empirică a complexităţii algoritmilor. O alternativă la analiza matematică a complexităţii
o reprezintă analiza empirică. Aceasta poate fi utilă pentru:

a obţine informaţii preliminare privind clasa de complexitate a unui algoritm;


pentru a compara eficienţa a doi (sau mai mulţi) algoritmi destinaţi rezolvării aceleiaşi probleme;
pentru a compara eficienţa mai multor implementări ale aceluiaşi algo ritm;
pentru a obţine informaţii privind eficienţa implementării unui algoritm pe un anumit calculator.
Necesitatile algoritmilor eficienţi

 Se poate constata cu uşurinţă că în ultimii ani viteza de calcul a microprocesoarelor devine din ce în ce mai
mare.
 Se pune problema dacă este chiar necesară găsirea de algoritmi din ce în ce mai eficienţi pentru rezolvarea
problemelor, sau se poate aştepta mai bine să apară o nouă generaţie de calculatoare.
 Să presupunem că lucrăm cu un calculator care este capabil să efectueze un milion de operaţii elementare pe
secundă.
 În tabelul următor sunt indicaţi timpii necesari efectuării a n 2, n3, 2n, 3n operaţii elementare cu ajutorul unui
astfel de calculator, pentru diferite valori ale lui n.
o n=20 n=30 n=40 n=50 n=60

n2 0,0004 0,0009 0,0016 0,0025 0,0036


secunde secunde secunde secunde secunde

n3 0,001 0,008 0,027 0,125 0,216


secunde secunde secunde secunde secunde

2n 1 17,9min. 12,7zile 35,7ani 366


secundă secole
3n 58m in. 6,5ani 3855 2x108 1,3-1013
secole secole secole

 Soluţia reală nu poate fi alta decât găsirea unui algoritm eficient.


 Principala cauză pentru care unii algoritmi pot să ajungă la timpi de lucru practic infiniţi, chiar
pentru valori relativ mici ale lui n, este nu lipsa de performanţe a calculatorului, ci faptul că funcţia
exponenţială f(n) = an, cu a > 1 creşte foarte repede.
 Conform acestor afirmaţii, este indicată ca pentru o problemă dată să elaborăm algoritmi care să nu
fie exponenţiali.
 Sunt consideraţi “buni” acei algoritmi pentru care numărul operaţiilor este polinomial (adică se
poate exprima sub forma unui polinom în n, unde n este numărul datelor de intrare).

Tipuri de ordine de complexitate

Dacă timpul estimat este sub forma O(n), spunem că algoritmul este în timp liniar.

Dacă timpul estimat este de ordinul O(n k) spunem că algoritmul este în timp polinomial (n = 2-
patratric, n = 3 – cubic, …).

Dacă timpul estimat este sub forma O(2n), O(3n) şi aşa mai departe, spunem că algoritmul este în timp
exponenţial.

Un algoritm de ordinul o(n!) este asimilat unui algoritm în timp exponenţial, deoarece n! =
123…..n > 222 …2 = 2n-1.

În practică nu sunt admişi (nu se folosesc) decât algoritmii în timp polinomial, însă gradul
polinomului nu este chiar indiferent.

În concluzie, ori de câte ori se rezolvă o problemă, este bine să se găsească un algoritm de timp
polinomial. Mai mult, se caută un algoritm care să rezolve problema în timp polinomial de grad minim.

Spaţială: Θ(n2 ) Pentru memorarea soluţiilor subproblemelor


Temporală: O(n3 )
 Ns: Număr total de subprobleme: O(n2 )
 Na: Număr total de alegeri la fiecare pas: O(n)
 Complexitatea este de obicei egala cu Ns x Na

1.3. Metode şi strategii de proiectare a algoritmilor (alias tehnici de programare)

Ultima etapă în rezolvarea unei probleme – implementarea – este într-adevăr decisivă şi doveditoare, dar
primele două etape au o importanţă capitală. Ele sînt singurele ce pot oferi răspunsuri 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ă ?

Există printre ele chiar unele probleme extrem de dificile pentru care s-a demonstrat riguros că nu admit
soluţie cu ajutorul calculatorului.

Ideea centrală a etapei a doua – proiectarea uni 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 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.

Exemplu concret: există o clasă întreagă de probleme ce cer implicit să se genereze toate obiectele unei
mulţimi (cum ar fi problema generării tuturor permutărilor unei mulţimi cu n elemente). În acest caz este cunoscută
dinainte proprietatea ce trebuie să o îndeplinească fiecare soluţie ca să fie un obiect al spaţiului de căutare a soluţiilor.
Efortul de soluţionare va fi redus atunci la aflarea, căutarea sau generarea pe baza proprietăţii respective a tuturor
obiectelor posibile, fără însă a lăsa vreunul pe dinafară.
1.3.1.BackTracking. Pentru a preciza mai exact în ce constă această metodă, vom relua pe un exemplu
concret cele spuse deja. Avem următoarea problemă: se cere generarea tuturor permutărilor unei mulţimi de n elemente
ce nu conţin elementul x (dat dinainte) pe primele două poziţii. Conform celor afirmate, este suficient să “construim”
modelul abstract - graful - (mai precis arborele) tuturor permutărilor celor n elemente. Apoi, printr-o parcurgere
exhaustivă a nodurilor sale, prin una din metodele BFS sau DFS, să păstrăm numai acele noduri ce verifică în momentul
“vizitării” condiţia impusă (lipsa lui x de pe primele două poziţii). Observăm că această metodă necesită folosirea în
scopul memorării dinamice a drumului parcurs (în timpul căutării soluţiei) a mecanismului de stivă, fapt sugerat chiar de
numele ei: tracking, 40 adică înregistrarea pistei parcurse. Acest mecanism de stivă, care permite atît memorarea pistei
cît şi revenirea – back-tracking-ul, este unul din mecanismele de bază ce este folosit pe scară largă în procedurile de
gestiune dinamică a datelor în memorie. În plus, există unele cazuri particulare de probleme în care soluţia căutată se
obţine în final prin “vărsarea” întregului conţinut al stivei şi nu doar prin “nodul” ultim vizitat, aflat în vîrful stivei.
Exemplul cel mai potrivit de problemă ce necesită o strategie de rezolvare backtracking este Problema Labirintului: se
cere să se indice, pentru o configuraţie labirintică dată, traseul ce conduce către ieşirea din labirint. Iată un exemplu
sugestiv: 9 8 7 6 10 1  5 11 2 3 4 12 13 14 15 Observaţi cum, după 15 paşi, este necesară o revenire (backtracking)
pînă la căsuţa 6, de unde se continuă pe o altă pistă. “Pista falsă” a fost memorată în stivă, element cu element, iar
revenirea se va realiza prin eliminarea din stivă tot element cu element. Cînd în vîrful stivei reapare căsuţa cu numărul 6,
stiva începe din nou să crească memorînd elementele noului drum. În final stiva conţine în întregime soluţia: drumul
corect către ieşirea din labirint. 6 7 1  5 8 2 3 4 9 1 0 În consecinţă, indiferent de forma particulară ce o poate lua sau de
modul în care este “citită” în final soluţia, esenţialul constă în faptul că backtracking-ul este o metodă de programare ce
conţine obligatoriu gestiune de stivă. Lipsa instrucţiunilor, explicite sau “transparente”, de gestionare a stivei într-un
program (de exemplu, lipsa apelului recursiv), este un indiciu sigur de recunoaştere a faptului că acel algoritm nu
foloseşte metoda sau strategia de rezolvare BackTracking. Tot o metodă back-tracking este şi metoda de programare
cunoscută sub numele programare recursivă. Ea este mai utilizată decît metoda clasică BackTracking, fiind mai
economicoasă din punctul de vedere al minimizării efortului de programare. Această metodă se reduce la construirea, în
mod transparent pentru programator, a arborelui apelurilor recursive, traversarea acestuia prin apelarea recursivă
(repetată) şi efectuarea acţiunilor corespunzătoare în momentul “vizitării” fiecărui nod al arborelui. Apelarea recursivă
constituie “motorul vehiculului” de traversare şi are doar rolul de a permite traversarea arborelui. Gestionarea stivei
apelurilor recursive şi revenirea - back-tracking-ul rămîne în sarcina mediului de programare folosit şi se efectuează într-
un mod mascat pentru programator. Din acest punct de vedere, programatorului îi revine sarcina scrierii corecte a
instrucţiunii de apel recursiv şi a instrucţiunii ce “scurt-circuitează” bucla infinită a apelurilor recursive. Singurele
instrucţiuni care “fac treabă”, în sensul rezolvării propriuzise a problemei respective, sînt cele cuprinse în corpul
procedurii.

BackTracking. Pentru a preciza mai exact în ce constă această metodă, vom relua pe un exemplu concret
cele spuse deja. Avem următoarea problemă: se cere generarea tuturor permutărilor unei mulţimi de n elemente ce nu
conţin elementul x (dat dinainte) pe primele două poziţii. Conform celor afirmate, este suficient să “construim” modelul
abstract - graful - (mai precis arborele) tuturor permutărilor celor n elemente. Apoi, printr-o parcurgere exhaustivă a
nodurilor sale, prin una din metodele BFS sau DFS, să păstrăm numai acele noduri ce verifică în momentul “vizitării”
condiţia impusă (lipsa lui x de pe primele două poziţii).

Observăm că această metodă necesită folosirea în scopul memorării dinamice a drumului parcurs (în timpul
căutării soluţiei) a mecanismului de stivă, fapt sugerat chiar de numele ei: tracking, 40 adică înregistrarea pistei parcurse.
Acest mecanism de stivă, care permite atît memorarea pistei cît şi revenirea – back-tracking-ul, este unul din
mecanismele de bază ce este folosit pe scară largă în procedurile de gestiune dinamică a datelor în memorie. În plus,
există unele cazuri particulare de probleme în care soluţia căutată se obţine în final prin “vărsarea” întregului conţinut al
stivei şi nu doar prin “nodul” ultim vizitat, aflat în vîrful stivei.

Tehnica Backtracking propune generarea soluţiei prin completarea vectorului x în ordine x1x2... xn şi are la
bazǎ un principiu “de bun simţ”: dacǎ se constatǎ cǎ având o combinaţie parţialǎ de formǎ v1v2...v k-1 (unde vi,…,vk-1
sunt valori deja fixate), dacǎ alegem pentru xk o valoare vk şi combinaţia rezultatǎ nu ne permite sǎ ajungem la o
soluţie, se renunţǎ la aceastǎ valoare şi se încearcǎ o alta (dintre cele netestate în aceastǎ etapǎ). Într-adevǎr, oricum
am alega celelalte valori, dacǎ una nu corespunde nu putem avea o soluţie.

Exemplul cel mai potrivit de problemă ce necesită o strategie de rezolvare backtracking este Problema
Labirintului: se cere să se indice, pentru o configuraţie labirintică dată, traseul ce conduce către ieşirea din labirint. Iată
un exemplu sugestiv: 9 8 7 6 10 1  5 11 2 3 4 12 13 14 15

Altgoritmul general al metodei Backtracking

Pentru evitarea generǎrii combinaţiilor neconvenabile se procedeazǎ astfel:


Presupunem cǎ s-au gǎsit valorile v1v2…vk-1 pentru componentele x1x2... xk-1 (au rǎmas de determinat
valorile pentru xk…xn). ne ocupǎm în continuare de componenta xk. Paşii urmaţi sunt:
1. Pentru început, pentru xk nu s-a testat încǎ nici o valoare.
2. Se verificǎ dacǎ existǎ valori netestate pentru xk .
a) În caz afirmativ, se trece la pasul 3.
b) Altfel, se revine la componenta anterioarǎ, xk-1; se reia pasul 2 pentru k=k-1.
3. Se alege prima valoare v dintre cele netestate încǎ pentru xk.
4. Se verificǎ dacǎ acestǎ combinaţie parţialǎ v1v2…vk-1v ne poate conduce la un rezultat (dacǎ
sunt îndeplinite anumite condiţii de continuare).
a) Dacǎ valoarea aleasǎ este bunǎ se trece la pasul 5.
b) Altfel, se rǎmâne pe aceeaşi poziţie k şi se reia cazul 2.
5. Se verificǎ dacǎ s-a obţinut o soluţie .
a) În caz afirmativ, se tipǎreşte aceastǎ soluţie şi se rǎmâne la aceeaşi componentǎ xk,
reluându-se pasul 2.
b) Altfel se reia altgoritmul pentru urmǎtoarea componentǎ (se trece la pasul 1 pentru
k=k+1).
Înainte de a scrie programul care ne va obţine soluţiile, trebuie sǎ stabilim unele detalii cu privire la:

- vectorul soluţie – câte componente are, ce menţine fiecare componentǎ.


- mulţimea de valori posibile pentru fiecare componentǎ (sunt foarte importante limitele
acestei mulţimi).
- condiţiile de continuare (condiţiile ca o valoare x[k]sǎ fie acceptatǎ).
- condiţia ca ansamblul de valori generat sǎ fie soluţie.
Observaţi cum, după 15 paşi, este necesară o revenire (backtracking) pînă la căsuţa 6, de unde se continuă
pe o altă pistă. “Pista falsă” a fost memorată în stivă, element cu element, iar revenirea se va realiza prin eliminarea din
stivă tot element cu element. Cînd în vîrful stivei reapare căsuţa cu numărul 6, stiva începe din nou să crească memorînd
elementele noului drum. În final stiva conţine în întregime soluţia: drumul corect către ieşirea din labirint. 6 7 1  5 8 2 3
4 9 1 0 În consecinţă, indiferent de forma particulară ce o poate lua sau de modul în care este “citită” în final soluţia,
esenţialul constă în faptul că backtracking-ul este o metodă de programare ce conţine obligatoriu gestiune de stivă. Lipsa
instrucţiunilor, explicite sau “transparente”, de gestionare a stivei într-un program (de exemplu, lipsa apelului recursiv),
este un indiciu sigur de recunoaştere a faptului că acel algoritm nu foloseşte metoda sau strategia de rezolvare
BackTracking.

Tot o metodă back-tracking este şi metoda de programare cunoscută sub numele programare recursivă. Ea
este mai utilizată decît metoda clasică BackTracking, fiind mai economicoasă din punctul de vedere al minimizării
efortului de programare. Această metodă se reduce la construirea, în mod transparent pentru programator, a arborelui
apelurilor recursive, traversarea acestuia prin apelarea recursivă (repetată) şi efectuarea acţiunilor corespunzătoare în
momentul “vizitării” fiecărui nod al arborelui. Apelarea recursivă constituie “motorul vehiculului” de traversare şi are
doar rolul de a permite traversarea arborelui. Gestionarea stivei apelurilor recursive şi revenirea - back-tracking-ul
rămîne în sarcina mediului de programare folosit şi se efectuează într-un mod mascat pentru programator. Din acest
punct de vedere, programatorului îi revine sarcina scrierii corecte a instrucţiunii de apel recursiv şi a instrucţiunii ce
“scurt-circuitează” bucla infinită a apelurilor recursive. Singurele instrucţiuni care “fac treabă”, în sensul rezolvării
propriuzise a problemei respective, sînt cele cuprinse în corpul procedurii. De exemplu, iată cum arată în limbajul de
programare Pascal procedura generală de generare a permutărilor în varianta recursivă şi arborele de generare a
permutărilor mulţimii {1,2,3} (n=3), pe nivele:

Procedure Permut(k:byte;s:string); { k – nivelul în arbore, s - şirul}

Var i:byte;tmp:char;

Begin If k=n then begin { scurt-circuitarea recursivităţii} For i:=1 to n do Write(s[i]); { prin afişarea permutării }
Write(';'); { urmată de un punct-virgulă } end else For i:=k to n do begin { singurele instrucţiuni “ce fac treabă” }
tmp:=s[i];s[i]:=s[k];s[k]:=tmp; { sînt for-ul şi cele trei atribuiri } Permut(k+1,s); { apelul recursiv ce permite parcugerea } end;
{ arbor. de generare a celor n! permutări} End;

Observăm că arborele permutărilor este identic cu arborele apelurilor recursive şi că controlul şi gestiunea
stivei se face automat, transparent faţă de programator. Instrucţiunilor de control (din background) le revine sarcina de a
păstra şi de a memora, de la un apel recursiv la altul, string-ul s ce conţine permutările. Deşi această procedură recursiv
de generare a permutărilor pare o variantă de procedură simplă din punctul de vedere al programatorului, în realitate, ea
conţine într-un mod ascuns efortul de gestionare a stivei: încărcarea-descărcarea stringului s şi a întregului k. Acest efort
este preluat în întregime de instrucţiunile incluse automat de mediul de programare pentru realizarea recursivităţii.
Avantajul metodei back-tracking este faptul că efortul programatorului se reduce la doar trei sarcini: 1. “construirea”
grafului particular de căutare a soluţiilor 2. adaptarea corespunzătoare a uneia din metodele generale de traversare-
vizitare a grafului în situaţia respectivă (de exemplu, prin apel recursiv) 3. adăugarea instrucţiunilor “ce fac treabă” care,
fiind apelate în mod repetat în timpul vizitării nodurilor (grafului soluţiilor posibile), rezolvă gradat problema, găsind sau
construind soluţia. Acţiunea de revenire ce dă numele metodei, backtracking - revenire pe “pista lăsată”, este inclusă şi
este efectuată de subalgoritmul de traversare a grafului soluţiilor posibile. Acest subalgoritm are un caracter general şi
face parte din “zestrea” oricărui programator. În cazul particular în care graful soluţiilor este arbore, atunci se poate
aplica întotdeauna cu succes metoda programării recursive care conduce la un cod-program redus şi compact. Prezentăm
din nou procedura generală DepthFirstSearch (DFS) de traversare a unui graf în varianta recursivă (ce “construieşte” de
fapt arborele de traversare a grafului avînd ca rădăcină nodul de pornire) pentru a pune în evidenţă cele spuse.

Procedura DFS(v:nod); Vizitează v; { aici vor fi acele instrucţiuni “care fac treabă” } Marchează v; // v
devine un nod vizitat // { poate să lipsească în anumite implementări } Cît timp (există w nemarcat nod adiacent lui v)
execută DFS(w); { apelul recursiv este “motorul vehiculului” } { ce permite parcurgerea grafului şi gestiunea stivei de
revenire }

Există situaţii în care, la unele probleme, putem întîlni soluţii tip-backtracking fără însă a se putea sesiza la
prima vedere prezenţa grafului de căutare asociat şi acţiunea de traversare a acestuia, ci doar prezenţa stivei. O privire
mai atentă însă va conduce obligatoriu la descoperirea arborelui de căutare pe graful soluţiilor, chiar dacă el există doar
într-o formă mascată. Acest fapt este inevitabil şi constituie esenţa metodei – căutare (generare) cu revenire pe pista
lăsată.

Back-tracking-ul, metodă generală şi cu o largă aplicabilitate, fiind reductibilă în ultimă instanţă la


traversarea spaţiului -grafului de căutare- a soluţiilor, are marele avantaj că determină cu certitudine toate soluţiile
posibile, cu condiţia ca graful asociat de căutare a soluţiilor să fie corect. Dar 42 ea are marele dezavantaj că necesită un
timp de execuţie direct proporţional cu numărul nodurilor grafului de căutare asociat (sau numărul cazurilor posibile). În
cele mai multe cazuri acest număr este exponenţial (e n ) sau chiar mai mare, factorial (n!), unde n este dimensiunea
vectorului datelor de intrare. Acest fapt conduce la o durată de execuţie de mărime astronomică făcînd într-un astfel de
caz algoritmul complet inutilizabil, chiar dacă el este corect teoretic. (De exemplu, dacă soluţionarea problemei ar
necesita generarea tuturor celor 100! permutări (n=100), timpul de execuţie al algoritmului depăşeşte orice imaginaţie.)
În astfel de situaţii, în care dimensiunea spaţiului de căutaregenerare a soluţiilor are o astfel de dependenţă în funcţie de n
(fiind o funcţie de ordin mai mare decît funcţia polinomială), este absolut necesară îmbunătăţirea acestei metode sau
înlocuirea ei. Nu este însă necesară (şi de multe ori nici nu este posibilă!) abandonarea modelului abstract asociat - graful
soluţiilor posibile, cu calităţile şi proprietăţile sale certe - ci este necesară doar obţinerea unei durate de execuţie de un
ordin de mărime inferior printr-o altă strategie de parcurgere a spaţiului de căutare.
Probleme ce necesită back-tracking

Problema clasică de programare care necesită back-tracking (revenirea pe urma lăsată) este problema ieşirii
din labirint. - iată o soluţie simplă care iniţializează labirintul în mod static, ca o matrice de caractere

#include <stdio.h>

#include <stdlib.h>

#include <conio.h>

#define XMAX 6

#define YMAX 6

char a[XMAX+1][YMAX+1]={

"*******",

"* * *",

"* * * *",

"* * ****",

"** * *",

"* * ",

"*******" };

int x0=1,y0=2;

void print(void){ int i,j; for(i=0;i<=XMAX;i++) { for(j=0;j<=YMAX;j++) putchar(a[i][j]);


putchar('\n'); } getchar(); clrscr(); }

void escape(int x,int y) { if(x==XMAX || y==YMAX) { puts("Succes!"); exit(1);} a[x][y]='*';


print(); if(a[x][y+1]==' '){puts("la dreapta"); escape(x,y+1);}

if(a[x+1][y]==' '){puts("in jos "); escape(x+1,y);}

if(a[x][y-1]==' '){puts("la stinga "); escape(x,y-1);}

if(a[x-1][y]==' '){puts("in sus "); escape(x-1,y);} return; }

void main(void){ escape(x0,y0); puts("Traped!"); }

Probleme ce necesită back-tracking

Problema N reginilor in C. Pentru rezolvarea problemei am folosit algoritmul Backtracking .


Insusi problema consta in aranjarea N reginelor pe o tabla de sah cu marimea NxN , astfel incit ele sa nu sa
se bata una pe alta.

Descrierea algoritmului :

1) Incepem din coltul sting de sus al tablei (1,1).


2) Verificam daca aceasta zona este sigura , daca DA , plasam regina , daca NU , ne deplasam mai
departe.
3) Daca nu putem plasa regina pe oarecare rind K , ne intoarcem la rindul K-1 , replasam regina de pe
rindul K-1 pe urmatoarea casuta.
4) Repetam algoritmul pina cind nu ajungem la rindul K=N si ultima regina a fost plasata cu succes.

LISTINGUL PROGRAMULUI

#include <stdio.h>

#include <stdlib.h>

#define min(a,b) (a < b ? a : b) // gasirea minimului

int tabla[20][20]={0},n; //n - marimea

int plasare(int , int ); // functia pentru verificarea plasarii

void BackTrack(int); // algoritmul Bactrack

void afisam(); //afisarea tablei

int main()

{printf("Introdu marimea tablei de sah \r\n");

scanf("%d",&n); // cerem marimea tablei

BackTrack(1); // argumentul este 1 deoarece incepem de pe pozitia 1

void BackTrack(int rind)

{int i=0,j=0;

for(i=1;i<=n;i++)

if( plasare(rind,i) ) // daca este posibila plasarea se efectueaza instructiunele

{ tabla[rind][i]=1;
if(rind==n) afisam(); // daca am ajuns la rindul final – afisam

else BackTrack(rind+1); //in caz contrar ne deplasam cu un rind mai jos si recursiv
apelam functia

tabla[rind][i]=0; // daca nu este posibila plasarea pe rindul K+1 , mutam


regina de pe pozitie

void afisam()

{int i,j;

for(i=1;i<=n;i++,printf("\r\n"))

for(j=1;j<=n;j++)

printf("%d ",tabla[i][j]);

exit(0);

int plasare(int rind ,int col)

{int i,j,reset;

for(i=1;i<=n;i++) //parcurgere si verificare pe linii si coloane

if( tabla[rind][i]==1 || tabla[i][col]==1 ) return 0;

reset=min(rind,col)-1;

for(i=rind-reset , j=col-reset; i<n,j<n; i++,j++) // parcurgere si verificare pe diagonala nr 1

if(tabla[i][j]==1) return 0;

for(i=rind-reset,j=col+reset; i<n, j>0; i++, j--) //parcurgere si verificare pe diagonala nr 2

if(tabla[i][j]==1) return 0;

return 1;

SCREENSHOT :

CAZUL 4X4

Am luat cazul pentru marimea 4x4 . Dupa cite vedem , plasarea este corecta . Cifra 1 reprezinta
regina . CAZUL 8X8
Cazul 16x16

1.3.2. Divide et impera. Ideea (divide si cucerește) este atribuita lui Filip al IIlea, regele
Macedoniei (382-336 i.e.n.), tatăl lui Alexandru cel Mare si se refera la politica acestuia fata de statele
grecești. In CS – Divide et impera se refera la o clasa de algoritmi care au ca principale caracteristici faptul
ca împart problema in subprobleme similare cu problema inițiala dar mai mici ca dimensiune, rezolva
problemele recursiv si apoi combina soluțiile pentru a crea o soluție pentru problema originala.

Schema Divide et impera consta in 3 pași la fiecare nivel al recurentei: Divide problema data intr-
un număr de subprobleme Impera (cucerește) – subproblemele sunt rezolvate recursiv. Daca subproblemele
sunt suficient de mici ca date de intrare se rezolva direct (ieșirea din recurenta) Combina – soluțiile
subproblemelor sunt combinate pentru a obține soluția problemei inițiale

Avantaje: Produce algoritmi eficienti Descompunerea problemei in subprobleme faciliteaza


paralelizarea algoritmului in vederea executiei sale pe mai multe procesoare.

Dezavantaje: Se adauga un overhead datorat recursivitatii (retinerea pe stiva a apelurilor


functiilor).

Algoritmul Merge Sort este un exemplu clasic de rezolvare cu D&I Divide: Divide cele n elemente
ce trebuie sortate in 2 secvențe de lungime n/2 Impera: Sortează secvențele recursiv folosind merge sort
Combina: Secvențele sortate sunt asamblate pentru a obține vectorul sortat Recurența se oprește când
secvența ce trebuie sortata are lungimea 1 (un vector cu un singur element este întotdeauna sortat ☺ )
Operația cheie este asamblarea soluțiilor parțiale.

Algoritm [Cormen] MERGE-SORT(A, p, r) 1 if p < r

2 then q ← [(p + r)/2] // divide

3 MERGE-SORT(A, p, q) //impera

4 MERGE-SORT(A, q + 1, r)

5 MERGE(A, p, q, r) // combina (interclasare)

Algoritm [Cormen]

MERGE(A, p, q, r) // p si r sunt capetele intervalului, q este “mijlocul”

1 n1 ← q - p + 1 // numarul de elemente din partea stanga

2 n2 ← r – q // numarul de elemente din partea dreapta


3 create arrays L[1 -> n1 + 1] and R[1 -> n2 + 1]

4 for i ← 1 to n1

5 do L[i] ← A[p + i - 1] // se copiaza partea stanga in L

6 for j ← 1 to n2

7 do R[j] ← A[q + j] // si partea dreapta in R

8 L[n1 + 1] ← ∞

9 R[n2 + 1] ← ∞

10 i ← 1

11 j ← 1

12 for k ← p to r // se copiaza inapoi in vectorul de

13 do if L[i] ≤ R[j] // sortat elementul mai mic din cei

14 then A[k] ← L[i] // doi vectori sortati deja

15 i ← i + 1

16 else A[k] ← R[j]

17 j ← j + 1

Complexitatea:

dimensiunea subproblemelor

T(n) = 2 * T(n/2) + Θ(n)

/ \ complexitatea interclasarii

/ numar de subprobleme

Divide et impera – alte exemple:

Calculul puterii unui numar: x^ n Algoritm “clasic” pentru i = 1> n rez = rez * x; return rez Complexitate:
Θ(n)

Algoritm divide et impera daca n este par return x^n/2 * x^n/2

altfel (n este impar) return x * x^(n-1)/2 * x^(n-1)/2

Complexitate: T(n) =T(n/2)+Θ(1)=>T(n)=Θ(logn)

2. Calculul celei mai scurte distante intre 2 puncte din plan


(http://www.cs.ucsb.edu/~suri/cs235/ClosestPair.pdf) algoritmul naiv – Θ(n2 )

3. sortează punctele in ordinea crescătoare a coordonatei x (Θ(nlog n))


4. împărțim setul de puncte in 2 seturi de dimensiune egala si calculam recursiv distanta minima in fiecare
set (l = linia ce împarte cele 2 seturi, d = distanta minima calculata in cele 2 seturi)

5. elimina punctele care sunt plasate la distanta de l > d

6. sortează punctele ramase după coordonata y

7. calculează distantele de la fiecare punct rămas la cei 5 vecini (nu pot fi mai multi)

8. daca găsește o distanta < d, atunci actualizează d

Merge Sort foloseste algoritmul DIVIDE ET IMPERA . Asta permite sa efectuam sortarea mai rapid . Viteza
de opeartii este constanta O(n log n) . In comparatie cu metoda bulelor care este O(n^2) , observam ca Merge
sort este in avantaj.

REPREZENTARE :

1)Se divide tabloul in jumate , iar tabloul nou creat

inca in jumate , pina nu ajungem la etapa cind nu mai


putem divide tabloul .

2)Se sorteaza sub tablourile prin comparare

3) Se repeta mersul pentru a doua jumate a

tabloului .

Rezultatele finale :
LISTINGUL PROGRAMULUI

#include <stdio.h>

void Merge(int*,int,int*,int,int*);

void Merge_sort(int*,int);

int i,j=0;

int main(){ int n,A[40]; // tabloul initial

printf("Introdu marimea tabloului:");

scanf("%d",&n);

printf("Introdu elementele:");

for(i=0;i<n;i++) //luam elementele

scanf("%d",&A[i]);

printf("\r\n Tabloul nesortat : "); //afisarea tabloului sortat

for(i=0;i<n;i++)

printf("%d ",A[i]);

Merge_sort(A,n); // apelam functia Merge_sort , ii transmitem ca argument marimea si inceputul tabloului

printf("Tabloul sortat: ");

for(i=0;i<n;i++)

printf("%d ",A[i]);

void Merge_sort(int A[],int n) {

int mid;

mid=n/2; // gasirea mijlocului

int left[mid] , right[n-mid]; // crearea a 2 subtablouri

if(n<2) return; // daca nu mai putem diviza tabloul – ne intoarcem

for(i=0;i<mid;i++) left[i]=A[i]; // copiem elementele in tabloul sting

for(i=0;i<n;i++) right[i-mid]=A[i]; //copiem elementele in tabloul drept

Merge_sort(left,mid); // recursiv apelam functia Merge_Sort , divizind partea stinga a tabloului

Merge_sort(right,n-mid); // recursiv apelam functia Merge_Sort , divizind partea dreapta a tabloului

Merge(left,mid,right,n-mid,A); // Sortam prin comparare tablourile divizate

}
void Merge(int L[],int nl,int R[],int nr, int A[] ) { i=0;j=0; int k=0;

//sortam prin comparare in sub tablouri

while(i<nl && j<nr) {

if(L[i]<=R[j]) A[k]=L[i++];

else A[k]=R[j++];

k++;

//copiem elementele ramase

while (i<nl)

A[k++]=L[i++];

while(j<nr)

A[k++]=R[j++]; }

SCREENSHOT :

Problema turnurilor din Hanoi": Fie trei turnuri (tije) identificate în continuare cu a, b şi c. Turnul
a conţine n discuri de dimensiuni diferite aranjate în ordine crescătoare în raport cu dimensiunea lor (vezi
Figura 8.1). Problema constă în a transfera discurile de pe a pe c păstrând ordinea de aşezare a discurilor.
Discurile se pot transfera de pe un turn pe altul după următoarele reguli:
        Transferul constă din mai multe mutări de discuri. La o mutare se ia un singur disc de pe un turn şi se
mută pe altul.
        Nu poate fi plasat un disc de dimensiune mare pe unul de dimensiune mai mică.
        Pentru transferarea discurilor de pe un turn pe altul se poate folosi cel de-al treilea turn pentru manevră
(adică pentru a pune pe el temporar anumite discuri).

Figura 8.1 Turnurile din Hanoi (n = 4)


Să observăm că, pentru a transfera discurile de pe a (sursă) pe c (destinaţie), folosind b ca şi
manevră, putem proceda astfel:
1.      Dacă n = 1 atunci mutăm discul de pe a pe c.
2.      Dacă n = 2 atunci mutăm temporar un disc (cel de sus � mai mic) de pe a pe b, apoi mutăm discul
rămas (discul mai mare) de pe a pe c şi în final mutăm discul mic de pe b pe c.
3.      Dacă n = 3 atunci aplicăm metoda de la pasul 2 pentru a transfera temporar 2 discuri (pe cele din vârf)
de pe a pe b, apoi mutăm pe c discul rămas pe a, şi în final transferăm de pe b pe c discurile mutate
temporar.

Figura 8.2 Transferarea a patru discuri de pe a pe c


În caz general, pentru un număr oarecare de discuri, soluţia pentru transferarea a n discuri de pe
sursa - a  pe destinaţia - c folosind un suport de manevră - b este: (1) transferă (temporar) n-1 discuri de pe
a pe b folosind c ca suport de manevră, (2) mută discul rămas de pe a pe c şi (3) transferă cele n-1 discuri
(puse temporar pe b) de pe b pe c. Aceste operaţii sunt redate grafic în Figura 8.2.
                                                                                                //transfera n discuri de
pe a (sursa) pe
                                                                                                //c (destinatie) folosind b
(manevra)
 Subalgoritmul hanoi(n, a, b, c) este:  
    Dacă n = 1 atunci                                                           //daca pe sursa e un singur disc,
      muta(a, c)                                                                      //muta-l
    altfel                                                                                  //daca sunt mai multe discuri
de transferat,
      hanoi(n-1, a, c, b);                                                        //transfera temporar n-1 discuri pe
b,
      muta(a, c)                                                                       //muta-l pe cel ramas pe c, si
      hanoi(n-1, b, a, c)                                                         //transfera-le la locul lor si pe cele
sf-hanoi                                                                                 //puse tempoarar pe b

            Un alt exemplu se întâlneşte în rezolvarea problemei labirintului. Un labirint este modelat printr-o
matrice A a cărei valori sunt caracterele 'C' (de la culoar) sau 'Z' (de la zid). Dându-se o poziţie (i,j) în matrice
(labirint) se cere: (a) să se verifice dacă se poate ieşi din labirint, respectiv (b) să se găsească un drum de ieşire
din labirint.
            Pentru punctul (a), putem defini o funcţie Labirint(i,j) verifică dacă A[i,j+1] este 'C', în care caz
apelează Labirint(i,j+1), sau dacă LAB[i+1,j]='C', în care caz apelează Labirint(i+1,j), etc.  Dacă se găseşte un
drum se precizează aceasta prin indicatorul ind, care este ADEVARAT cât timp nu s-a găsit un drum şi FALS
când a fost găsit un drum.
            În cazul unui drum înfundat este necesar să se revină pe poziţii anterioare. Pentru a nu intra în cicluri
infinite, se marchează locurile prin care am trecut prin 'X'.
            Lăsăm în seama cititorului scrierea efectivă a subalgoritmului Labirint(u,v) (o soluţie este dată în
[Boian88]).
1.3.3. Greedy. Metodă de rezolvare eficientă a unor probleme de optimizare. Soluția trebuie să
satisfacă un criteriu de optim global (greu de verificat) optim local mai ușor de verificat. Se aleg soluții
parțiale ce sunt îmbunătățite repetat pe baza criteriului de optim local pană ce se obțin soluții finale.
Soluțiile parțiale ce nu pot fi îmbunătățite sunt abandonate proces de rezolvare irevocabil (fără reveniri).

În strategia backtracking căutarea soluţiei, adică vizitarea secvenţială a nodurilor grafului soluţiilor cu
revenire pe urma lăsată, se face oarecum “orbeşte” sau rigid, după o regulă simplă care să poată fi rapid
aplicată în momentul “părăsirii” unui nod vizitat. În cazul metodei (strategiei) greedy apare suplimentar
ideea de a efectua în acel moment o alegere. Dintre toate nodurile următoare posibile de a fi vizitate sau
dintre toţi paşii următori posibili, se alege acel nod sau pas care asigură un maximum de “cîştig”, de unde şi
numele metodei: greedy = lacom. Evident că în acest fel poate să scadă viteza de vizitare a nodurilor – adică
a soluţiilor ipotetice sau a soluţiilor parţiale – prin adăugarea duratei de execuţie a subalgoritmului de
alegere a următorului nod după fiecare vizitare a unui nod. Există însă numeroşi algoritmi de tip greedy
veritabili care nu conţin subalgoritmi de alegere. Asta nu înseamnă că au renunţat la alegerea greedy ci,
datorită “scurtăturii” descoperite în timpul etapei de analiză a problemei, acei algoritmi efectuează la fiecare
pas o alegere fără efort şi în mod optim a pasului (nodului) următor. Această alegere, dedusă în etapa de
analiză, conduce la maximum de “profit” pentru fiecare pas şi scurtează la maximum drumul către soluţia
căutată. Aparent această metodă de căutare a soluţiei este cea mai eficientă, din moment ce la fiecare pas se
trece dintr-un optim (parţial) într-altul. Totuşi, ea nu poate fi aplicată în general ci doar în cazul în care
există certitudinea alegerii optime la fiecare pas, certitudine rezultată în urma etapei anterioare de analiză a
problemei.

Schema generală de rezolvare a unei probleme folosind programarea lacoma:


Rezolvare_lacoma(Crit_optim, Problema)

1. sol_partiale = sol_initiale(problema); // determinarea soluțiilor parțiale

2. sol_fin = Φ;

3. while (sol_partiale ≠ Φ)

4.for-each(s in sol_partiale)

5. if(s este o solutie a problemei) { // daca e soluție

6. sol_fin = sol_fin U {s}; // finala se salvează

7. sol_partiale = sol_partiale \ {s};

8. } else // se poate optimiza?

9. if(optimizare_posibila(s, Crit_optim, Problema)) // da

10. sol_partiale = sol_partiale \ {s} U optimizare(s,Crit_optim,Problema)

11. else sol_partiale = sol_partiale \ {s}; // nu

12. return sol_fin

Alt exemplu
2. Problema rucsacului Trebuie să umplem un rucsac de capacitate maximă M kg cu obiecte care au
greutatea mi şi valoarea vi . Putem alege mai multe obiecte din fiecare tip cu scopul de a maximiza valoarea
obiectelor din rucsac.

Varianta 1: putem alege fracţiuni de obiect – “problema continuă”

Varianta 2: nu putem alege decât obiecte întregi (număr natural de obiecte din fiecare tip) – ”problema 0-1”

3. Varianta 1: Algoritm Greedy sortăm obiectele după raportul vi /mi

adăugăm fracţiuni din obiectul cu cea mai mare valoare per kg până epuizăm stocul şi apoi adăugăm
fracţiuni din obiectul cu valoarea următoare

Exemplu: M = 10; m1 = 5 kg, v1 = 10, m2 = 8 kg, v2 = 19, m3 = 4 kg, v3 = 4 Soluție: (m2, , v2 ) 8kg şi
2kg din (m1 ,v1 ) – valoarea totală: 19 + 2 * 10 / 5 = 23

4.Varianta 2: Algoritmul Greedy nu funcţionează => contraexemplu Exemplu: M = 10; m1 = 5 kg, v1 = 10,
m2 = 8 kg, v2 = 19, m3 = 4 kg, v3 = 4 Rezultat corect – 2 obiecte (m1 ,v1 ) – valoarea totală: 20 Rezultat
algoritm Greedy – 1 obiect (m2 ,v2 ) – valoarea totală: 19

Algoritmii Greedy funcţionează cand:

 problema are proprietatea de substructură optimă


 soluţia problemei conţine soluţiile subproblemelor
 problema are proprietatea alegerii locale
 alegând soluţia optimă local se ajunge la soluţia optimă global
 Fie E o mulţime finită nevidă şi I ⊂ F(E) a.i. ∅ ∈ I, ∀X ⊆ Y şi Y ∈ I => X ∈ I. Atunci spunem ca
(E,I) sistem accesibil.
 Submulţimile din I sunt numite submulţimi “independente”:
Exemple: Ex1: E = {e1 , e2 , e3 } si I = {∅, {e1 }, {e2 }, {e3 }, {e1 , e2 }, {e2 , e3 }} – mulțimile
ce nu conțin e1 si e3 .
Ex2: E – muchiile unui graf neorientat şi I mulţimea mulţimilor de muchii ce nu conţin un ciclu
(mulțimea arborilor).
Ex3: E set de vectori dintr-un spaţiu vectorial, I mulţimea mulţimilor de vectori linear independenţi.
Ex4: E – muchiile unui graf neorientat şi I mulţimea mulţimilor de muchii în care oricare 2 muchii
nu au un vârf comun.
 Un sistem accesibil este un matroid daca satisface proprietatea de interschimbare: X, Y ∈ I şi |X| < |
Y| => ∃e ∈ Y \ X a.i. X ∪ {e}∈I
 Teorema. Pentru orice subset accesibil (E,I) algoritmul Greedy rezolvă problema de optimizare dacă
şi numai dacă (E,I) este matroid.
 Algoritmul generic Greedy devine:
X=∅
sortează elementele din E în ordinea descrescătoare a ponderii
pentru fiecare element e ∈ E (sortat) repetă
X = X ∪ {e} dacă şi numai dacă (X ∪ {e}) ∈ I
Întoarce X

Dezavantajul este că, la majoritatea problemelor dificile, etapa de analiză nu poate oferi o astfel de
“pistă optimă“ către soluţie. Un alt dezavantaj al acestei strategii este că nu poate să conducă către toate
soluţiile posibile ci doar către soluţia optimă (din punct de vedere a alegerii efectuate în timpul căutării
soluţiei), dar poate oferi toate soluţiile optime echivalente.
LISTINGUL PROGRAMULUI in C

#include <stdio.h>

struct Obiect //structura pentru fiecare obiect, masa , valoarea coeficient

{ int masa;

int val;

float coef; // coeficient valoare / masa

}O[20],temp; // cream un tablou de obiecte , si o variabila temporara pentru sortare

int main(){ int nr,i,j; float V=0, M=0 , Mmax;

printf("\t\t\t GREEDY ALGORITHM\r\n");

printf("\t\t\t Problema rucsacului cu elemente separabile\r\n");

printf("\r\n Introdu capacitatea maxima a rucsacului : "); // initializam masa maxima a rucsacului

scanf("%f",&Mmax);

printf("\r\n Introdu numarul de obiecte: "); // introducem numarul de obiecte

scanf("%d",&nr);

for(i=0;i<nr;i++) // introducem datele pentru fiecare obeict

{ printf("\r\nIntrodu masa pentru obiectul nr %d : ",i+1);

scanf("%d",&O[i].masa);

printf("\r\nIntrodu valoarea obiectului nr %d :",i+1);

scanf("%d",&O[i].val);

O[i].coef=(float)O[i].val / O[i].masa;

for(j=nr;j>0;j--) // SORTARE DUPA COEFICIENTI

for(i=0;i<j;i++)

if(O[i].coef < O[i+1].coef)

{ temp=O[i];

O[i]=O[i+1];

O[i+1]=temp;

//GREEDY ALGORITHM

for(i=0;i<nr && M<Mmax;i++)


if(M+O[i].masa <= Mmax ) // daca masa nu intrece masa maxima , luam obiectul in intregime

{ V=(float)V+O[i].val;

M=(float)M+O[i].masa;

else{ // in caz contrar luam doar o parte

V=(float)V+O[i].coef*(Mmax-M);

M=(float)Mmax; }

printf("\r\nValoarea maxima ce poate fi luata : %f ",V);

printf("\r\nMasa finala : %f",M);}

SCREENSHOT :

Algoritmul GREEDY lucreaza foarte bine pentru obiecte ce pot fi


fractionate , impartite . Obtinem rezultatul dorit. Este o buna alegere pentru
optimizarea problemelor de acest tip.

1.3.4.Programarea dinamică. Este o metodă sau strategie ce îşi propune să elimine dezavantajele
metodei recursive care, în ultimă instanţă, am văzut că se reduce la parcurgerea în adîncime a arborelui
apelurilor recursive (adică backtracking). Această metodă se apropie ca idee strategică de metoda Greedy,
avînd însă unele particularităţi.

 Descriere generală
Soluții optime construite iterativ asamblând soluții optime ale unor probleme similare de
dimensiuni mai mici.
 Algoritmi “clasici”
Algoritmul Floyd-Warshall care determină drumurile de cost minim dintre toate perechile de noduri
ale unui graf.
AOC
Înmulţirea unui şir de matrici
Numere catalane Viterbi
Algoritm generic
Programare dinamică (crit_optim, problema){
// fie problema0 problema1 … probleman astfel incât
// probleman= problema; problemai mai simpla decât problemai+1
1. Sol = soluții_initiale(crit_optim, problema0 );
2. Pentru i = 1 la n repetă { // construcție soluții pentru problemai // folosind soluțiile problemelor
precedente
3. Soli = calcul_soluții(Sol, crit_optim, Problemai ); // determin soluția problemeii
4. Sol = Sol U Soli ; // noua soluție se adaugă pentru a fi refolosită pe viitor
5. }
6. s = soluție_pentru_probleman (Sol); // selecție / construcție soluție finala
7. Întoarce s;
8. }
Caracteristici:
 soluție optimă a unei probleme conține soluții optime ale subproblemelor.
 Decompozabilitatea recursivă a problemei P in subprobleme similare P = Pn , Pn-1, … P0 care
acceptă soluții din ce in ce mai simple.
 Suprapunerea problemelor (soluția unei probleme Pi participă in procesul de construcție a soluțiilor
mai multor probleme Pk de talie mai mare k > i) – memoizare (se foloseşte un tablou pentru
salvarea soluţiilor subproblemelor pentru a nu le recalcula)
 In general se folosește o abordare bottom-up, de la subprobleme la probleme.
Diferențe Greedy – programare dinamică
Programare lacomă
Sunt menținute doar soluțiile parțiale curente din care evoluează soluțiile parțiale următoare
Soluțiile parțiale anterioare sunt eliminate
Se poate obține o soluție neoptimă. (trebuie demonstrat că se poate aplica)
Programare dinamică
 Se păstrează toate soluțiile parțiale
 La construcția unei soluții noi poate contribui orice altă soluție parțială generată anterior
 Se obține soluția optimă dacă soluţiile parţiale sunt independente între ele.
Diferenţe divide et impera – programare dinamică
Divide et impera
abordare top-down – problema este descompusă în subprobleme care sunt rezolvate independent
xxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxx xxxxxxxxxxxxxxxxxxxxx
Programare dinamică:
 abordare bottom-up - se porneşte de la sub-soluţii elementare şi se combină sub-soluţiile
mai simple în sub-soluţii mai complicate, pe baza criteriului de optim
 se evită calculul repetat al aceleiaşi subprobleme prin memorarea rezultatelor intermediare
Exemplu: Parantezarea matricilor (Chain Matrix Multiplication)
 Se dă o şir de matrice: A1 , A2 , ..., An .
 Care este numărul minim de înmulţiri de scalari pentru a calcula produsul: A1 x A2 x ... x An ?
 Să se determine una dintre parantezările care minimizează numărul de înmulţiri de scalari

Înmulţirea matricilor

A(p, q) x B (q, r) => pqr înmulţiri de scalari.

Dar înmulţirea matricilor este asociativă (deşi nu este comutativă).

A(p, q) x B (q, r) x C(r, s)

(AB)C => pqr + prs înmulţiri

A(BC) => qrs + pqs înmulţiri

Ex: p = 5, q = 4, r = 6, s = 2

(AB)C => 180 înmulţiri

A(BC) => 88 înmulţiri

Concluzie: Parantezarea este foarte importantă!

Soluţia banală
Matrici: A1 , A2 , ..., An .

Vector de dimensiuni: p0 , p1 , p2 , ... , pn .

Ai (pi-1, pi ) A1 (p0 , p1 ), A2 (p1 , p2 ), …

Dacă folosim căutare exhaustivă şi vrem să construim toate parantezările posibile pentru a
determina minimul: Ω(4^n / n^3/2).

Vrem o soluţie polinomială folosind P.D

Descompunere în subprobleme

Încercăm să definim subprobleme identice cu problema originală, dar de dimensiune mai mică.

∀ 1 ≤ i ≤ j ≤ n:

Notăm Ai, j = Ai x … x Aj . Ai,j are pi-1 linii si pj coloane: Ai,j(pi-1, pj )

m[i, j] = numărul optim de înmulţiri pentru a rezolva subproblema Ai,j

s[i, j] = poziţia primei paranteze pentru subproblema Ai,j

Care e parantezarea optimă pentru Ai, j?

Problema iniţială: A1,n

Combinarea subproblemelor

Pentru a rezolva Ai,j

trebuie găsit acel indice i ≤ k < j care asigură parantezarea optimă:

Ai, j = (Ai x…x Ak ) x (Ak+1 x…x Aj )

Ai, j = Ai, k x Ak+1, j.

Alegerea optimal

Căutăm optimul dintre toate variantele posibile de alegere (i ≤ k < j)

Pentru aceasta, trebuie însă ca şi subproblemele folosite să aibă soluţie optimală (adică Ai, k şi Ak+1, j)

Substructura optimal

 Dacă ştim că alegerea optimală a soluţiei pentru problema Ai, j implică folosirea subproblemelor (Ai, k
şi Ak+1, j) şi soluţia pentru Ai, j este optimală, atunci şi soluţiile subproblemelor Ai, k şi Ak+1, j
trebuie să fie optimale!
 Demonstraţie: Folosind metoda cut-and-paste (metodă standard de demonstrare a substructurii optimale
pentru problemele de programare dinamică).
 Observație: Nu toate problemele de optim posedă această proprietate! Ex: drumul maxim dintr-un graf
orientat.
Definirea recursivă
 Folosind descompunerea in subprobleme, combinarea subproblemelor, alegerea optimală şi
substructura optimală putem să rezolvăm problema prin programare dinamică.
 Următorul pas este să definim recursiv soluţia unei subprobleme.
 Vrem să găsim o formulă recursivă pentru m[i, j] şi s[i, j].
 Cazurile de bază sunt m[i, i]
 Noi vrem să calculăm m[1, n]
 Cum alegem s[i, j] ?
 Bottom-up de la cele mai mici subprobleme la cea iniţială
Conform celor expuse putem scrie Ci,j = min  Ci,j + Ci,j + didk+1dj+1 1kj.

Se va reţine la fiecare pas indice care realizeazã minimul MinNrMul, adicã modul de
asociere al matricelor. Aceastã valoare ne permite construirea arborelui binar care
descrie ordinea efectuãrii operaţiilor. Vârfurile arborelui vor conţine limitele subşirului
de matrice ce se asociazã, rãdãcina va conţine (1,n), iar un subrabore cu rãdãcina (i,j)
va avea ca descenden[i pe (i,k) şi (k+1,j), unde k este valoarea pentru care se
realizeazã optimul cerut. Parcurgerea arborelui în postordine indicã ordinea efectuãrii
produselor.

program OrdineInmultireMatrici;

const MaxMatrici = 100;

type PNod = ^TNod;

TNod = record

ValSt, ValDr: Integer;

ArbSt, ArbDr: PNod;

end;

var Nr: Integer; { Numarul matricilor }

Dim: array[1 .. MaxMatrici + 1] of Integer; { Dimensiunile matricilor }

NrMul: array[1 .. MaxMatrici + 1, 1 .. MaxMatrici + 1] of Integer;

{ Nr. inmultirilor elementare necesare calcularii prod. Ai x ... x Aj }

{ Citirea datelor }

procedure Citeste;

var I: Integer;

begin

repeat

Write('Numarul matricilor: ');

Readln(Nr);

until (Nr > 0) and (Nr <= MaxMatrici);

Writeln('Sirul dimensiunilor:');

for I := 1 to Nr + 1 do
repeat

Write('Dimensiunea ', I, ': ');

Readln(Dim[I]);

until Dim[I] > 0;

end;

{ Calculul numarului inmultirilor }

function CalcNrMul(I, J, K: Integer): Integer; { I <= K <= J }

begin

CalcNrMul := NrMul[I, K] + NrMul[K + 1, J] +

Dim[I] * Dim[K + 1] * Dim[J + 1];

end;

{ Minimizarea numarului inmultirilor }

function MinNrMul(I, J: Integer; var KMin: Integer): Integer;

var K: Integer;

begin

KMin := I;

for K := I + 1 to J - 1 do

if CalcNrMul(I, J, K) < CalcNrMul(I, J, KMin) then KMin := K;

MinNrMul := CalcNrMul(I, J, KMin);

end;

{ Construirea arborelui }

function ConsArb(St, Dr: Integer): PNod;

var Arbore: PNod;

begin

New(Arbore);

with Arbore^ do

begin

ValSt := St;

ValDr := Dr;

if St < Dr then
begin

ArbSt := ConsArb(St, NrMul[Dr, St]);

ArbDr := ConsArb(NrMul[Dr, St] + 1, Dr);

end

else

begin

ArbSt := nil;

ArbDr := nil;

end;

end;

ConsArb := Arbore;

end;

{ Parcurgerea in postordine a arborelui }

procedure PostOrdine(Arbore: PNod);

begin

if Arbore <> nil then

with Arbore^ do

begin

PostOrdine(ArbSt);

PostOrdine(ArbDr);

Write('(', ValSt, ',', ValDr, ');');

end;

end;

{ Rezolvarea problemei }

procedure Rezolva;

var

Arbore: PNod;

I, J, K, L: Integer;

begin

for I := 1 to Nr do
NrMul[I, I] := 0;

for L := 1 to Nr - 1 do

for I := 1 to Nr - L do

begin

J := I + L;

NrMul[I, J] := MinNrMul(I, J, K);

NrMul[J, I] := K; { Se memoreaza indicele minimului }

end;

Arbore := ConsArb(1, Nr);

Writeln('Sirul asocierilor:');

PostOrdine(Arbore);

Writeln;

end;

{ Programul principal }

begin

Citeste;

Rezolva;

end.

/****************************************************\

* Problema ordinii inmultirii matricilor Rezolvare în limbajul C *

\****************************************************/

#include <stdio.h>

#include <stdlib.h>

#define MAX_MATRICI 100

typedef struct tag_nod

{ int val_st, val_dr;

struct tag_nod *arb_st, *arb_dr;

} nod;

int nr; /* Numarul matricilor */


int dim[MAX_MATRICI + 1]; /* Dimensiunile matricilor */

int nr_mul[MAX_MATRICI + 1][MAX_MATRICI + 1];

/* Nr. inmultirilor elementare necesare calcularii prod. Ai x ... x Aj */

/* Citirea datelor */

void citeste(void) { int i;

do { printf("Numarul matricilor: "); scanf("%d", &nr);

} while (nr <= 0 || nr > MAX_MATRICI);

printf("Sirul dimensiunilor:\n");

for (i = 0; i < nr + 1; i++)

do { printf("Dimensiunea %d: ", i + 1); scanf("%d", &dim[i]);

} while (dim[i] <= 0);

/* Calculul nr. inmultirilor */

int calc_nr_mul(int i, int j, int k)

{ return nr_mul[i][k] + nr_mul[k + 1][j] + dim[i] * dim[k + 1] * dim[j + 1]; }

/* Minimizarea numarului inmultirilor */

int min_nr_mul(int i, int j, int *kmin)

{ int k; *kmin = i;

for (k = i + 1; k <= j - 1; k++)

if (calc_nr_mul(i, j, k) < calc_nr_mul(i, j, *kmin)) *kmin = k;

return calc_nr_mul(i, j, *kmin);

/* Construirea arborelui */

nod *cons_arb(int st, int dr)

{ nod *arbore = malloc(sizeof(nod));

arbore->val_st = st;

arbore->val_dr = dr;

if (st < dr) { arbore->arb_st = cons_arb(st, nr_mul[dr][st]);

arbore->arb_dr = cons_arb(nr_mul[dr][st] + 1, dr); }

else arbore->arb_st = arbore->arb_dr = NULL;


return arbore;

/* Parcurgerea in postordine a arborelui */

void postordine(nod *arbore)

{ if (arbore != NULL)

{ postordine(arbore->arb_st);

postordine(arbore->arb_dr);

printf("(%d,%d);", arbore->val_st + 1, arbore->val_dr + 1);

/* Rezolvarea problemei */

void rezolva(void) { nod *arbore; int i, j, k, l;

for (i = 0; i < nr; i++) nr_mul[i][i] = 0;

for (l = 1; l <= nr - 1; l++)

for (i = 0; i < nr - l; i++)

{ j = i + l;

nr_mul[i][j] = min_nr_mul(i, j, &k);

nr_mul[j][i] = k;

arbore = cons_arb(0, nr - 1);

printf("Sirul asocierilor:\n");

postordine(arbore);

printf("\n");

/* Programul principal */

int main(void) { citeste(); rezolva(); return 0; }

Programarea dinamica, ca si metoda divide et impera, rezolva problemele combinand solutiile


subproblemelor. Dupa cum am vazut, algoritmii divide et impera partitioneaza problemele in subprobleme
independente, rezolva subproblemele in mod recursiv, iar apoi combina solutiile lor pentru a rezolva
problema initiala. Daca subproblemele contin subsubprobleme comune, in locul metodei divide et impera
este mai avantajos de aplicat tehnica programarii dinamice.
Sa analizam insa acum pentru ca sa vededem ce se intampla cu un algoritm divide et impera in aceasta
din urma situatie. Descompunerea recursiva a cazurilor in subcazuri ale aceleiasi probleme, care sunt apoi
rezolvate in mod independent, poate duce uneori la calcularea de mai multe ori a aceluiasi subcaz, si deci,
la o eficienta scazuta a algoritmului. Sa ne amintim, de exemplu, de algoritmul fib1 din Capitolul 1.

PROBLEMA RUCSACULUI (0-1) Daca ne amintim de problema rucsacului fractional care putea
fi rezolvata prin metoda GREEDY , insa nu era rezolvabila pentru Problema rucsacului 0-1 , acum putem
rezolva problema cu ajutorul programarii dinamice . In acest caz , programarea dinamica ne va permite sa
alegem obiectele astfel incit sa avem valoarea maxima si sa nu intrecem capacitatea maxima a ruscacului.

Vom avea nevoie de tablou bidimensional pentru memorarea datelor :

Pentru rezolvarea problemei date se va folosi formula data :

EXEMPLU :

Avem 4 obiecte :

1 obiect : Valoarea= 5 , Greutatea =5

2 obiect : Valoarea= 4 , Greutatea =6

3 obiect : Valoarea= 7 , Greutatea =8

4 obiect : Valoarea= 7 , Greutatea =4

Alcatuim tabloul prin metoda programarii dinamice si obtinem :

Din tablou rezulta ca cea mai buna combinatie intre obiecte este : 4 si 3 (valoare=14 , greutate=12)

LISTINGUL PROGRAMULUI

#include <stdio.h>

#include <stdlib.h>

struct Obiect //structura pentru obiect

{ int W; int V; }O[20];

int Valori[20][100]={0};

int Luam[20][100]={0} , cr=0,valoare_fin=0,masa_fin=0;

int maxim(int, int);


int main(){ int nr , Wmax; int k,w;

printf("Introdu numarul de obiecte : "); scanf("%d",&nr);

printf("\r\nIntrodu capacitatea rucsacului : "); scanf("%d",&Wmax);

for(k=1;k<=nr;k++) //introducem datele

{ printf("Introdu Masa obiectului nr %d :",k); scanf("%d",&O[k].W);

printf("Introdu Valoarea obiectului nr %d : ",k); scanf("%d",&O[k].V); }

for(k=1;k<=nr;k++)

for(w=0;w<=Wmax;w++) //alcatuirea tabelului

{ if (O[k].W>w) { cr=0; Valori[k][w]=Valori[k-1][w]; }

Else Valori[k][w]=maxim(Valori[k-1][w],Valori[k-1][w-O[k].W]+O[k].V);

if(cr==1) Luam[k][w]=1;

for(k=nr,w=Wmax;k>0;k--) //tabloul Luam va arata obiecetele mai convenabil de luat

if(Luam[k][w]==1) {

printf("\r\n Luam obiectul %d ",k); valoare_fin=valoare_fin+O[k].V;

masa_fin=masa_fin+O[k].W; w=w-O[k].W; }

printf("\r\n\r\n Valoarea maxima ce poate fi luata : %d ",valoare_fin);

printf("\r\n\r\n Masa maxima ce poate fi luata : %d ",masa_fin);

int maxim(int a, int b) //gasirea maximului

{ cr=0;

if(b>a) { cr=1; return b; }

else return a;

SCREENSHOT:
Arbori optimi la căutare

Def 2.1: Fie K o mulțime de chei. Un arbore binar cu cheile K este un graf orientat si aciclic A = (V,E) a.i.:
Fiecare nod u ∈ V conține o singură cheie k(u) ∈ K iar cheile din noduri sunt distincte.

Există un nod unic r ∈ V a.i. i-grad(r) = 0 si ∀u ≠ r, i-grad(u) = 1. ∀u ∈ V, e-grad(u) ≤ 2; S(u) / D(u) =


subarbore stânga / dreapta.

Def 2.2: Fie K o mulțime de chei peste care exista o relație de ordine . Un arbore binar de căutare satisface:
∀u,v,w ∈ V avem (v ∈ S(u) => cheie(v) cheie(u)) ∧ (w ∈ D(u) => cheie(u) cheie(w))

Căutare


Caută(elem, Arb)
 dacă Arb = null
 Întoarce null
dacă elem = Arb.val // valoarea din nodul crt.
 Întoarce Arb
 dacă elem Arb.val
 Întoarce Caută(elem, Arb.st)
 Întoarce Caută(elem, Arb.dr)
Complexitate: Θ(logn)
Inserţie în arbore de căutare
 Inserare(elem, Arb)
 dacă Arb = vid // adaug cheia in arbore
 nod_nou(elem, null, null)
 dacă elem = Arb.val // valoarea există deja
 Întoarce Arb
 dacă elem Arb.val
 Întoarce Inserare(elem, Arb.st) // adaugă in stânga
 Întoarce Inserare(elem, Arb.dr) // sau in dreapta
Complexitate: Θ(logn)

Pentru a o înţelege este necesară evidenţierea dezavantajului major al recursivităţii. El constă din
creşterea exagerată şi nerentabilă a efortului de execuţie prin repetarea ineficientă a unor paşi.
Urmărind arborele apelurilor recursive se observă repetarea inutilă a unor cazuri rezolvate anterior,
calculate deja înainte pe altă ramură a arborelui. Metodă eminamente iterativă, programarea dinamică
elimină acest dezavantaj prin “răsturnarea” procedeului de obţinere a soluţiei şi implicit a arborelui
apelurilor recursive. Printr-o abordare bottom-up (de la bază spre vîrf) ea reuşeşte să elimine operaţiile
repetate inutil în cazul abordării top-down (de la vîrf spre bază). Cu toţii am învăţat că, dacă vrem să
calculăm “cu mîna” o combinare sau un tabel al combinărilor, în loc să calculăm de fiecare dată
combinări de n luate cîte k pe baza definiţiei recursive: C(n,k)=C(n-1,k-1)+C(n-1,k) cînd n,k>0, sau,
C(n,k)=1 cînd k=0 sau n=k, este mult mai eficient să construim Triunghiul lui Pascal, pornind de la
aceeaşi definiţie a combinărilor.

C(4,2)

C(3,1) + C(3,2)

C(2,0) + C(2,1) C(2,1) + C(2,2)

1 C(1,0) + C(1,1) C(1,0) + C(1,1) 1

1 1 1 1

Observaţi cum în arborele apelurilor recursive apar apeluri în mod repetat pentru calculul aceleaşi
combinări. Acest efort repetat este evitat prin calcularea triunghiului lui Pascal în care fiecare combinare va
fi calculată o singură dată. În mod asemănător, aceeaşi diferenţă de abordare va exista între doi algoritmi de
soluţionare a aceleaşi probleme, unul recursiv – backtracking - şi altul iterativ - proiectat prin metoda
programării dinamice.

Dezavantajele acestei metode provin din faptul că, pentru a ţine minte paşii gata calculaţi şi a evita repetarea
calculării lor (în termeni de grafuri, pentru a evita calcularea repetată a unor noduri pe ramuri diferite ale
arborelui apelurilor recursive), este nevoie de punerea la dispoziţie a extra-spaţiului de memorare necesar şi
de un efort suplimentar dat de gestiunea de memorie suplimentară.

1.3.5. Branch & Bound. Este strategia cea mai sofisticată de proiectare a algoritmilor. Ea a apărut
datorită existenţei problemelor pentru care soluţia de tip backtracking poate necesita un timp astronomic de
rulare a programului. În rezolvarea acestor probleme apare o asemenea penurie de informaţii încît modelul
abstract asociat problemei - graful de căutare a soluţiilor – nu poate fi precizat în avans, din etapa de analiză.
Singura soluţie care rămîne este includerea unui subalgoritm suplimentar ce permite construirea acestui graf
pe parcurs, din aproape în aproape. Apariţia acelui subalgoritm suplimentar dă numele metodei:
branch&bound. Este posibilă compararea algoritmului branch&bound cu un robot ce învaţă să se deplaseze
singur şi eficient printr-un labirint. Acel robot va fi obligat să-şi construiască în paralel cu căutarea ieşirii o
hartă (un graf !) a labirintului pe care va aplica apoi , pas cu pas, metode eficiente de obţinere a drumului cel
mai scurt. La strategia de căutare a soluţiei în spaţiul (graful) de căutare - backtracking, fiecare pas urma
automat unul după altul pe baza unei reguli încorporate, în timp ce la strategia greedy alegerea pasului
următor era făcută pe baza celei mai bune alegeri. În cazul acestei strategii – branch&bound, pentru pasul
următor algoritmul nu mai este capabil să facă vreo alegere pentru că este obligat mai întîi să-şi determine
singur nodurile vecine ce pot fi vizitate. Numele metodei, branch=ramifică şi bound=delimitează,
provine de la cele două acţiuni ce ţin locul acţiunii de alegere de la strategia Greedy. Prima acţiune este
construirea sau determinarea prin ramificare a drumurilor de continuare, iar a doua este eliminarea
continuărilor (ramurilor) ineficiente sau eronate. Prin eliminarea unor ramuri, porţiuni întregi ale spaţiului
de căutare a soluţiei rămînînd astfel dintr-o dată delimitate şi “izolate”. Această strategie de delimitare din
mers a anumitor “regiuni” ale spaţiului de căutare a soluţiilor este cea care permite reducerea ordinului de
mărime a acestui spaţiu. Soluţia aceasta este eficientă doar dacă cîştigul oferit prin reducerea spaţiului de
căutare (scăzînd efortul suplimentar depus pentru determinarea şi eliminarea din mers a continuărilor
ineficiente) este substanţial. Soluţiile de tip backtracking, avînd la bază un schelet atît de general (algoritmul
de traversare a grafului de căutare a soluţiilor) sînt relativ simplu de adaptat în rezolvarea unor probleme.
Poate acesta este motivul care a condus pe unii programatori lipsiţi de experienţă la convingerea falsă că
“Orice este posibil de rezolvat prin backtracking”. La ora actuală, lista problemelor pentru care nu se cunosc
decît soluţii exponenţiale, total nerezonabile ca durată de execuţie a programului de soluţionare, cuprinde
cîteva sute de probleme, una mai celebră ca cealaltă. Reamintim doar de “banala” (dar agasanta) Problemă a
Orarului unei instituţii de învăţămînt care nu admite o soluţie backtracking datorită duratei astronomice de
aşteptare a soluţiei. Datorită totalei lor ineficienţe în execuţie, soluţiile backtracking obţinute după o analiză
şi o proiectare “la prima mînă” (brute-force approach, în limba engleză) ajung să fie reanalizate din nou cu
mai multă atenţie. Se constată atunci că modelul abstract asociat problemei, fie este prea sărac în informaţii
pentru determinarea grafului de căutare a soluţiilor, fie conduce la un graf de căutare avînd dimensiunea
nerezonabilă (exponenţială sau factorială, faţă de dimensiunea n a vectorului de intrare). Singura soluţie care
rămîne în această situaţie la dispoziţie este ca aceste soluţii să fie reproiectate prin metoda branch&bound.

Un exemplu uşor de înţeles de “problemă branch&bound“ îl oferă Problema Generală a Labirintului. Spre
deosebire de Problema Labirintului prezentată anterior (care admitea o soluţie de tip backtracking), în
varianta extinsă a acestei probleme, numărul direcţiilor posibile de urmat la fiecare pas poate fi oricît de
mare, iar obstacolele pot avea orice formă şi dimensiune. În acest caz, singura posibilitate este construirea
“din mers” a spaţiului de căutare a soluţiei. Astfel, pentru determinarea unui drum de ieşire din labirint sau a
drumului cel mai scurt (dacă este posibilă determinarea acestuia în timp rezonabil!) este obligatorie
adoptarea strategiei branch&bound. Oferim în continuare o situaţie concretă, ilustrată. Sesizaţi că
obstacolele, avînd forme şi dimensiuni diferite, nu pot fi ocolite decît pe un traseu “razant” sau pe un traseu
ce urmează contorul exterior al acestora. Acest fapt complică mult problema şi impune luarea unor decizii
“la faţa locului”, în momentul întîlnirii şi ocolirii fiecărui obstacol, ceea ce impune o strategie de rezolvare
de tip branch&bound – ramifică şi delimitează:      & . Deşi această strategie poate să crească uneori
surprinzător de mult eficienţa algoritmilor de soluţionare (din nerezonabili ca timp de execuţie ei pot ajunge
rezonabili, datorită reducerii dimensiunii exponenţiale a spaţiului de căutare a soluţiei), aplicarea ei este
posibilă doar printr-un efort suplimentar în etapa de analiză şi în cea de proiectare a algoritmului.
Dezavantajul major al acestei metode constă deci în efortul major depus în etapa de analiză a problemei
(analiză care însă se va face o singură dată şi bine!) şi efortul suplimentar depus în etapa proiectării
algoritmului de soluţionare. Din experienţa practică este cunoscut faptul că, pentru a analiza o problemă
dificilă un analist poate avea nevoie de săptămîni sau chiar luni de zile de analiză, în timp ce algoritmul de
soluţionare proiectat va dura, ca timp de execuţie, doar cîteva zeci de minute. Dacă programul obţinut nu
este necesar a fi rulat decît o dată, aceasta este prea puţin pentru “a se amortiza” costul mare al analizei şi
proiectării sale. În acea situaţie, soluţia branch&bound este nerentabilă şi, probabil că ar fi mai ieftină
strategia backtracking de soluţionare, chiar şi cu riscul de a obţine o execuţie (singura de altfel) a
programului cu durata de o săptămînă (ceea ce poate să însemne totuşi economie de timp).

Aplicatia metodei Branch and Bound. O problemă generală ce ar putea fi rezolvată utilizând
această tehnică ar putea fi umătoarea:

Se dă o configuraţie iniţială, pe care o vom nota CI. Se ştie că unei configuraţii i se pot aplica un
număr finit de transformări (TR). Se cere şirul minim de transformări prin care se poate ajunge de la
configuraţia iniţială CI la o configuraţie finală CF dată.

Problema ar putea fi tratată folosind metoda backtracking, dar timpul de execuţie va fi foarte mare,
mai ales în cazul problemelor de dimensiuni mari. Având în vedere că nu se cere generarea tuturor soluţiilor
problemei, ci doar a soluţiei de lungime minimă, BB propune o variantă specifică de abordare, am putea
spune că într-un anumit sens este o combinare a metodelor backtracking şi greedy.

Rezolvarea problemei se bazează pe următoarea idee: s-ar putea construi un arbore, ale cărui noduri
să conţină configuraţiile ce pot fi obţinute în urma aplicării tuturor transformărilor posibile. Vorbind la
modul general, o parcurgere în lăţime a arborelui (Bread-First), ar conduce la obţinerea soluţiei de lungime
minimă. Bineînţeles acest lucru practic nu este posibil, deoarece problema poate fi de dimensiune mare.

Prin succesor al unui nod în acest arbore vom înţelege o configuraţie în care se poate ajunge pornind
de la nodul respectiv prin aplicarea uneia din transformările permise.
Având în vedere că în arbore anumite configuraţii se pot repeta şi în plus dimensiunea arborelui
(numărul de noduri) poate fi mare, nu se va reţine întreg arborele, ci o listă cu nodurile ce trebuie prelucrate,
pe care o vom numi în continuare LIST.

Prin expandare a unui nod se înţelege generarea tuturor succesorilor posibili ai nodului.

La un moment dat, un nod din LIST pot avea una din următoarele 2 stări: poate fi expandat, sau
neexpandat, după cum a fost sau nu ales deja pentru expandare.

Problema este de a selecta pentru expandare un anumit nod din LIST, ţinând cont de faptul că se va
cere soluţia de lungime minimă. Pentru acest lucru, se foloseşte un mecanism specific inteligenţei artificiale,
asociind fiecărui nod din listă o funcţie de cost f=g+h, g reprezentănd numărul de mutări necesare pentru a
ajunge de la configuraţia iniţială la configuraţia curentă, respectiv h reprezentănd numărul (estimativ) de
mutări necesare pentru a ajunge de la configuraţia curentă la configuraţia finală. Funcţia h se numeşte
funcţie euristică, alegerea acesteia fiind importantă din punct de vedere al vitezei de execuţie al
algoritmului.

În plus, pentru a putea reface şirul mutărilor, va trebuie să reţinem pentru fiecare nod din listă
adresa “părintelui”, adică a configuraţiei din care a rezultat prin expandare configuraţia curentă.

În concluzie, nodurile din LIST sunt înregistrări ce conţin următoarele informaţii:

 configuraţia curentă
 valoarea funcţiei de cost f
 adresa nodului “părinte”
 adresa nodului următor din LIST
 un câmp care să indice dacă nodul a fost sau nu expandat
Algoritmul

1. Se va introduce în LIST un nod corespunzător configuraţiei iniţiale - pentru CI se va alege g=0 şi f=h
2. Căt timp încă nu s-a ajuns la configuraţia finală şi în LIST există noduri încă neexpandate, se execută:
 se selectează din LIST nodul neexpandat având funcţia de cost f minimă
 se expandează acest nod, obţinând toţi succesorii acestuia
 pentru fiecare succesor generat, se execută:
 se calculează funcţia g ataşată succesorului, ca fiind valoarea lui g avută de părinte +1
 se verifică dacă succesorul apare în lista LIST (a fost generat anterior)
 în caz negativ, este trecut în LIST cu funcţia de cost corespunzătoare (f=g+h) şi
este marcat ca fiind neexpandat
 în caz afirmativ, se verifică dacă valoarea lui g actuală este mai mică decât
valoarea lui g cu care a fost găsit în LIST
 dacă da, nodul găsit în LIST este direcţionat către actualul părinte şi i
se ataşează noul g, iar dacă nodul a fost marcat ca fiind expandat, i se
schimbă marcajul (devine neexpandat)
3. Dacă pasul 2 se încheie cu LIST vidă, înseamnă că problema nu are soluţie.
4. Dacă pasul 2 se încheie cu selectarea configuraţie finale, se va reface şirul transformărilor de la
configuraţia finală spre configuraţia iniţială, utilizând legătura “părinte”
Observaţii
1. Datorită modului de generare al soluţiei (se selectează spre expandare nodul cel mai “promiţător”),
soluţia obţinută va fi de lungime minimă.
2. Se poate demonstra matematic acest lucru, folosind inducţia matematică.
PROBLEMA “CĂSUŢE ADIACENTE” (Metoda Branch and Bound) Se consideră 2n căsuţe
situate pe aceeaşi linie. Două căsuţe adiacente sunt goale, n-1 căsuţe conţin caracterul ‘A’, iar celalte n-1
căsuţe conţin caracterul ‘B’. O mutare constă în interschimbarea conţinutului a două căsuţe adiacente nevide
cu cele două căsuţe libere. La intrare este dată configuraţia iniţială. Se cere să se determine un şir minim de
mutări prin care să se ajungă la configuraţia finală (cea în care toate caracterele ‘A’ apar înaintea
caracterelor ‘B’)

Exemple

1. configuraţia iniţială ABB**BAA


configuraţia finală AAA**BBB

şirul minim de mutări pentru a obţine configuraţia finală

 ABB**BAA
 ABBAAB**
 A**AABBB
 AAA**BBB
2. configuraţia iniţială AA**BBA
configuraţia finală AABAB**

şirul minim de mutări pentru a obţine configuraţia finală

 AA**BBA
 AABAB**
3. configuraţia iniţială BABABBA**ABABAB
configuraţia finală BBBBBBB**AAAAAA

Nu există soluţie

4. configuraţia iniţială ABB**AA


configuraţia finală ABAB**B

Nu există soluţie

5. configuraţia iniţială BAB**AA


configuraţia finală **BBAAA

şirul minim de mutări pentru a obţine configuraţia finală

 BAB**AA
 **BBAAA
 BB**AAA
 BBAAA**
 B**AABA
 BAA**BA
 **ABABA
 ABAB**A
Observaţii

1. În configuraţii s-a înlocuit spaţiul cu caracterul ‘*’, pentru vizibilitate.


2. Configuraţiile se vor reprezenta sub forma unor şiruri de caractere.
3. Pentru rezolvare se va aplica mecanismul BB. cu precizarea ca funcţia euristică h se va alege astfel:
dacă C este configuraţia curentă şi CF este configuraţia finală, atunci h(C) reprezintă numărul de
diferenţe dintre configuraţiile C şi CF, adică numărul de caractere (diferite de spaţiu) din C care nu se
află pe poziţia finală (din CF)
 pentru exemplul 1
h(‘ABB**BAA’)=1+1+1+1=4

h(‘A**AABBB’)=1+1+1=3
 pentru exemplul 2
h(‘AA**BBA’)=1+1+1=3

4. În algoritmul ce-l vom prezenta, soluţia va fi afişată în sens invers, sub forma şirului de mutări de la CF
până la CI.
5. Dacă numărul de mutări necesare pentru a ajunge la configuraţia finală creşte, timpul de execuţie creţte
foarte mult (exponenţial)
#include <stdio.h>
#include <conio.h>
#include <iostream.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define maxint 32000

/* Structura unui nod


- un camp de informatie inf - configuratia curenta sub forma de string
- g - functia de cost asociata nodului
- t - 0 daca nodul nu a fost expandat
- 1 daca nodul a fost deja expandat
- leg - adresa nodului urmator din lista
- sus - adresa nodului parinte (a configuratiei de unde a fost derivata
configuratia curenta) */
typedef struct nod
{
char inf[100];
int g,t;
struct nod *sus,*leg;
} LISTA;
LISTA *cap;
/* functia verifica daca o configuratie x apare sau nu in lista – daca da,in pointerul q se retine adresa unde a fost gasita
configuratia x in lista*/
int apare(char x[],LISTA *q)
{
q=cap;
while(q)
if (!strcmp(q->inf,x)) return 1;
else q=q->leg;
return 0;
}
/* functia determina numarul de aparitii ale caracterului y in sirul de carctere x*/
int nr(char x[],char y)
{
int k=0;
for(int i=0;x[i];i++)
k+=(x[i]==y)?1:0;
return k;
}
/* functia determina numarul de pozitii pe care difera configuratia x de configuratia finala */
int difera(char x[])
{
int k=0;
for(int i=0;x[i];i++)
k+=x[i]!=cf[i]?1:0;
return k;
}
/* functia interschimba in x caracterele de pe pozitiile i,i+1 cu caracterele de pe pozitiile j,j+1. Rezultatul
interschimbarii se retine in y */
void schimb(char x[],int i,int j,char y[])
{
strcpy(y,x);
char c=y[i];
y[i]=y[j];
y[j]=c;
c=y[i+1];
y[i+1]=y[j+1];
y[j+1]=c;
}
/* eliberarea zonei de memorie ce a fost alocata listei */
void elib(void)
{
while (cap)
{
LISTA *p=cap;
cap=cap->leg;
delete p;
}
}

/* functia care tipareste solutia problemei - y este configuratia finala, configuratiile intermediare se vor obtine pe baza
legaturii SUS a nodurilor curente */
void tipar(LISTA *q,int gasit)
{
LISTA *p;
int k=1;
if (!gasit) cout<<endl<<"nu este solutie";
else
{
clrscr();
cout<<endl<<k<<" "<<cf;
int cond=1;
while (cond)
if (!strcmp(q->inf,ci)) cond=0;
else
{
p=cap;
int f=0;
while (p&&!f)
if (!strcmp(p->inf,q->inf))
{
k++;
cout<<endl<<k<<" "<<p->inf;
f=1;
}
else p=p->leg;
q=q->sus;
}
cout<<endl<<k+1<<" "<<q->inf;
elib();
}
getch();
}
/* functia care realizeaza expandarea unui nod q - daca in urma expandarii s-a ajuns la configuratia finala, y va contine
configuratia finala */
void expandare(LISTA *q,int &k,char y[])
{
int j;
LISTA *r,*p;
char x[100];
strcpy(x,q->inf);
k++;
j=strchr(x,' ')-x;
strcpy(y,x);
for(int i=0;i<=strlen(x)-2;i++)
// if ((i!=j-1)&&(i!=j)&&(i!=j+1)&&(i!=j+2))
if (!isspace(x[i])&&!isspace(x[i+1]))
{
schimb(x,i,j,y);
if (!strcmp(y,cf))
break;
else
{
int v=k+difera(y);
if (!apare(y,p))
{
r=new LISTA;
strcpy(r->inf,y);
r->sus=q;
r->leg=cap;
r->t=0;
cap=r;
r->g=v;
}
else
if (p->g>v)
{
p->g=v;
p->sus=q;
if (p->t)
p->t=!p->t;
}
}
}
}
/* se realizeaza expandarea repetata a nodurilor din lista - pentru expandare se va alege nodul pentru care functia de
cost este minima*/
void b_b(void)
{
LISTA *q;
char y[100];
int gasit;
cap=new LISTA;
strcpy(cap->inf,ci);
cap->leg=NULL;
cap->sus=NULL;
cap->t=0;
cap->g=difera(ci);
int cond=1;
int k=0;
while (cond)
{
gasit=0; // gasit=0 daca nu mai exista noduri neexpandate
LISTA *p=cap;
int min=maxint;
while (p)
{
min=(!p->t && (min>p->g))?(q=p,p->g):min;
p=p->leg;
}
if (min==maxint) cond=0;
else
{
gasit=1;
q->t=! q->t;
expandare(q,k,y);
if (!strcmp(y,cf)) cond=0;
}
}
tipar(q,gasit);
}
void main(void)
{ char ci[20],cf[20];
clrscr();
cout<<”Dati configuratia initiala:”;
cin>>ci;
cout<<”Dati configuratia finala:”;
cin>>cf;
if ((nr(ci,'a')!=nr(cf,'a'))||(nr(ci,'b')!=nr(cf,'b')))
{
cout<<endl<<"nu este solutie";
getch();
}
else
b_b();
}
1.3.6. Recursivitatea. Aşa cum am amintit deja, această metodă de programare poate fi privită ca formă
particulară de exprimare a metodei Back-Tracking. Cu toate acestea, cei ce cunosc istoria informaticii şi originile
programării ştiu că această metodă are totuşi un caracter special. Aceste lucruri dorim să le evidenţiem în continuare.
Încă înainte de apariţia primului calculator şi, deci implicit a oricărei tehnici de programare, unii matematicieni erau
profund preocupaţi de noţiunea de calculabilitate. Această noţiune îi putea ajuta în efortul lor deosebit de a fundamenta
noţiunea elementară de algoritm sau metodă automată de calcul. În paralel, cele mai valoroase rezultate le-au obţinut
latino-americanul Alonso Church şi englezul Alan Turing. În timp ce Turing a introdus pentru algoritm modelul
matematic abstract cunoscut sub numele de Maşina Turing (care stă la bazele modelului actual de calculator), Church a
fundamentat noţiunea de metodă de calcul sau calculabilitatea pe funcţiile recursive. Astfel, teza lui Church afirma că
orice funcţie definită pe domeniul numerelor naturale este calculabilă dacă şi numai dacă ea este recursivă. Deşi
aparatul teoretic folosit de Church era în întregime matematic (se baza numai pe funcţii numerice naturale), lui nu i-a
fost greu să demonstreze că orice algoritm nenumeric se reduce la funcţii recursive şi la mulţimi recursive de numere
naturale (pe baza unor codificări riguros alese). Acest din urmă rezultat este cel care ne interesează pe noi şi noi îl vom
reformula fără ai afecta valabilitatea: orice algoritm poate fi rescris printr-un algoritm recursiv (limbajul de programare
Lisp se bazează în întregime pe acest fapt). Chiar dacă nu constituie o demonstraţie riguroasă, următoarea echivalenţă
practică (descrisă în pseudo-cod) este deosebit de convingătoare: orice instrucţiune de ciclare este echivalentă cu un
apel recursiv de subprogram sau funcţie. 45 Varianta iterativă-cu ciclu Varianta cu apel recursiv contor:=val_init;
Repetă Corp_ciclu; Incrementează(contor); Pînă cînd contor=val_finală;
Funcţie_Recursivă(contor){ Dacă contor char *sir1="primul",*sir2="al doilea";

void strcopy(char *sursa,char *dest){ if ((*dest++=*sursa++)==NULL) return; else strcopy(sursa,dest); }

void main(void){ printf("\nInainte, sirul sursa:%s, sirul destinatie:%s",sir1,sir2); strcopy(sir1,sir2);


printf("\nSi dupa, sirul sursa:%s, sirul destinatie:%s",sir1,sir2); }

2. Să se afişeze primele n pătrate perfecte.

#include #include

int n;

void patrat(int m){ if(m>n)return; else { printf("%i:%i ",m,m*m); patrat(m+1); } }

void main(void){ printf("n=");scanf("%i",&n); patrat(1); }

3.Algoritmul lui Euclid.

#include

int cmmdc(int m,int n){ if (n==0) return(m); else cmmdc(n,m%n); }

void main(void){ int m,n; printf("m,n=");scanf("%i,%i",&m,&n); printf("cmmdc(%i,%i)=


%i",m,n,cmmdc(m,n)); }
4. Se citeşte n, să se găsească primul număr prim mai mare decît n. (Se presupune cunoscută demonstraţia
faptului că există p-prim mai mare decît oricare n. Sîntem astfel siguri că algoritmul se opreşte! )

#include

#include …

int n;

int are_divizor(int p,int d){ if(d>sqrt(p))return 0; else if(p%d==0) return 1; else are_divizor(p,d+1); }

void prim(int p){ if(!are_divizor(p,2)){ printf("\n%i",p); return; } else prim(p+1); }

void main(){ printf("n=");scanf("%i",&n); prim(n+1); }

5. Să se afişeze primele n numere prime.

#include

#include

int n;

int are_divizor(int p,int d){ if(d>sqrt(p))return 0; else if(p%d==0) return 1; else are_divizor(p,d+1); } void
prim(int p,int i){ if(i>n)return; if(!are_divizor(p,2)){ printf("%i,",p); prim(p+1,i+1); 47 } else prim(p+1,i); }
void main(){ printf("n=");scanf("%i",&n); prim(2,1); }

6. Se citeşte n gradul unui polinom P şi a[0],a[1],...,a[n] coeficienţii reali ai acestuia. Se citeşte o valoare
reală x, să se calculeze P(x).

#include

int n; float a[20],x;

float P(int i){ if(i==1)return a[0]; else return P(i-1)*x+a[i-1]; }

void citeste_coef(int i){ if(i>n)return; else {printf("%i:",i);scanf("%f",&a[i]);citeste_coef(i+1);} }

void main(){ printf("n=");scanf("%i",&n); citeste_coef(0); printf("x=");scanf("%f",&x); printf("P(%f)=


%f",x,P(n+1)); }

7. Se citesc m şi n gradele a două polinoame P şi Q, şi a[0],a[1],...,a[m] respectiv b[0],b[1],...,b[n]


coeficienţii reali ai acestora. Să se afişeze coeficienţii c[0],c[1],...,c[m+n] ai polinomului produs R=PxQ.

#include

int m,n; float a[20],b[20],c[40];

float suma_prod(int i,int j){ if(j==i)return a[i]*b[0]; else return a[i-j]*b[j]+suma_prod(i,j+1); }

void calc_coef(int i){ if(i>m+n)return; else c[i]=suma_prod(i,0); }

void citeste_coef(float a[],int i){ if(i>n)return; else {printf("%i:",i);scanf("%f",&a[i]);citeste_coef(a,i+1);} }


void afis_coef(float a[],int i){ if(i>n)return; else {printf("%f ",a[i]);afis_coef(a,i+1);} }
void main(){ printf("m(gradul polinomului P)=");scanf("%i",&m); printf("Introd.coef.polinomului P:");
citeste_coef(a,0); 48 printf("n(gradul polinomului Q)=");scanf("%i",&n); printf("Introd.coef.polinomului
Q:"); citeste_coef(b,0); calc_coef(0); afis_coef(c,0); }

Metoda Greedy combinata cu Backtracking(problema rucsacului)


Se dau n obiecte care se pun la dispozitia unei persoane care le poate transporta cu ajutorul unui rucsac..
Pentru fiecare obiect i se notează prin c, >0 câştigul obţinut în urma transportului său în întregime la
destinaţie, iar prin gi >0.
Să se facă o încărcare a rucsacului astfel încât câştigul să fie maxim ştiind că sau:
1) pentru orice obiect putem încărca orice parte din el în rucsac
2) orice obiect poate fi încărcat numai în întregime în rucsac
Pentru a objţine soluţia optimă în acest caz e suficient să aplicăm metoda Greedy, încărcănd în rucsac
obiectete în ordinea descrescătoare a valorilor c i/gi până când acesta se umple. Pentru a şti ordinea folosim o
procedură care construieşte vectorul k: al poziţiilor în şirul ordonat descrescător.
Algoritmul in C:

#include<time.h>

#include <stdio.h>

int main() { double start,stop; //variabile care determina timpulde executie a programului

start=clock() ; //]ncepe calculul timpului de executie

int n, p[200], rez[200], k, M, i, j, sum, temp, x;

printf("Introduceti numarul de obiecte dorite:"); scanf("%d", &n); //citim numarul deobiecte

printf("Introduceti greutatea fiecarui obiect\n");

for (i=0; i<n; i++) { printf("Obiectul %d=",i); scanf("%d", &p[i]); //citim greutatea fiecarui obiect

printf("Introduceti adincimea rucsacului: "); scanf("%d",&M); //citim greutatea care incape in rucsac

for (i=0; i<n-1; i++) //parcurge pina la penulltimul element

for (j=i+1; j<n; j++) //parcurge pina la ultimul element

if (p[i]>p[j]) { temp = p[i]; p[i] = p[j]; p[j] = temp; }; //sorteaza in crestere greutatea

k = 0; sum = 0; //initial suma este nula sau 0

for (i=0; i<n; i++) { x = p[i];

if ( sum + x <= M ) //daca suma nu depaseste cea din rucsac

{ rez[k++] = x; sum += x; //se adauga la suma posibila in rucsac

} else break; //daca nu se termina parcurgerea

printf("\nNumarul de obiecte care poate fi luat este %d cu greutatea de %dkg sau 43kg",k,sum);

printf("\n"); //afiseaza numarul de obiecte si greutatea posibila

stop=clock();printf("Timpul de executie=%lf s", (stop-start)/CLOCKS_PER_SEC); //afiseaza timpul de executie

getch();
}

4.Analiza şi implementarea ceasului de timp real al sistemului pentru a estima performanţele algoritmului
(#include <time.h>)

4.1. Funcții pentru gestiunea datei și orei

     Prototipurile functiilor pentru citirea/setarea datei si orei. Ele implica includerea fisierului dos.h și time.h.

 void getdate(struct date *d); - încarca structura de tip date spre care pointeaza d  cu datele corespunzatoare
furnizate de sistemul de  operare;
 void gettime(struct time *t); - încarca structura de tip time spre care pointeaza t  cu datele corespunzatoare
furnizate de sistemul de  operare;
 void setdate (struct date *t);  - seteaza data curenta în conformitate cu datele de  tip date;
 void settime (struct time *t); - seteaza ora curenta în conformitate cu datele de tip  time spre care pointeaza t.
Tipurile date si time sunt definite în fisierul time.h. astfel:
      struct date ;

      struct time ;

Funcţiile de timp:
1) time_t time(time_t *timer); - returnează numărul de secunde scurse de la 00:00:00 GMT, 1
ianuarie 1970, memorând valoarea respectivă la adresa timer (cu condiţia să fie diferită de NULL) - alegerea anului
1970 ca referinţă s-a făcut pentru compatibilitatea cu UNIX.
- are prototipul în time.h;
- dacă se apelează în forma time(NULL), valoarea este numai returnată, nu şi memorată;
- time_t este definită în time.h ca long (typedef long time_t;);
Exemplu:
time_t *z,x;
char *sir;
z=(time_t*)malloc(sizeof(time_t));
x=time(z);
sir=ctime(z); printf("timpul scurs este %ld, data este: %s\n",x,sir);

2) double difftime(time_t time2, time_t time1); - calculează diferenţa (în secunde) între două momente de
timp (memorate în time1 şi time2, tot în secunde);
- are prototipul în time.h;
- rezultatul diferenţei este returnat ca un double;

3) char *ctime(const time_t *time); - funcţia converteşte valoarea timpului de la adresa pointată de argumentul
din time (completată printr-un apel al funcţiei time() într-un şir de 26 de caractere de forma: DDD MMM dd
hh:mm:ss YYYY , unde
DDD=ziua (Mon, Tue...)
MMM=luna (Jan,Feb...)
dd=ziua din luna (1..31)
hh:mm:ss=ora:min:sec
YYYY=an
- are prototipul în time.h;
- returnează un pointer pe şirul de caractere format (o zonă de memorie alocată static, rescrisă la fiecare apel);

Exemplu:
time_t *z,x; char *sir;
z=(time_t*)malloc(sizeof(time_t));
x=time(z); sir=ctime(z); printf("Data este: %s\n",sir);
4) int stime(time_t *tp) - fixează data şi ora sistem;
- are prototipul în time.h;
- tp pointează pe valoarea dorita, măsurata în secunde de la 00:00:00 GMT, 1 ianuarie 1970;
- returnează 0;

5) void getdate(struct date *datep) - citeşte data sistem, completând structura pointată de datep;
- are prototipul în DOS.H;
- structura date este definită ca:
-
struct date{ int da_year; /*anul curent*/
char da_day; /*ziua din lună*/
char da_mon; /*luna (1 = ianuarie)*/
};

6) void gettime(struct time *timep) - citeşte ora sistem, completând structura pointată de timep;
- are prototipul în DOS.H;
- structura time este definită ca:

struct time {
unsigned char ti_min;
unsigned char ti_hour; unsigned char ti_sec;
unsigned char ti_hund; /*sutimi de secundă*/
};

7) void setdate(struct date *datep) - fixează data sistem conform conţinutului structurii de tip date pointată de
datep;
- are prototipul în DOS.H;

8) void settime(struct time *timep) - fixează ora sistemului conform valorilor din structura de tip time, pointată
de timep;
- are prototipul în DOS.H;

9) struct tm *localtime(const time_t *timer) - funcţia primeşte ca parametru adresa unei valori returnate de
time() şi returnează un pointer la o structură de tip tm completată corespunzător;
- are prototipul în time.h;

10) time_t mktime(struct tm *t)- completează datele calendaristice din structura de tip tm pointată de t;
- are prototipul în time.h;
- returnează aceeaşi valoare ca time(), -1 dacă nu se poate determina corect tm_wday sau tm_yday;
(adică ziua din saptămână ca şir de caractere sau ziua din an , ca număr)

11) char *asctime(const struct tm *tblock) - converteşte data şi ora în ASCII;


- are prototipul în time.h;
- face conversia datelor dintr-o structură de tip tm, pointată de tblock, într-un şir de caractere, de o manieră identică cu
funcţia ctime();

FUNCŢII PENTRU GESTIUNEA DATEI ŞI OREI


      Dam mai jos prototipurile a patru functii pentru citirea/setarea datei si orei. Ele implica includerea
fisierului dos.h.

void getdate(struct date *d); - încarca structura de tip date spre care pointeaza d  cu datele corespunzatoare
furnizate de sistemul de  operare;

void gettime(struct time *t); - încarca structura de tip time spre care pointeaza t  cu datele corespunzatoare
furnizate de sistemul de  operare;
void setdate (struct date *t);      - seteaza data curenta în conformitate cu datele de  tip date;

void settime (struct time *t);      - seteaza ora curenta în conformitate cu datele de tip  time spre care
pointeaza t.

a) time_t time(time_t *timer); - returnează numărul de secunde scurse de la 00:00:00 GMT, 1 ianuarie
1970, memorând valoarea respectivă la adresa timer (cu condiţia să fie diferită de NULL) - alegerea
anului 1970 ca referinţă s-a făcut pentru compatibilitatea cu UNIX (referinţa DOS fiind 1980);

b) double difftime(time_t time2, time_t time1); - calculează diferenţa (în secunde) între două momente de
timp (memorate în time1 şi time2, tot în secunde);
c) char *ctime(const time_t *time); - funcţia converteşte valoarea timpului de la adresa pointată de argumentul din
time (completată printr-un apel al funcţiei time() într-un şir de 26 de caractere de forma: DDD MMM dd hh:mm:ss
YYYY
      Dam mai jos prototipurile a patru functii pentru citirea/setarea datei si orei. Ele implica includerea fisierului dos.h.

void getdate(struct date *d); - încarca structura de tip date spre care pointeaza d  cu datele corespunzatoare furnizate de sistemul de  operare;

void gettime(struct time *t); - încarca structura de tip time spre care pointeaza t  cu datele corespunzatoare furnizate de sistemul de  operare;

void setdate (struct date *t);      - seteaza data curenta în conformitate cu datele de  tip date;

void settime (struct time *t);      - seteaza ora curenta în conformitate cu datele de tip  time spre care pointeaza t.

Tipurile date si time sunt definite în fisierul dos.h. astfel:

      struct date ;

      struct time ;


Exemplu:

#include <stdio.h>;                     //  Timp : Data, Ora 

#include <dos.h>;

void main (void)……..

Funcţiile de timp
6. time_t time(time_t *timer); - returnează numărul de secunde scurse de la 00:00:00 GMT, 1 ianuarie 1970, memorând valoarea respectivă la
adresa timer (cu condiţia să fie diferită de NULL) - alegerea anului 1970 ca referinţă s-a făcut pentru compatibilitatea cu UNIX (referinţa DOS
fiind 1980);
- are prototipul în time.h;
- dacă se apelează în forma time(NULL), valoarea este numai returnată, nu şi memorată;
- time_t este definită în time.h ca long (typedef long time_t;);
Exemplu:
time_t *z,x;
char *sir;
z=(time_t*)malloc(sizeof(time_t));
x=time(z);
sir=ctime(z); printf("timpul scurs este %ld, data este: %s\n",x,sir);...
7. double difftime(time_t time2, time_t time1); - calculează diferenţa (în secunde) între două momente de timp (memorate în time1 şi time2, tot
în secunde);
- are prototipul în time.h;
- rezultatul diferenţei este returnat ca un double;
8. char *ctime(const time_t *time); - funcţia converteşte valoarea timpului de la adresa pointată de argumentul din time (completată printr-un
apel al funcţiei time() într-un şir de 26 de caractere de forma: DDD MMM dd hh:mm:ss YYYY , unde
DDD=ziua (Mon, Tue...)
MMM=luna (Jan,Feb...)
dd=ziua din luna (1..31)
hh:mm:ss=ora:min:sec
YYYY=an
- are prototipul în time.h;
- returnează un pointer pe şirul de caractere format (o zonă de memorie alocată static, rescrisă la fiecare apel);
Exemplu:
....time_t *z,x; char *sir;
z=(time_t*)malloc(sizeof(time_t));
x=time(z); sir=ctime(z); printf("data este: %s\n",sir);...
Rezultatul este de forma:
Thu Aug 23 12:15:30 2001
9. int stime(time_t *tp); - fixează data şi ora sistem;
- are prototipul în time.h;
- tp pointează pe valoarea dorita, măsurata în secunde de la 00:00:00 GMT, 1 ianuarie 1970;
- returnează 0;
Exemplu:
...time_t t; char *data; t=time(NULL); //află timpul scurs
data=ctime(&t); // scoate data în formatul specific: DDD MMM dd hh:mm:ss YYYY
printf("Data curenta este %s", data);
t=t-24L*60L*60L; //scade un număr de secude pentru a ajunge în ziua precedenta; se foloseşte L pentru long int
stime(&t) //setează data curentă la noua valoare
printf("Data de ieri este: %s", ctime(&t)); //o afişează....
//se revine la data curenta t=t+24L*60L*60L şi se seteaza
10. void getdate(struct date *datep); - citeşte data sistem, completând structura pointată de datep;
- are prototipul în DOS.H;
- structura date este definită ca:
5. struct date{ int da_year; /*anul curent*/
6. char da_day; /*ziua din lună*/
7. char da_mon; /*luna (1 = ianuarie)*/
8. };
Exemplu:
...struct date d;
getdate(&d); printf("an:%d luna:%d zi:%d",d.da_year,d.da_mon,d.da_day);...
11. void gettime(struct time *timep); - citeşte ora sistem, completând structura pointată de timep;
- are prototipul în DOS.H;
- structura time este definită ca:
struct time {
unsigned char ti_min;
unsigned char ti_hour; unsigned char ti_sec;
unsigned char ti_hund; /*sutimi de secundă*/
};
Exemplu:
...struct time t;
gettime(&t);
printf("ora curentă este %2d:%2d:%2d:%2d",t.ti_hour,t.ti_min,t.ti_sec,t.ti_hund);...
12. void setdate(struct date *datep); - fixează data sistem conform conţinutului structurii de tip date pointată de datep;
- are prototipul în DOS.H;
Exemplu:
struct date d_noua,d_veche;
getdate(&d_veche); //reţine data curentă
d_noua.da_year=2003; //completează structura cu valori
d_noua.da_mon=5; d_noua.da_day=30;
setdate(&d_noua); //setează noua dată
printf("an:%d luna:%d zi:%d",d_noua.da_year,d_noua.da_mon,d_noua.da_day);
setdate(&d_veche); //restaurează vechea data
printf("an:%d luna:%d zi:%d",d_veche.da_year,d_veche.da_mon,d_veche.da_day);...
13. void settime(struct time *timep); - fixează ora sistemului conform valorilor din structura de tip time, pointată de timep;
- are prototipul în DOS.H;
Exemplu:
...struct time t;
gettime(&t); printf("ora curentă este %2d:%2d:%2d:%2d",t.ti_hour,t.ti_min,t.ti_sec,t.ti_hund);
t.ti_min=t.ti_min+5; //adaugă 5 minute
settime(&t); printf("ora modificată este %2d:%2d:%2d:%2d",t.ti_hour,t.ti_min,t.ti_sec,t.ti_hund);...
14. struct tm *localtime(const time_t *timer); - funcţia primeşte ca parametru adresa unei valori returnate de time() şi returnează un pointer la o
structură de tip tm completată corespunzător;
- are prototipul în time.h;
- structura tm este definită în time.h astfel:
struct tm {
int tm_sec; /*secunda */
int tm_min; /*minut */
int tm_hour; /*ora 0..23 */
int tm_mday; /* ziua din luna 1..31 */
int tm_mon; /* luna din an 0..11 */
int tm_year; /*anul - 1900*/
int tm_wday; /*0..6, 0=duminică */
int tm_yday; /*ziua din an 0..365 */
int tm_isdst; /* este nenul dacă se aplică şi corecţia pentru
ora de vară - USA (daylight saving time) */
};
- returnează adresa unei structuri statice, rescrisă la fiecare apel;
15. time_t mktime(struct tm *t); - completează datele calendaristice din structura de tip tm pointată de t;
- are prototipul în time.h;
- returnează aceeaşi valoare ca time(), -1 dacă nu se poate determina corect tm_wday sau tm_yday;
(adică ziua din saptămână ca şir de caractere sau ziua din an , ca număr)
16. char *asctime(const struct tm *tblock); - converteşte data şi ora în ASCII;
- are prototipul în time.h;
- face conversia datelor dintr-o structură de tip tm, pointată de tblock, într-un şir de
caractere, de o manieră identică cu funcţia ctime();
Pentru o formatare mai sofisticată a afişării timpului se poate folosi functia strftime().
Exemplu:
...time_t t;
struct tm *tblock;
t=time(NULL) // află timpul
tblock=localtime(&t); //converteşte şi completează datele din structura tblock
printf("Data şi Ora locală sunt %s" , asctime(tblock));...
Structuri predefinite. Anumite functii de bibliotecã folosesc tipuri structurã definite în fisiere de tip H si care “ascund” detalii ce
nu intereseazã pe utilizatori. Un exemplu este tipul FILE definit în "stdio.h" si a cãrui definitie depinde de implementare si de
sistemul de operare gazdã (o structurã “opacã”, invizibilã pentru utilizator). Structura “struct tm” definitã în fisierul <time.h>
contine componentele ce definesc complet un moment de timp:
struct tm { int tm_sec, tm_min, tm_hour; int tm_mday, tm_mon, tm_year; int tm_wday, tm_yday; int tm_isdst; };

Ceas creat in limbajul C , culoarea textului se schimba la ficare secunda .

#include <stdio.h>

#include <time.h>

#include <stdlib.h>

#include <unistd.h>

int main() { struct tm *loc; char s[8]={"color 07"},i=48; // cream sirul si regulam culoarea

time_t timp;

while(1) //ciclu vesnic

{ system(s);

timp=time(NULL);

s[7]=i;

if(i==57) i=47; //resetam culoarea

loc=localtime(&timp); // preluarea timpului

printf("\r\n\r\n\r\n\r\n \t\t\t\t");

printf(" Ora : %d : %d : %d",loc->tm_hour,loc->tm_min,loc->tm_sec);

sleep(1); //pauza de 1 secunda

system("cls");

i++;

} }

Pentru o formatare mai sofisticată a afişării timpului se poate folosi functia strftime()
Exemplu:
time_t t;
struct tm *tblock;
t=time(NULL) // află timpul
tblock=localtime(&t); //converteşte şi completează datele din structura tblock
printf("Data şi Ora locală sunt %s" , asctime(tblock));
Listingul programului :

//Presupunem ca vrem sa platim o suma S si avem la //dispozitie un numar nelimitat de monezi avand

//valorile k^0, k^1,...k^(n-1).

//Aratati ca metoda GREEDY da mereu solutia optima.

#include <stdio.h>

#include <conio.h>

#include <math.h>

#include <time.h>

int fun(int *suma,int b)

{ int r; r=*suma/b;

if (r>0) printf("%d de %d \n",r,b);

*suma=*suma % b;

return (suma,r);

main() { time_t start_time , stop_time; int *p,suma,n,k,i;

clrscr(); printf("introduceti k :"); scanf("%d",&k);

printf("introduceti n :"); scanf("%d",&n); printf("introduceti suma :"); scanf("%d",&suma);

p=&k; time(&start_time);

for (i=n-1;i>=0;i--)

fun( &suma, pow(*p,i) ); time(&stop_time);

printf (" program rulat in %.10f secunde:\n",stop_time - start_time); getch();

Rezultate obtinute :

1. Care sunt regulile generale pentru evaluarea timpului de execuţie?


Regurile generale pentru evaluarea timpului de executie a programului sunt:

1) algoritmul să fie simplu de înţeles, de codificat şi de depanat;

2) algoritmul să folosească eficient resursele calculatorului, să aibă un timp de execuţie redus

Dacă programul care se scrie trebuie rulat de un număr mic de ori, prima cerinţă este mai importantă; în această
situaţie, timpul de punere la punct a programului e mai important decât timpul lui de rulare, deci trebuie aleasă
varianta cea mai simplă a programului. Se vor crea doua variabile de tip float sau double pentru calcului inceputului
timpului de executie si marcarea sfirșitului timpului de executie. Apoi din timpul final scadem timpul timpul inițial si
formăm timpul de executie in secunde
2. Care sunt etapele analizei empirice şi în care cazuri se face analiza empirică a algoritmilor?
În analiza empirică a unui algoritm se parcurg de regulă următoarele etape:

 Se stabileşte scopul analizei.


 Se alege metrica de eficienţă ce va fi utilizată (număr de execuţii ale unei/unor operaţii sau timp de execuţie a
întregului algoritm sau a unei porţiuni din algoritm.
 Se stabilesc proprietăţile datelor de intrare în raport cu care se face analiza (dimensiunea datelor sauproprietăţi
specifice).
 Se implementează algoritmul într-un limbaj de programare.
 Se generează mai multe seturi de date de intrare.
 Se execută programul pentru fiecare set de date de intrare.
 Se analizează datele obţinute.
3. Ce proprietăţi posedă funcţiile asimptotice?
Funcțiile asimptotice au o efeciență pentru rezolvarea problemelor dedimensiuni mari. Problema eficienţei devine
critică pentru probleme de dimensiuni mari se face analiza complexităţii pentru cazul când n este mare teoretic se
consideră că n —> ∞, în felul acesta luându-se în considerare doar comportarea termenului dominant. Acest tip de
analiză se numeşte analiză asimptotică. In cadrul analizei asimptotice se consideră că un algoritm este mai eficient
decât altul dacă ordinul de creştere al timpului de execuţie al primului este mai mic decât al celuilalt.

. Probleme propuse
I. Aplicaţi metoda divizării pentru rezolvarea problemelor următoare:
1. Sortaţi un şir prin metoda inserţiei. (Sortarea şirului a[1..n] se poate reduce la sortarea subşirului a[1..n-1]
urmată de inserarea valorii a[n] în acest subşir sortat.)
2. Căutaţi secvenţial prima apariţie a unei valori într-un şir. (Indicele primei apariţii a lui v în şirul a[1..n] este 1
dacă v = a[1], iar în caz contrar este indicele primei apariţii în subşirul a[2..n].)
3. Scrieţi un subalgoritm recursiv pentru tipărirea unui şir. (Tipărirea şirului a[1..n] se reduce la tipărirea
subşirului a[1..n-1] şi apoi a elementului a[n].)
4. Calculaţi recursiv Cnk pentru 0  k  n. (Folosiţi faptul că pentru 0 < k < n are loc:
Cnk =              �moduri de selectare a k obiecte din n�
         Cn-1k +  �moduri de selectare a k obiecte din primele n-1 obiecte�
         Cn-1k-1      �moduri de selectare a k-1 obiecte din primele n-1 obiecte şi
                         includerea obiectului cu numărul de ordine n�)
5. Scrieţi un subalgoritm recursiv care calculează cel mai mare divizor comun a n numere. Pentru calculul
celui mai mare divizor comun  (a, b) a două numere a şi b, folosiţi rezultatele:
(a, b) = 2* (a / 2, b / 2), pentru a şi b pare
(a, b) = (a / 2, b), pentru a  par şi b impar
(a, b) = (a - b, b), pentru a şi b impare.
6. Calculaţi recursiv xn, unde x este real iar n este natural. (Folosiţi reprezentarea lui n ca sumă de puteri ale
lui 2. De exemplu, x11 =  (( x8 ) x2 ) x1.)
II. Aplicaţi metoda backtracking pentru rezolvarea problemelor următoare:
7. Fie k şi n numere naturale, cu 0  k  n. Determinaţi toate submulţimile cu k elemente ale mulţimii {1, 2, ...,
n}.
8. Fie o tablă dreptunghiulara de lungime l şi înalţime h, având pe suprafaţa ei n găuri de coordonate numere
întregi. Efectuând numai tăieturi verticale şi orizontale, decupaţi din tablă o bucată de arie maximă care nu
prezintă găuri.
9. Se dau m,nN. Tipăriţi toate submulţimile cu m elemente ale multimii {1, 2, ..., n}.
10. Fie o hartă ce conţine ţări (cu relaţiile de vecinătate între ele). Determinaţi numărul minim de culori
necesare pentru a colora harta astfel încât oricare două ţări vecine să fie colorate cu culori diferite.
11. Fie n monezi cu valori întregi strict pozitive a1, ..., an.. Determinaţi o modalitate de plată a unei sume S
(dacă există), folosind un număr minim de monezi.
12. Într-un oraş există n intersecţii legate prin străzi. Distribuiţi un număr minim de poliţişti în intersecţii astfel
încât să fie supravegheate toate strazile.
III. Aplicaţi metoda greedy pentru rezolvarea problemelor următoare:
13. (Minimizarea timpului mediu de aşteptare) Un singur procesor trebuie să execute n lucrări. Pentru fiecare
lucrare i (1  i  n) se cunoaşte în prealabil timpul de execuţie ti. Minimizaţi timpul mediu de aşteptare pentru
execuţia lucrărilor:

(timpul de aşteptare până la terminarea execuţiei lucrării i) / n.


            Indicaţie: De exemplu, dacă avem trei lucrări cu timpii de execuţie t1 = 4, t2 = 14 şi t3 = 2, atunci sunt
posibile 6 ordini de execuţie. Executând lucrările în ordinea 1, 2, 3, lucrarea 1 aşteaptă până la terminarea
execuţiei ei 4 unităţi de timp, lucrarea 2 aşteaptă 4+14 unităţi de timp, iar 3, 4+14+2. Timpul total în acest caz
este 42. Executând lucrările în ordinea 3, 1, 2, obţinem un timp total de aşteptare 28 care este si timpul minim.
            Arătaţi că strategia greedy de selectare la fiecare pas a lucrării cu timpul de execuţie minim dintre
lucrările rămase este optimă.
14. (Interclasarea optimă a şirurilor ordonate) Fie n şiruri ordonate S1, S2, ..., Sn. Construiţi şirul ordonat T care
conţine exact elementele şirurilor S1, S2, ..., Sn. Realizaţi acest lucru prin interclasări succesive a câte două şiruri,
minimizând numărul total de operaţii (deplasări de elemente şi comparaţii).
            Indicaţie: Numărul maxim de operaţii efectuate la interclasarea a două şiruri S1 şi S2 este lungime(S1)+
lungime(S2). Astfel, pentru trei şiruri S1,  S2  şi S3 cu lungimile 30, 40 şi 20, dacă interclasăm S1 cu S2 şi apoi
rezultatul obţinut cu S3 obţinem un număr maxim de operaţii (30 + 40) + (70 + 20) = 160. În schimb, dacă
interclasăm S1 cu S3 şi apoi rezultatul obţinut cu S2 obţinem un număr maxim de operaţii (30 + 20) + (50 + 40) =
140.
            Arătaţi că strategia greedy de selectare la fiecare pas (n-1 paşi în total) a două şiruri de lungime minimă
este optimă.
15. Pe o bandă magnetică sunt n programe. Pentru fiecare program i (1  i  n) de lungime li se cunoaşte
cunoaşte probabilitatea pi cu care poate fi apelat (cerut), p1 + p2 + ... + pn = 1. Pentru a citi un program trebuie
să citim banda de la început (secvenţial). În ce ordine să memorăm programele pentru a minimiza timpul
mediu de citire a unui program oarecare?
            Indicaţie: Se pun în ordinea descrescătoare a rapoartelor pi / li.
16. (Selectarea activităţilor) Fie n activităţi identificate în cele ce urmează prin numerele 1, 2, ..., n. Aceste
activităţi doresc să folosească aceeaşi resursă R (de exemplu o sală). La un moment de timp dat resursa R
poate fi folosită de o singură activitate. Fiecare activitate are un timp de pornire si şi un timp de oprire fi, unde
si  fi. Dacă este selecţionată activitatea i atunci ea se desfăşoară pe durata intervalului [si, fi). Spunem că
activităţile i şi j sunt compatibile dacă intervalele [si, fi) şi [sj, fj) sunt disjuncte (nu se intersectează). Problema
selectării activităţilor constă din selectarea unei mulţimi maximale de activităţi mutual compatibile.
            Indicaţie: Presupunem că activităţile sunt ordonate crescător în raport cu timpul de terminare: f1  f2
 ...  fn.
Subalgoritmul SelecteazăActivităţi(n, s, f, A) este:                                       {s[i]  f[i] şi f[1]  ...  f[n]}
                                                                                                                                {A muţimea activităţilor
selectate}
A := {1}                                                                  {Activitatea 1 "trebuie" selectată.}
j := 1                                                                        {indicele activitaţii selectate ce are timpul de terminare cel mai
mare}
Pentru i = 2, n execută                                       {Considerăm fiecare activitate în ordinea terminării lor...}
                Dacă si  fj                                              {Poate începe activitatea i în raport cu timpul maxim de }
                     atunci A := A  {j}                          {terminare a activităţilor deja selectate?}
                                  j := i                                                                        {Reţine "ultima" activitate selectată (cu timpul de
terminare maxim)}
                sfdacă
sf-SelecteazăActivităţi
            (a) Demonstraţi corectitudinea acestui algoritm Greedy.
            (b) Scrieţi un algoritm Greedy pentru cazul în care activităţile nu sunt ordonate în raport cu timpul de
terminare.
17. La un concurs de automobilism de la linia de start până la final sunt plasate n staţii de benzină (la diferite
distanţe). Având rezervorul plin, maşina unui concurent poate parcurge cel mult o distanţă d (în kilometri).
Concurentul doreşte să se oprească de cât mai puţine ori şi desigur să parcurgă întreg drumul de la punctul de
start la cel final. Descrieţi o metodă eficientă pe care trebuie să o aplice concurentul şi arătaţi că strategia
respectivă conduce la o soluţie optimă.
IV. Aplicaţi metoda programării dinamice pentru rezolvarea problemelor următoare:
18. Se consideră două cuvinte a şi b cu m şi respectiv n litere. Să se transforme cuvântul a în cuvântul b
utilizand trei operaţii elementare:
            a - adăugarea unei litere,
            m - modificarea unei litere
            s - ştergerea unei litere
Efectuaţi transformarea printr-un număr minim de operaţii.
19. Fie o listă de n cuvinte. Să se formeze cel mai lung şir în care fiecare cuvânt începe cu litera cu care se
termina predecesorul său.
20. Se consideră un triunghi de numere naturale format din n linii. Prima linie conţine un număr, a doua două
numere, ultima n numere naturale. Cu ajutorul acestui triunghi se pot forma sume de numere naturale astfel:
- se porneşte cu numărul din linia 1;
- succesorul unui număr se află pe linia următoare plasat sub el (aceeaşi coloana) sau pe diagonala la dreapta
(coloana creşte cu 1)
Care este cea mai mare sumă care se poate forma astfel şi care sunt numerele care o alcatuiesc?
21. Fie şirul de numere a1, a2, ..., an (n > 0). Determinaţi cel mai lung subşir crescător al şirului a.
22. Fie şirurile de a1, a2, ..., am (m > 0) şi b1, b2, ..., bn (n > 0). Determinaţi cel mai lung subşir comun al şirurilor a
şi b.
Metoda Greedy
Problema 1. (Submulţime maximă). Se dă o mulţime X={x1, x2, …,xn} cu elemente reale. Se cere să se
determine o submulţime Y a sa astfel incatâ suma elementelor acestei submulţimi să fie maximă.

Problema 2. (Problema rucsacului *). O persoană are un rucsac cu care pot transporta o greutate maximă
M. Persoana are la dispoziţie n obiecte şi cunoaste pentru fiecare obiect greutatea şi câştigul care se
obţine în urma transportului său la destinaţie. Se cere să se precizeze ce obiecte trebuie să transporte
persoana în aşa fel încât câştigul să fie maxim.
*
Dacă obiectele pot fi divizate pentru a le transporta, atunci problema poartă numele de problema continuă a
rucsacului (deoarece valorile apar continuu). În acest fel se poate obţine o încărcare mai eficientă a rucsacului.
Dacă obiectele nu pot fi divizate pentru a le transporta, atunci problema poartă numele de problema discretă a
rucsacului (deoarece valorile apar discret). În acest caz s-ar putea ca rucsacul să nu fie încărcat cu eficienţă maximă.

Problema 3. (Memorarea optima a textelor pe benzi). Fiind date n texte de lungimi L1, L2, …, Ln. şi m benzi
magnetice, se cere poziţionarea optimă a textelor pe aceste benzi, astfel încât timpul de citire a unui text
oarecare de pe benzi să fie minim (ori de câte ori este nevoie de un text, sunt citite toate textele aflate
înaintea lui pe bandă şi bineînţeles textul respectiv). Se presupune că frecvenţa de citire a textelor este
aceeaşi.

Problema 4. (Interclasarea optimă a mai multor şiruri ordonate).

Se dau n şiruri S1, S2,…,Sn de lungimi L1,L2,…,Ln . În cadrul fiecărui şir elementele sunt ordonate crescător,
şirul S conţinând exact elementele din cele n şiruri. Acest lucru se va realiza făcând succesiv.

Problema 5. (Problema spectacolelor). Într-o sală, într-o zi, trebuie planificate n spectacole. Pentru
fiecare spectacol se cunoaşte intervalul în care se desfăşoară: [st, sf]. Se cere să se planifice un număr
maxim de spectacole astfel încât să nu se suprapună.

Problema 6. (Maximizarea valorilor unei expresii). Se dau n numere întregi nenule a1, a2,...,an şi m
numere întregi nenule c1,c2,...,cn Să se determine o submulţime B a mulţimii A={a1, a2,..., an}

Care să maximizeze valoarea expresiei:

E=c1x1+c2x2+...+cnxn,,

Ştiind că nm şi xi { a1, a2,..., an }  i  { 1,2,...,n}.

Problema 7. (Staţia de servire). O singură staţie de servire (procesor, pompă de benzină etc.) trebuie să
satisfacă cererile a n clienţi. Timpul de servire necesar fiecărui client este cunoscut în prealabil: pentru
clientul i este necesar un timp ti, i=1,2,...,n. Dorim să minimizăm timpul total de aşteptare
n
T =∑
i=1 ( timpul de aşteptare pentru clientul i )

ceea ce este acelaşi lucru cu a minimiza timpul mediu de aşteptare, care este T/n. De exemplu; dacă trei
clienţi cu t1 =5, t2 =10, t3 =3, sunt posibile şase ordini de servire date de tabelul următor:

Ordinea

1 2 3 5+(5+10)+(5+10+3)=38
1 3 2 5+(5+3)+(5+3+10)=31
2 1 3 10+(10+5)+(10+5+3)=43
2 3 1 10+(10+3)+(10+3+5)=41

3 1 2 3+(3+5)+(3+5+10)=29  optim

3 2 1 3+ (3+10)+(3+10+5)=34

În primul caz, clientul 1 este servit primul, clientul 2 aşteaptă până este servit clientul 1 şi apoi este servit,
clientul 3 aşteaptă până sunt serviţi clientul 1, 2 şi apoi este servit. Timpul total de aşteptare al celor trei
clienţi este 38. Ordinea optimă de servire este 3, 2, 1.

Metoda BACKTRACKING

Problema 1. (Generarea aranjamentelor/funcţiilor* injective). Se citesc n şi k numere naturale. Se cere să


se genereze toate submulţimile mulţimii 1,2,...,n de k elemente. Două mulţimi cu aceleaşi elemente, la
care ordinea acestora diferă, sunt considerate distincte.
*
Matematic, prin aranjamente de n luate câte k se înţelege numărul aplicaţiilor injective de la mulţimea 1,2,...,k
la mulţimea 1,2,...,n

Problema 2. (Generarea partiţiilor unui număr natural). Se citeşte un număr natural n. Se cere, să se
tipărească toate modurile de descompunere a sa ca sumă de numere naturale.
Exemplu. Pentru n=4 avem: 4, 31, 22, 211, 13, 121, 112, 1111.
Observaţie. Ordinea numerelor din sumă este importantă. Astfel, se tipăreşte 112 dar şi 211 precum şi
121.

Problema 3. (Plata unei sume cu bancnote de valori date). Sunt disponibile n tipuri de monezi. Pentru
fiecare tip k=1,2,...,n, valoarea unei monezi este numărul natural a[k], iar numărul de monezi de acest tip
este nr[k]. Fiind dată o sumă de mani p, se cer toate modalităţile
În care ea poate fi achitată cu monezile disponibile.

Problema 4. (Metagrama). Să se scrie un program care, citind un cuvânt şi un număr natural cuprins între
1 şi lungimea , să afişeze toate anagramările obţinute din cuvânt, după eliminarea literei de pe poziţia
citită.

Problema 5. (Generarea elementelor unui produs carterian). Fie date m mulţimi


A1 ,A2 ,...,Am. unde pentru fiecare i1,2,...,m Ai= 1,2,...,ni . Se pune problema generării tuturor celor
n1 * n2 * ... * nm elemente ale produsului cartezian A1* A2*...* Am.
Problema 6. (Problema căsătoriilor stabile). Se consideră n fete care urmează să se căsătorească cu n
băieţi. Fetele şi băieţii îşi exprimă preferinţele unul faţă de altul prin numerele reale din intervalul [0, 1].
Preferinţa fetei i pentru băiatul j este dată de fb[i,j], iar preferinţa băiatului i pentru fata j este dată de
bf[i,j]; elementele matricilor fb şi bf sunt numere reale din intervalul [0, 1]. Băiatul ales de fată i are
numărul x[i] . Bineînţeles, vectorul x va fi un vector permutare. Costul căsătoriei fetei i cu băiatul x[i] este
fb[i, x[i]]*bf[x[i], i], iar costul general (care trebuie minimizat) este suma după i a acestor valori. Mai mult,
se cere ca cele n căsătorii să fie stabile, adică să nu existe (i,j) cu ij astfel încât fata i să prefere băiatul x[j]
băiatului x[i], iar băiatul x[j] să prefere fata i fetei j.

Problema 7. (Paranteze). Se dă numărul natural n0. Să se determine toate şirurile de n paranteze care se
închid corect.

Exemplu. n=6 ((( ))), ( )( )( ), (( )( )), ( )(( )), (( ))( ).

Problema 8. (Generarea partiţiilor unei mulţimi * ). Se consideră mulţimea 1,2,...,n. Se cer toate
partiţiile acestei mulţimi.

*Submulţimile A1 ,A2 , ..., Am ale unei mulţimi A constituie o partiţie a acesteia, dacă sunt disjuncte între ele (nu au
elemente comune) şi mulţimea rezultată în urma reunirii lor este A.

Problema 9. (Generarea tuturor submulţimilor unei mulţimi).

Să consideră mulţimea 1,2,...,n. Se cer toate submulţimile acestei mulţimi.

Problema 10. (Generarea combinărilor). Se citesc n şi k numere naturale, cu n mai mare sau egal cu k. Se
cere să se genereze toate submulţimile cu k elemente ale mulţimii 1,2,...,n. Două mulţimi se consideră
egale dacă şi numai dacă au aceleaşi elemente, indiferent de ordinea în care acestea apar.

Problema 11. (Generarea permutărilor*). Se consideră un număr natural n. Se cere să se genereze toate
permutările mulţimii 1,2,...,n . Două mulţi se consideră egale dacă şi numai dacă au aceleaşi elemente,
indiferent de ordinea în care acestea apar.
*
Menţionăm că orice permutare este alcătuită din toate elementele distincte ale mulţimii iniţiale aşezate eventual într-o
altă ordine. De fapt, o permutare a unei mulţimi A este o funcţie bijectivă definită pe A cu valori în A.

Problema 12. (Colorarea hărţilor). Fiind dată o hartă cu n ţări, se cer toate soluţiile de colorare a hărţii,
utilizând cel mult 4 culori, astfel încât două ţări cu frontieră comună să fie colorate diferit. Faptul că sunt
suficiente numai 4 culori pentru orice hartă să poată fi colorată a fost demonstrat.

Problema 13. (Problema comis-voiajorului). Un comis-voiajor trebuie să viziteze un număr de n oraşe.


Iniţial, acesta se află într-unul din ele, notat cu 1. Comis-voiajorul doreşte să viziteze toate oraşele fără să
treacă de două ori prin acelaşi oral iar la întoarcere să revină în oraşul 1. Cunoscând legăturile dintre oraşe,
se cere să se tipărească toate drumurile posibile pe care le poate efectua comis-voiajorul.

Problema 14. (Umplerea unei suprafeţe închise). Se dă o matrice binară (elementele ei au numai valorile 0
sau 1). Valorile 1 delimitează o anumită suprafaţă închisă în cadrul matricei (elementele aparţinând acestei
suprafeţe sunt marcate cu 0). Se dau de asemenea coordonatele
x şi y ale unui element al matricei semnificând un punct din interiorul acestei suprafeţe. Se cere
schimbarea valorilor 0 din suprafaţa închisă cu o altă valoare (colorarea suprafeţei închise).

Problema 15. (Problema fotografiei). O fotografie alb-negru este prezentată sub forma unei matrice
binare. Ea înfăţişează unul sau mai multe obiecte.

Problema 16. (Problema canibalilor şi misionarilor). Pe malul unei ape se găsesc c canibali şi m misionari.
Ei urmează să treacă apa, având la dispoziţie o barcă cu două locuri. Se ştie că, dacă atât pe un mal cât şi pe
celălalt, avem mai mulţi canibali decât misionari, misionarii sunt mâncaţi de canibali. Se cere să se scrie un
program care să furnizeze toate variantele de trecere a apei, în care misionarii să nu fie mâncaţi.

Probleme 17. (Drapele). Avem la dispoziţie 6 culori: alb, galben, roşu, verde, albastru, negru.

Să se precizeze toate drapelele tricolore care se pot proiecta, ţinând că trebuie respectate următoarele
reguli:

 orice drapel are culoarea din mijloc galben sau verde;


 cele trei culori de pe drapel sunt distincte.
Problema 18. (Attila şi regele). Un cal şi un rege se află pe o tablă de şah. Unele câmpuri sunt „arse”,
poziţiile lor fiind cunoscute. Calul nu poate călca pe câmpuri „arse”, iar orice mişcare a calului face ca
respectivul câmp să devină „ars”. Să se afle dacă există o succesiune de mutări permise (cu restricţiile de
mai sus) prin care calul să poată ajunge la rege şi să revină la poziţia iniţială. Poziţia iniţială a calului precum
şi poziţia regelui sunt considerate „nearse”.

Problema 19. (Problema discretă a rucsacului). Cu ajutorul unui rucsac având

Capacitatea maximă admisibilă G trebuie să se transporte o serie de obiecte din n disponibile, având
greutăţile g1 ,g2 , ..., gn , aceste obiecte fiind de utilităţile c1 ,c2 , ... ,cn . Presupunând că obiectele nu por fi
luate decât în întregime (neputându-se diviza), să se găsească o modalitate de umple optim rucsacul.

Problema 20. (Labirint). Se dă un labirint sub forma unei matrice binare în care unităţile corespund
spaţiilor pe unde se poate trece, iar zerourile corespund zidurilor. Un şoricel pus într-o anumită căsuţă a
labirintului va trebui să ajungă într-o altă căsuţă a labirintului, unde se află o bucăţică de caşcaval. El se
poate mişca doar ortogonal, nu şi diagonal.

Problema 21.(Puncte albe şi negru). Se dau n puncte albe şi n puncte negru în plan, de coordonate
întregi. Fiecare punct alb se uneşte cu câte un punct negru, astfel încât din fiecare punct, fie alb sau negru,
pleacă exact un segment.

Să se determine o astfel de configuraţie de segmente aşa încât oricare două segmente să nu se


intersecteze. Se citesc n perechi de coordonate corespunzând punctelor albe şi n perechi de coordonate
corespunzând punctelor negre.

Problema 22. Fiind dat un număr natural pozitiv n, se cere să se producă la ieşire toate descompunerile
sale ca sumă de numere prime.
Problema 23. (Colorarea hărţilor). Fie dată hartă cu n ţări. Presupunând că dispunem de un număr de s
culori, se cere să se determine toate variantele de colorare a hărţii, care să respecte condiţia ca orice două
ţări vecine (care au frontieră comună) să fie colorate diferit. Se cere varianta iterativă a algoritmului.

Problema 24. (Figuri conexe). Se consideră un caroiaj dreptunghiular cu m linii şi n coloane, în care
anumite celule sunt ocupate. Două celule libere sunt considerate ca făcând parte din aceeaşi componentă
conexă dacă există între ele un drum format numai din celule libere şi în care două celule consecutive sunt
vecine pe orizontală sau pe verticală. Se cere să se afle componentele conexe şi numărul lor.

Problema 25. se consideră un caroiaj dreptunghiular cu m linii şi n coloane, în care anumite poziţii sunt
ocupate (interzise), precum şi o poziţie iniţială (i0, j0), considerată liberă. Se cere să se determine pentru
toate poziţiile la care poate ajunge un mobil ce pleacă din pleacă din punctul iniţial (i0, j0), distanţa lor faţă
de acest punct, măsurată în deplasări elementare. Se precizează că o deplasare elementară a mobilului
constă în repoziţionarea sa:

- cu o poziţie la dreapta pe aceeaşi linie;


- cu o poziţie la stângă pe aceeaşi linie;
- cu o poziţie în jos pe aceeaşi coloană,
bineînţeles dacă noua poziţie este liberă.

Metoda programării dinamice


Problema 26. (Subşir crescător de lungime maximă). Se consideră un vector de n elemente întregi. Se
cere să se tipărească cel mai lung subşir crescător al acestuia.

Problema 27. (Determinarea drumurilor de cost minim într-un graf). Se consideră un graf orientat dat
printr-o matrice A, numită matricea costurilor ataşată grafului, astfel:

A(i, j)=¿{c, dacă vârfurile i şi j sunt unite cu o muchie de cost c(c>0);¿{0, dacă vârfurile i şi j coincid,¿{∞,dacă vârfurile i şi j nu sunt unite printr−o muchie.¿ ¿ ¿
¿
Se cere ca pentru fiecare pereche de vârfuri (i, j) să se tipărească lungimea drumului minim de la i la j.

Problema 28. (Înmulţirea optimă a unui şir de matrice). Să presupunem că vrem să calculăm produsul A1 *
A2 *...*An unde pentru fiecare i 1,2,...,n, A1 este o matrice de dimensiuni

(d1, di+1). Asociativitatea înmulţirii a două matrici ne oferă mai multe posibilităţi de a calcula produsul A1*
A2 * ...* An, posibilităţi care nu necesită însă acelaşi număr de operaţii. Se pune problema de a determina o
posibilitate de asociere a înmulţirii matricilor astfel încât numărul de operaţii să fie minim.

Problema 29. (Investiţii). Se consideră un număr natural s semnificând o sumă de bani. Această sumă
poate fi investită în k secţii aparţinând aceleiaşi firme. Beneficiile anuale care se pot obţine sunt date de
tabelul următor:
Suma Secţia 1 2 ... k

0 b01 b02 ... b0k

1 b11 b12 ... b1k

2 b21 b22 ... b2k

... ... ... ... ...

s bs1 bs2 ... bsk


Se cere să se precizeze beneficiul maxim care se poate obţine, precum şi repartiţia pe secţii a sumei.

Problema 30. Pentru suma de 3 milioane lei ce se poate investi în trei secţii cu un beneficiu anual dat de
tabelul următor (unde sumele sunt exprimate în milioane lei):

Suma Secţia 1 2 3

0 0 0 0

1 0.1 0.2 0.3

2 0.2 0.3 0.4

3 0.3 0.4 0.5

Soluţia optimă constă în a investi 0 milioane în secţia 1, i milion în secţia 2 şi 2 milioane în secţia 3. În acest
mod se obţine un beneficiu anual de 0,6 milioane lei.

Problema 31. (Problema patronului). Un patron a cumpărat un calculator şi doreşte să înveţe să lucreze
pe el. Pentru aceasta va umple un raft de cărţi din colecţia „Informatica în lecţii de 9 minute şi 60
secunde”. Raftul are lungimea L cm (L este număr natural). Seria dispune de n titluri 1, 2, ... ,n având
grosimile g1 ,g2, ... , gn cm (numere naturale).

Să se selecteze titlurile pe care le va cumpăra patronul, astfel încât raftul să fie umplut complet (suma
grosimilor cărţilor cumpărate să fie egală cu lungimea raftului) şi numărul cărţilor achiziţionate să fie
maxim.

Observaţii: 1) Într-un raft cărţile pot fi aşezate doar vertical.

2) 1 n  60 1 L  200.

Date de intrare: Fişierul de intrare (cu numele citi de la tastatură), are următoarea structură:

În linia 1 : L

În linia 2: g1 ,g2, ... ,gn.

Date de ieşire: rezultatele se vor găsi în fişierul OUTPUT.TXT care va conţine două linii:

În linia 1: numărul de cărţi cumpărate;

În linia 2: grosimile lor.

Observaţii. 1) dacă problema nu are soluţie se va scrie în OUTPUT.TXT mesajul: „NU”.

2) Dacă problema are mai multe soluţii optime, fişierul OUTPUT.TXT va conţine una dintre ele.

3) datele conţinute în fişierul de intrare sunt corecte.

Exemple: 1) Dacă fişierul INPUT.TXT conţine:

10

3 7 1111111
Fişierul OUTPUT.TXT va conţine:

31111111

2) Dacă fişierul INPUT.TXT conţine:


7

356

Fişierul OUTPUT.TXT va conţine:

NU

Metoda divide et Impera.


Problema 32. (Maxim). Se citeşte un vector cu n componente, numere naturale. Se cere să se tipărească
valoarea maximă utilizând strategia Divide et impera.
Problema 33. (Quicksort). Fie vectorul v cu n componente numere întregi (sau reale). Se cere ca vectorul
să fie sortat crescător utilizând metoda sortării rapide.

Problema 34. (Problema tăieturilor). Se dă o bucată de tablă de formă dreptunghiulară cu lungimea l şi


înălţimea h, având pe suprafaţa ei n găuri de coordonate numere întregi. Se cere să se decupeze din ea o
bucată de arie maximă care nu prezintă găuri. Sunt permise numai tăieturi orizontale şi verticale.

Problema 35. Scrieţi un program în care calculatorul să ghicească un număr natural ales de
dumneavoastră( numărul este cuprins între 1 şi 30.000). Atunci când calculatorul vă propune un număr îi
veţi răspunde prin:
1, dacă numărul este prea mare;
2, dacă numărul este prea mic;
0, dacă numărul a fost ghicit.

Problema 36.(Plieri). Se consideră un vector cu n componente, numere naturale. Definim plierea


vectorului ca fiind suprafaţa unei jumătăţi, numită donatoare, peste o alta, numită receptoare. În cazul în
care vectorul are un număr impar de componente, cea din mijloc este eliminată. În acest fel se ajunge la un
vector ale cărui elemente au numerotarea jumătăţii receptoare.
Exemplu: Vectorul 1, 2, 3,4, 5 se poate plia în două moduri: 1,2 şi 4,5. Plierea se aplică în mod repetat
până se ajunge la un vector cu o singură componentă, iar conţinutul ei se numeşte element final. Să se
precizeze care sunt elementele finale şi care este şirul de plieri prin care se poate ajunge la ele.

Problema 37. Se dă un vector cn n componente la început nule. O secţiune pe poziţia k va incrementa


toate elementele aflate în zona de secţionare anterioară situate între poziţia 1 şi k.

Exemplu.
0 0 0 0 0 0 0 se secţionează pe poziţia 4;
1 1 1 1 0 0 0 se secţionează pe poziţia 1;
2 1 1 1 0 0 0 se secţionează pe poziţia 3;
3 2 2 1 0 0 0 etc.
Să se determine o ordine de secţionare a unui vector cu n elemente astfel încât suma elementelor sale să
fie s. Valorile n şi s se citesc.

Metoda BRANCH and BOUND.


Problema 38. (Problema mutărilor). Se consideră un vector cu 2x(n-1) cifre. Primele n poziţii sunt
ocupate cu cifre de 1, poziţia n+1 conţine cifra 0 şi ultimele n poziţii sunt ocupate cu cifre de 2. Ştiind că
cifra 0 îşi poate schimba locul cu orice cifră aflată la cel mult două poziţii distanţa de ea, se cere să se
plaseze pe primele n poziţii cifrele 2 şi pe ultimele n poziţii cifrele 1.
Problema 39. (Problema pătratului). Se consideră un pătrat cu nxn căsuţe. Fiecare căsuţă conţine un
număr natural între 1 şi nx(n-2). Două căsuţe sunt ocupate cu numărul 0. Fiecare număr natural, diferit de
0, apare o singură dată în cadrul pătratului. Ştiind că 0 îşi poate schimba locul cu orice număr natural aflat
deasupra, la dreapta, la stânga sau jos, în raport cu poziţia în care se află numărul 0, se cere să se precizeze
şirul de mutări prin care se poate ajunge de la o configuraţie iniţială la o configuraţie finală. Se cere de
asemenea ca acest şir de mutări să fie optim, în sensul că trebuie să se ajungă la configuraţia finală print-un
număr minim de mutări.

I=¿(3 2 0¿) (1 0 4¿) ¿¿¿


Exemplu. Pentru n=3 se dă configuraţia iniţială ¿ ; se
F=¿(1 2 3¿)( 4 5 6¿)¿¿¿
Cere configuraţia finală ¿ .
Algoritmi probabilistici.

Problema 40. Arătaţi cun poate fi aplicat stilul Sherwood de aproximare probabilistică pentru sortarea
rapidă.

Problema 41. (Eliminarea tuturor vârfurilor unui graf). Fie dat un graf neorientat. Prin mutare înţelegem
eliminarea din graf a unui vârf împreună cu toţi vecinii săi. Se cere să se furnizeze o secvenţă cu număr
minim de mutări care are drept rezultat eliminarea tuturor vârfurilor grafului.

Algoritmi genetici.

Problema 42. Rezolvaţi problema comis-voiajorului folosind algoritmi genetici (vezi problema

Problema 43.Rezolvaţi problema planificării lucrărilor pe mai multe procesoare utilizând algoritmi genetici
(vezi problema )

Problema 44. (Problema orarului). Să se elaboreze orarul unei şcoli folosind algoritmi genetici.

Bibliografie

4. Tudor Sorin, Tehnici de programare, Editura L&S Infomat, Bucureşti, 1998


5. Manual de programare C, (după Kernigham şi Ritchie) Microinformatica, Cluj-Napoca, 1986
6. Muşlea I., Programarea în C, Microinformatica, Cluj-Napoca, 1992
7. Roger Penrose, Mintea noastră…cea de toate zilele, (titlul original: Emperor's mind), Editura
tehnică, Bucureşti, 2001
8. Roger Penrose, Incertitudinile raţiunii. Umbrele minţii, (titlul original: Shadows of the mind),
Editura tehnică, Bucureşti, 2000
9. Keith Devlin, Vîrsta de aur a matematicii, (titlul original: Matemathics: The New Golden Age),
Editura Thetha, Bucureşti, 2000
10. Solomon Marcus, Gîndirea algoritmică, Editura tehnică, Bucureşti, 1982
11. L. Livovschi, H. Georgescu, Bazele informaticii, Editura didactică şi pedagogică, Bucureşti, 1981

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