Sunteți pe pagina 1din 32

Alegerea strategiei de proiectare a produselor program

La Constructia produselor program deseori putem intilni cazuri problematice, cind exista mai multe solutii posibile,
iar nici o solutie nu este “corecta” sau “gresita”, ci numai “mai buna” sau “mai rea” luindu-se in seama gradul curent
de intelegere a problemei. Adaugator fiecare astfel de caz problematic este esential unic, fara reguli explicite de
solutionare, iar proiectantul va fi responsabil pentru consecintele solutiei alese.
Cu acest model de gindire despre proiectarea program vor exista 2 variabile care vor determina calea spre solutia de
proiectare:
- Gradul de intelegere a domeniului problemei
- Gradul de intelegere a spatiului solutiei?
In lumea produselor program, intelegerea domeniului problemei inseamna gindirea despre constringeri tehnice,
cerintele functionale, atribute de calitate si constringeri de afacere. Intelegerea constringerilor este critic importanta
fiindca ele vor defini unele limite a spatiului de solutii. Cerintele functionale vor avea de a face cu ce va efectua
sistemul, iar atributele de calitate vor avea de a face cu comportamentul sistemului la la executarea unor functii date.
In final constringerile de afacere sunt niste lucruri pe care clientul le cere si care necesita o solutie pur si simplu din
considerente de afacere. De exemplu acestea pot fi bugetul si data de expediere.
Spatiul de solutii este un teren multi-dimensional care este ocupat de posibilitati practic nelimitate. Ca un proiectant
lucrul vostru consta in navigarea in acest spatiu in cautarea solutiilor problemei. Intelegerea curenta a problemei
limiteaza abilitatea de a vedea solutiile si deseori corespondenta solutiei poate fi inteleasa doar in referinta cu alta
solutie posibila. Aceasta implica ca intelegerea curenta a problemei dicteaza intelegerea solutiilor potentiale, iar
intelegerea solutiei va aproviziona informatii adaugatoare despre problema. Aceasta este o pricina de ce este deseori
bun de a avea produs program gata care lucreaza in pentru prezentarea rapida in fata utilizatorilor.

Figura 1 – Graficul cunostintelor despre proiectare fata de cunostintele din domeniul problemei
La proiectarea a oricaror sisteme avem o tendinta naturala de a prefera sa facem decizii rationale de proeictare, acolo
unde dupa examinare a tutoror informatiilor noi alegem cea mai buna sau mai optimizata solutie. Efectuarea alegerei
rationale necesita nu doar cunoasterea a tutoror informatiilor din domeniul problemei, dar si a tuturor solutiilor
posibile. Atit constringerile de afacere, prin constringeri financiare si temporare, si deficientele de limita a creierului
uman limiteaza capacitatea noastra de a cauta aceste cunostinte prin crearea limitelor in jurul solutiei cautate.
Cautarea solutiei nu consta in optimizare ci in corespondenta solutiei maxim de bune cu informatia curent
disponibila. Lucrurile pot fi complicate deoarece indivizii pot intelege domeniul problemei si spatiul de solutii in
mod diferit, depinzind de experienta si capacitatile individualului, marimea problemei si complexitatea solutiei.
La proiectarea produselor program este natural de a dori sa ne miscam cit mai departe pe curba cunostintelor, pentru
a obtine proiectul cit mai mult de rational si optimal. Aceasta tendinta reiese din preferinta instinctuala de a evita
pierderi. In alte cuvinte, proiectantii asuma ca solutia optimala exista si sunt mai dispusi sa rejecteze solutiile mai
putin optimale.
Alegerea abordarii de proiectare produselor program: Aplicare
In proiectarea produselor program exista 4 tipuri de baza a strategiilor de proiectare:
- Proiectare planificata: proiectarea este completata inainte de a incepe implementarea. Asumptiile
esentiale consta in faptul ca este posibil de proiecta un sistem complet inainte de a incepe constructia.
- Proectare cu planificare minima: o parte din proiectare este completata inainte de a incepe
implementarea. Folosind aceasta strategie, noi intelegem ca numaidecit vor aparea schimbari, dar dorim sa
schitam directiile de baza.
- Proiectare evolutionara: Proiectul sistemului creste pe masura implementarii sistemului, dar cresterea este
sporadica si este controlata de ingineri cu experienta cu ajutorul practicilor cunoscute ca proiectare de
referinta si refactorizare. Cel putin o parte din cerintele sunt definite la inceput dar nu asteptate a fi
complete.
- Proiectare emergenta: proiectarea problemei este admisa de a avea loc in mod organic in timp ce sistemul
este implementata fara intentii specifice.

Figura 2 – Spectrul de Strategii de proiectare


Strategiile de proiectare corespunzatoare echipei date trebuie alese dupa cantitatea de riscuri aflata in graficul limitat
de cunostinte. Mai putin risc permite sa anticipam mai putine schimbari si mai multe optiuni de a alege strategia de
proiectare. Alegerea strategiei de proiectare va conditiona necesarul de planificare, impreuna cu preferintele echipei,
cunostintele curente despre domeniul problemei si spatiul solutiilor. Cu cit mai putin se cunoaste din domeniul
problemei cu atit mai putin se poate de a se planifica proiectul.
Concluzii: Ca strategii de dezvoltare in ingineria programarii care vor fi utilizate si mai inainte,
trebuie de considerat reingineria, reutilizarea, reconfigurarea si evolutia. Acestea se folosesc
pentru imbunatatirea performantelor sistemelor program, cu sustinerea cheltuielilor minime, prin
imbunatatirea arhitecturii sistemului, sau utilizarii a componentelor de program deja existente si
testate, care s-au dovedit a fi utile intr-un domeniu dat, necesitind integrare cu sistemul dezvoltat
pentru a atinge rezultate noi (de exemplu, biblioteci plug-in). De asemenea strategiile de
dezvoltare intotdeauna vor fi influentate de cunostintele din domeniile problemelor, eventual la
aparitia domeniilor noi de cunoastere neexplorate, cut atit mai putin vom putea planifica
proiectele in aceste domenii, deseori necesitind schimbari dese si proiectare mai putin bazata pe
planificare.

Cum se stabileste corectitudinea si eficienta solutionarii ?

          Este adevarat ca ultima etapa în rezolvarea unei probleme - implementarea - este decisiva
si doveditoare, dar primele doua etape au o importanta capitala. Ele sînt singurele ce pot oferi
raspunsuri corecte la urmatoarele întrebari dificile: Avem certitudinea ca solutia gasita este
corecta ? Avem certitudinea ca problema este complet rezolvata ? Cît de eficienta este solutia
gasita ? Cît de departe este solutia aleasa de o solutie optima ?

          Sa mentionam în plus ca literatura informatica de specialitate contine un numar


impresionant de probleme "capcana" pentru începatori, si nu numai pentru ei. Ele provin
majoritatea din realitatea imediata dar pentru fiecare dintre ele nu se cunosc solutii eficiente. De
exemplu, este dovedit teoretic ca problema, "aparent banala" pentru un calculator, a
proiectarii Orarului optimîntr-o institutie de învatamînt (scoala, liceu, facultate) este o problema
intratabila la ora actuala (toate programele care s-au realiza

          Daca ar fi sa sintetizam în cîte un cuvînt efortul asupra caruia se concentreaza fiecare din
cele trei etape - analiza, proiectarea si implementarea- cele trei cuvinte ar fi: corectitudine,
eficienta si impecabilitate. Etapa de analiza este singura care permite dovedirea cu argumente
riguroase a corectitudinii solutiei, iar etapa de proiectare este singura care poate oferi argumente
precise în favoarea eficientei solutiei propuse.

          În general problemele concrete din informatica au în forma lor initiala sau în enunt o
caracteristica pragmatica, fiind foarte ancorate în realitatea imediata. Totusi ele contin în
formularea lor initiala un grad mare de eterogenitate, diversitate si lipsa de rigoare. Fiecare
dintre aceste "defecte" este un obstacol major pentru demonstrarea corectitudinii solutiei. Rolul
esential al etapei de analiza este acela de a transfera problema "de pe nisipurile miscatoare" ale
realitatii imediate de unde ea provine într-un plan abstract, adica de a o modela. Acest "univers
paralel abstract" este dotat cu mai multa rigoare si disciplina interna, avînd legi precise, si poate
oferi instrumentele logice si formale necesare pentru demonstrarea riguroasa a corectitudinii
solutiei problemei. Planul abstract în care sînt "transportate" toate problemele de informatica este
planul sau universul obiectelor matematice iar corespondentul problemei în acest plan va
fi modelul matematic abstract asociat problemei. Demonstrarea corectitudinii proprietatilor ce
leaga obiectele universului matematic a fost si este sarcina matematicienilor. Celui ce
analizeaza  problema din punct de vedere informatic îi revine sarcina (nu tocmai usoara) de a
dovedi printr-o demonstratie constructiva ca exista o corespondenta precisa (o bijectie !) între
partile componente ale problemei reale, "dezasamblata" în timpul analizei, si partile componente
ale modelului abstract asociat. Odata descoperita, formulata precis si dovedita, aceasta "perfecta
oglindire" a problemei reale în planul obiectelor matematice ofera certitudinea ca toate
proprietatile si legaturile ce exista între subansamblele modelului abstract se vor regasii precis
(prin reflectare) între partile interne ale problemei reale, si invers. Atunci, solutiei abstracte
descoperite cu ajutorul modelului matematic abstract îi va corespunde o solutie reala concretizata
printr-un algoritm ce poate fi implementat într-un program executabil.

          Aceasta este calea generala de rezolvare a problemelor si oricine poate verifica acest fapt.
De exemplu, ca si exercitiu, încercati sa demonstrati corectitudinea (adica sa se aduca argumente
precise, clare si convingatoare în favoarea corectitudinii) metodei de extragere a radicalului
învatata înca din scoala primara (cu grupare cifrelor numarului în grupuri cîte doua, etc.) sau a
algoritmului lui Euclid de determinare a celui mai mare divizor comun a doua numere prin
împartiri întregi repetate. Desigur nu pot fi acceptate argumente copilaresti de forma:
"Algoritmul este corect pentru ca asa l-am învatat!" sau "Este corect pentru ca asa face toata
lumea !" din moment ce nu se ofera o argumentatie matematica riguroasa.

          Ideea centrala a etapei a doua - proiectarea unui algoritm de solutionare eficient poate fi
formulata astfel: din studiul proprietatilor si limitelor modelului matematic abstract asociat
problemei se deduc limitele inferioare ale complexitatii minimale ("efortului minimal
obligatoriu")inerente oricarui algoritm ce va solutiona problema în cauza. Complexitatea
interna a modelului abstract  si complexitatea solutiei abstracte se va reflecta imediat
asupracomplexitatii reale a algoritmului, adica asupra eficientei de solutionare a problemei.
Acest fapt permite prognosticarea înca din aceasta faza - faza de proiectare a algoritmului de
solutionare - a eficientei practice, masurabila ca durata de executie, a programului.

Noţiunea de corectitudine

Un program este corect dacă el satisface specificaţiile problemei. Nu ne interesează câtă


memorie foloseşte acest program, din câte instrucţiuni este compus, sau cât timp de execuţie
necesită. Cu alte cuvinte, un program este corect dacă pentru acele date de intrare care satisfac
specificaţiile problemei rezultatele obţinute în urma execuţiei sunt corecte.
Pentru orice program P deosebim trei tipuri de variabile, pe care le vom grupa în trei vectori X,
Y şi Z. Componentele vectorului X desemnează variabilele de intrare, deci datele presupuse
cunoscute în problema rezolvată prin programul P. Componentele vectorului Z sunt variabilele
care reprezintă rezultatele cerute de problemă. În sfârşit, componentele vectorului Y sunt
variabilele de lucru, care notează diferitele rezultate intermediare necesare în program.
O problemă nu are sens pentru orice date de intrare. Vom folosi predicatul φ(X) pentru a preciza
datele pentru care problema are sens. φ(X) se numeşte predicat de intrare sau precondiţie.
Pentru acele valori ale lui X pentru care predicatul este adevărat problema are sens, pentru
celelalte nu are sens să executăm programul P.
Între rezultatele Z ale problemei şi datele iniţiale X (cunoscute în problemă) există anumite
relaţii. Vom reda aceste relaţii prin predicatul de ieşire ψ(X,Z), numit şi postcondiţie. Acesta este
corect pentru acele valori a şi b ale vectorilor X şi Z pentru care rezultatele problemei sunt b în
cazul când datele iniţiale sunt a şi este fals în caz contrar. Deci, dacă executând programul cu
datele iniţiale a obţinem rezultatele b' şi ψ(a,b') este fals, acest fapt este un indiciu că rezultatele
obţinute în program nu sunt corecte

Definiţia 5.1.1. Spunem că cele două predicate, predicatul de intrare φ(X) şi predicatul de ieşire
ψ(X,Y), constituie specificaţia problemei. Cele două predicate se mai numesc precondiţie,
respectiv postcondiţie. Se subînţelege că pentru a cunoaşte această specificaţie trebuiesc
cunoscuţi şi vectorii X şi Z. Astfel, dacă în problemă se cere să se calculeze radicalul de ordinul
2 (notat prin r) dintr-un număr real pozitiv x, specificaţia problemei este dată prin predicatele:

φ(X): "x>0" (precondiţia)


ψ(X,Z): "r2=x" (postcondiţia)

Această specificaţie este corectă pentru un matematician, dar nu şi pentru un programator.


Numărul real r este în general transcendent şi nu poate fi reprezentat în calculator decât
aproximativ. Deci nu există procedeu finit prin care putem calcula pe r. Putem construi însă un
program care să aproximeze oricât de bine pe r şi va trebui să adăugăm acest lucru la enunţul
problemei, care devine: se cere să se calculeze r cu o eroare absolută maximă ε dată. În această
situaţie vectorul de intrare este X=(x,ε) iar specificaţia problemei va fi:

ϕ(X): "x>0 şi ε>0" ;


ψ(X,Z): "⏐r2-x⏐<ε" .

Se observă mai bine şi semnificaţia “preciziei” ε. Am ales ca el să majoreze diferenţa dintre r2


şi xîntrucât diferenţa dintre r şi valoarea exactă a radicalului e necunoscută. Nu puţine sunt
cazurile în care execuţia programului intră într-un ciclu infinit, deci nu se mai termină. Este un
caz nedorit, care trebuie evitat în programare. Referitor la corectitudinea programelor dăm
următoarele definiţii:

Definiţia 1. Spunem că programul P se termină în raport cu predicatul de intrare φ(X)


dacă pentru orice intrare a = (a1,a2,..., an) a vectorului X pentru care predicatul φ este adevărat,
execuţia lui P se termină.
Definiţia 2. Spunem că programul P este parţial corect în raport cu specificaţiile problemei dacă
pentru intrarea a pentru care φ(a) este adevărat şi execuţia programului se termină cu rezultatele
b=P(a) atunci ψ(a,b) este adevărat.
Definiţia 3. Spunem că un program P este total corect în raport cu specificaţiile problemei dacă
programul P se termină şi dacă el este parţial corect în raport cu aceste specificaţii. Există mai
multe metode de demonstrare a corectitudinii unui program. În continuare vom prezenta metoda
aserţiunilor inductive datorată lui Floyd, metodă care consideră algoritmii reprezentaţi prin
scheme logice. Subliniem că e vorba de corectitudinea logică a programelor şi nu de
corectitudinea lor sintactică. De aceea nu contează forma sub care e dat algoritmul: fie scris într-
o schemă logică, fie în Pseudocod, fie într-un limbaj de programare. În acest capitol prin
program ne referim la algoritmul corespunzător, indiferent de forma în care e scris. Metoda cere
alegerea unor puncte în schema logică, numite puncte de tăietură. Se va alege cel puţin un punct
de tăietură în fiecare buclă, un punct de tăietură la începutul schemei logice (dupăblocul de
citire) şi un punct de tăietură la sfârşitul schemei logice
În fiecare punct de tăietură se alege un predicat invariant. În punctul de tăietură de intrare
predicatul invariant este φ(X), iar în punctul de tăietură de ieşire predicatul invariant este ψ(X,Z).
Pentru o pereche (i,j) de puncte de tăietură pot exista mai multe drumuri de la i la j. Fie α un
astfel de drum cu proprietatea că el nu conţine un alt punct de tăietură. Vom nota prin Rα(X,Y)
predicatul care ne dă condiţia ca să se parcurgă acest drum şi prin rα(X,Y) funcţia care dă
transformarea variabilelor intermediare Y la traversarea drumului α. Schematic, putem
reprezenta notaţiile făcute astfel:

P(X,Y) Rα(X,Y) Pj(X,Y)


o──────────────────────────────────→o
i α j

Acestui drum i se asociază condiţia de verificare:


∀X∀Y (Pi(X,Y) ∧ Rα(X,Y) → Pj(X,rα(X,Y)))

Subliniem că între două puncte de tăietură pot exista mai multe drumuri directe distincte.
Floyd demonstrează următoarea teoremă:
Teorema 1. Dacă toate condiţiile de verificat sunt adevărate atunci programul P este parţial
corect în raport cu specificaţiile φ(X) şi ψ(X,Z). Ca exemplu de demonstrare a corectitudinii
unui program să considerăm următorul algoritm de calcul al celui mai mare divizor comun a
două numere date:
ALGORITMUL CMMDC1(n1,n2,d) ESTE:
DATE n1,n2; { A: φ(X)::= n1∈N, n2∈N}
FIE d:=n1;
FIE i:=n2;
CÂTTIMP (d≠i) ŞI (i>0) EXECUTĂ { B: PB::= (d,i)=(n1,n2)}
DACĂ d>i ATUNCI d:=d-i
ALTFEL i:=i-d
SFDACĂ
SFCÂT { C: PC= ψ(X,Z)::= d=(n1,n2)}
REZULTATE d;
SFALGORITM

Variabilele folosite, grupate în cei trei vectori X, Y, Z sunt:


X = (n1, n2), Y = (d, i) şi Z = (d). 115

Drumurile posibile în acest algoritm sunt: α1 = AB, α2 = BB şi α3 = BC, iar condiţiile de


parcurgere a acestor drumuri sunt:
Rα1(X,Y) = (d≠i) ∧ (i>0) ;
Rα2(X,Y) = (d≠i) ∧ (i>0) ;
Rα3(X,Y) = not [(d≠i) ∧ (i>0)] <=> (d=i) ∨ (i=0) .

Transformările pe aceste drumuri sunt date de:


rα1(X,Y) = (n1,n2) ;
rα2(X,Y) = Dacă d>i atunci (d-i,i) altfel (d,i-d);
rα3(X,Y) = rα2(X,Y).

Corespunzător acestor drumuri avem următoarele condiţii de verificare:


Cα1 = (n1≠n2) ∧ (0≠n2) → ( (n1,n2)=(n1,n2) ) ;
Cα2 = ((d,i)=(n1,n2)) ∧ (d≠i) ∧ (i≠0) →
((d>i) ∧ (d-i,i)=(n1,n2)) ∨
((d<i) ∧ (d,i-d)=(n1,n2)) ;
Cα3 = ((d,i)=(n1,n2)) ∧ ((d=i) ∧ (i=0))→( d=(n1,n2) ).
Se poate verifica uşor că toate aceste condiţii sunt adevărate.
Pentru a demonstra terminarea programelor vom folosi noţiunea de mulţime convenabilă.
Definiţia 4. O mulţime este convenabilă dacă ea este parţial ordonată şi nu conţine nici un
şir descrescător infinit. Exemple de mulţimi convenabile sunt mulţimea numerelor naturale N cu
relaţia "<" şi mulţimea NxN cu ordinea lexicografică:
(m1,n1)<(m2,n2) dacă (m1<m2) ∨ (m1=m2) ∧ (n1<n2).

Pentru a demonstra terminarea algoritmului va trebui să demonstrăm că anumite condiţii de


terminare au loc. Aceste condiţii spun că la trecerea prin schema logică (algoritm) de la un punct
de tăietură la altul valorile unor funcţii în mulţimea convenabilă aleasă scad. Întrucât nu putem
construi un şir (infinit) descrescător rezultă că nu vom trece de la un punct de tăietură la alt punct
de tăieturăde o infinitate de ori, deci execuţia este finită; cu alte cuvinte algoritmul se termină.
Prin urmare, în punctul de tăietură i se alege o funcţie ui: DX x DY → M, iar condiţia de
terminare pentru drumul α este:
∀X∀Y (φ(X)∧Rα(X,Y) → (ui(X,Y)>uj(X,rα(X,Y) )

În cazul în care s-a demonstrat parţial corectitudinea şi Pi(X,Y) a fost predicatul invariant în
punctul i, condiţia de terminare pentru drumul α care duce de la punctul i la punctul j se poate
lua: ∀X∀Y (Pi(X,Y)∧Rα(X,Y) → (ui(X,Y) > uj(X,rα(X,Y)) )
Pentru algoritmul CMMDC1 avem trei puncte de tăietură, pentru care alegem:
u1(X,Y) = 2*n1 + 2*n2;
u2(X,Y) = d + i;
u3(X,Y) = 0,
iar condiţiile de terminare corespunzătoare acestor drumuri sunt:
Tα1 = (n1≠n2)∧(n2>0) → (2*n1+2*n2 >n1+n2) ;
Tα2 = (PB∧(d>i)∧(i>0) → d+i>d) sau (PB∧(d<i)∧(i>0)→d+i>i) ;
Tα3 = True .

Se poate verifica că toate cele trei condiţii sunt adevărate pentru n1>0, dar Tα2 este falsă dacă
n1=0, întrucât d ia valoarea 0 şi inegalitatea i>i este falsă. În acest caz drumul α2 este parcurs de
oinfinitate de ori, deoarece variabilele d şi i nu-şi modifică valorile la parcurgerea acestui drum.
Pentru a obţine un algoritm total corect este necesară modificarea algoritmului CMMDC1 astfel
încât să nu se intre în drumul α2 dacă d=0. Obţinem următorul algoritm total corect:

ALGORITMUL CMMDC2(n1,n2,d) ESTE:


DATE n1,n2; { Punctul A: φ(X)::= n1∈N, n∈N }
DACĂ n1=0 ATUNCI FIE d:=n2; i:=0
ALTFEL FIE d:=n1; i:=n2;
SFDACĂ
CÂTTIMP (d≠i) ŞI (i>0) EXECUTĂ { Punctul B: PB::= (d,i)=(n1,n2) }
DACĂ d>i ATUNCI d:=d-i
ALTFEL i:=i-d
SFDACĂ
SFCÂT
{ Punctul C: PC= ψ(X,Z)::= d=(n1,n2)}
REZULTATE d;
SFALGORITM

Demonstrarea corectitudinii algoritmilor poate fi făcută după elaborarea lor, sau în timp ce sunt
concepuţi. Am văzut că ea presupune scrierea unor predicate invariante corespunzătoare
punctelor de tăietură, deci ea impune proiectantului respectarea unei discipline în programare.
Foarte probabil că aceste predicate invariante sunt sugerate de semnificaţiile variabilelor. Deci
este necesar ca fiecare variabilă să aibă semnificaţia ei şi să nu fie folosită în scopuri diferite.
Gries spunea: Proiectarea programului şi demonstrarea corectitudinii lui trebuie făcute în paralel.
Într-o astfel de proiectare convingerea programatorului că a conceput un algoritm corect creşte,
iar scrierea predicatelor invariante reduce numărul erorilor în programare. De asemenea, dacă s-
ar încerca demonstrarea corectitudinii algoritmilor descrişi, multe erori ar fi descoperite şi
eliminate în faza de proiectare.
Pornind de la această afirmaţie vom exemplifica în secţiunea următoare cum putem obţine
algoritmi corecţi prin rafinări succesive, pornind din specificaţii. Rezultă din exemplele date că
în prim plan este demonstraţia corectitudinii, propoziţiile care compun algoritmul fiind o
consecinţă a acestei demonstraţii.

Verificarea modelelor
Verificarea modelelor este o tehnică de verificare formală (semi) automată bazată pe modele şi
având o abordare orientată pe verificarea proprietăţilor. Explorează toate execuţiile posibile ale
sistemului, aşadar este mai “puternică” decât testarea sau tehnicile de analiză bazate pe simulari.
Această tehnică de verificare este alcatuită din trei parţi distincte:

 Un cadru general pentru specificarea sistemelor reale cuprinzând un limbaj de descriere


cât mai evoluat ( sisteme concurente formale (FCS) = sintaxă , sisteme tranziţionale (TS) =
semantică )
 Un limbaj pentru specificarea proprietăţilor
 O metodologie prin care se poate stabili dacă o anumită descriere a unui sistem satisface
o anumită proprietate.

Modelele pot fi create manual folosind limbaje de modelare particularizate pentru domeniul de


aplicabilitate. Ţinta verificatorului de modele este de a stabili dacă modelul satisface o anumită
proprietate deobicei exprimată într-o formă de logică temporală; în caz de eşec produc erori.

Vasta cercetare în verificarea modelelor a analizat şi aspecte cu comportament discret, cum ar fi


non-determinismul şi concurenţa. Noile realizări tehnologice ( tendinţa accentuată pentru unităţi
tehnologice mobile, portabile, adaptabile, auto-organizatorice ) au ridicat substanţial profilul
probabilistic al modelării şi verificării tehnologiilor software. Opţiuni precum “real – time “ şi
probabilitatea sunt deja folosite în protocoale distribuite precum Bluetooth , IEEE 802.11 ( WiFi
protocol ).

Randomizarea este cheia pentru obţinerea soluţiilor distribuite simetric , protocoale auto-


configurabile, sisteme auto-organizabile, algoritmi toleranţi la erori şi protocoale scalabile.

Probabilitatea joacă un rol important în modelarea incertitudinii, în planificarea şi luarea


deciziilor şi în analiza performanţelor şi dependenţelor.

Analiza si verificare

Analiza programelor e legata tot mai mult de verificarea formala.


Verificarea formala: stabileste ca un sistem e corect prin analiza riguroasa a unui model
matematic al sistemului
- in general, proprietati specice, detaliate despre comportament (ex.dupa evenimentul A apare
evenimentul B etc.)
- necesita in principiu analiza (simbolica) a secventelor de executie amodelului (explorarea
spatiului starilor)

Analiza statica: bazata tot pe tehnici matematice, riguroase


- de regula pentru proprietati mai generale
- folosind aproximatii sigure (conservatoare)
- de regula nu exploreaza spatiul starilor programului

Caracteristici si principii de baza

Analiza programelor: tehnici pentru prezicerea statica, la compilare a multimii comportamentelor


dinamice (la rulare) ale programului [Nielson & Nielson]

In general, o analiza precisa e nedecidabila (v. Church, Godel, Turing)

- analiza trebuie sa faca aproximatii


- dar trebuie sa fie sigura (sa corespunda semanticii programului, sisa nu omita situatii posibile /
erori.

Din punct de vedere practic:


- sucient de precisa (cu minim de avertismente false)
- ecienta (spatiu/timp) pentru a trata programe de dimensiuni realiste

Exemplu: calcule de reglare (filtre digitale)

BOOLEAN init; float P, X;


void filter () {
static float E[2], S[2];
if (INIT) S[0] = P = E[0] = X;
else P = ((((0.5 * X - E[0] * 0.7)+ E[1] * 0.4) + S[0] * 1.5) - S[1] * 0.7);
E[1] = E[0]; E[0] = X; S[1] = S[0]; S[0] = P;}
void main () {
X = 0.2 * X + 5;
INIT = TRUE;
while (1) {X = 0.9 * X + 35;
filter ();
INIT = FALSE;} }
Problema: demonstrarea absentei depasirilor, si raminerea lui P intr-uninterval dat (in acest caz,
[-1327.05, 1327.05] )tehnici de analiza pe intervale de valori, cu specific de teoria reglarii.

Exemplu: detectia statica a erorilor

Clase de erori frecvente in programe:


- folosirea variabilelor neinitializate
- dereferentierea de pointeri nuli
- depasirea limitelor de indici in tablou

Aceste erori pot detectate prin analiza statica a codului sursa


ex. Splint (U. Virginia) sau UNO (Bell Labs) pt. C sau ESC/Java(Compaq SRC)

int *p = malloc(100 * sizeof(int));


if (p != NULL) printf("%d", p[100]);
-------------
splint +bounds pointer.c
pointer.c:7:18: Array element p[100] used before definition
pointer.c:7:18: Possible out-of-bounds read: p[100]
Problema: analize in acelasi timp precise (fara multe alarme false) si scalabile la programe de
dimensiuni mari.

Concluzie

Modelul conceptual pentru verificarea corectitudenii programului:

1.2. 1.2.
1 2 1.3.
1
1.3.
1.1.
4 2

1.1.
3
1.2. De
folosire
1.1. 1.3. Auto- 2.5.
2 documentare 4
e
2.5.
1.1. 1.1 De
3
1 realizare

1.Documentar 2.5.
2.5. 2
e
Validare
2.5.
2.1. 1
1

2.1. Corectitudinea
2 2.1. Algoritmului
Inspectare 2.4.
Programului 5
2.1.
3
2.4.
2.4. 4
2.Programare
Verificare
2.4.
2.2. 3
1
2.4.
2.2. 2
2.2.
Testare
2 2.4.
2.3. 1
2.2. Depanare
3
2.2.
4 2.3.
3
2.3. 2.3.
1 2

Descrierea schemei conceptuale:

Schema data reprezinta modelul verificarii corestitudenii algoritmului pentru un program

Ca strategie globala aceasta verificare are 2 compartimente:


1. Documentarea
2. Programarea

Orice strategie va fi realizata conform unor anumite tactice/tehnici. Fiecare punct strategic, la rindul
sau, are anumite tactici, pe care le voi descrie mai jos.

1. Documentarea
1.1. De realizare
1.2. De folosire
1.3. Auto-documentare

2. Programarea
2.1. Inspectarea
2.2. Testarea
2.3. Depanarea
2.4. Verificarea
2.5. Validarea

Orice tactica isi are anumite obiective, care sunt necesare pentru realizarea sarcinilor puse.
In asa mod, eu am ajuns la urmatoarele obiective:

1. Documentarea :
1.1. De realizare
1.1.1. Enuntul initial al problemei
1.1.2. Specificatiile
1.1.3. Documentarea de proiectare
1.1.4. Documentarea de programare
1.2. De folosire
1.2.1. Regulile de utilizare
1.2.2. Instructiunile
1.3. Auto-documentare
1.3.1. Ajutorul necesar utilizatorului, fc. Help
1.3.2. Autoverificarea

2. Programarea
2.1. Inspectarea
2.1.1. Specificarea
2.1.2. Proiectarea
2.1.3. Codificarea
2.2. Testarea
2.2.1. Dupa specificatia problemei
2.2.2. Dupa textul programului
2.2.3. Testarea robustetei programului
2.2.4. Testarea de integrare
2.3. Depanarea
2.3.1. Executarea pas cu pas a programului
2.3.2. Observarea valorilor unei variabile
2.3.3. Modificarea valorilor
2.4. Verificarea
2.4.1. Analiza specificatiilor
2.4.2. Inspectarea proiectarii, codificarii, documentatiei
2.4.3. Executia simbolica a algoritmelor
2.4.4. Testarea produsului
2.4.5. Citirea documentatiei
2.5. Validarea
2.5.1. Testare cu date reale
2.5.2. Testarea robustitiei programului
2.5.3. Verificarea timpului in care s-au obtinut rezultatele
2.5.4. Forma aparitiei rezultatelor

Probleme propuse spre analiza.


Dându-se o tablă de şah de dimensiune nxn (n>3) să se aranjeze pe
ea n regine fără ca ele să se atace. Reamintim că o regină atacă linia,
coloana şi cele 2 diagonale pe care se află. În figura de mai jos
celulele colorare mai închis sunt atacate de regina poziţionată unde
indică litera “R”.

#include "stdio.h"
#include "conio.h"
#include "math.h"
clock_t begin, end;
double time_spent;

begin = clock();

int ataca(int linie, int memorie[])


//aceasta funcţie testează daca regina de pe linie este atacata de

// reginele poziţionate anterior. Este de fapt funcţia de Validare


{
int i;

//luam pe rând toate damele poziţionate anterior


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

//verificam daca cele 2 dame sunt pe aceeaşi coloana sau pe diagonală


if((memorie[linie]==memorie[i]) ||
(abs(memorie[i]-memorie[linie])==linie-i))

// se ataca, deci nu sunt corect poziţionate.


return 1;
return 0;
//daca am ajuns aici, înseamnă că nu se atacă
}

//căile de afişare sunt infinite


void afiseaza(int dim,int memorie[])
{
int l,c;
for(l=0;l<=dim;l++)
printf("_");
for(l=0;l<dim;l++)
{
printf("\n|");
for(c=0;c<dim;c++)
if(memorie[l]==c)
printf("%c",6);
else
if((l+c)&1)
printf(" ");
else
printf("%c",219);
printf("|");
}
printf("\n");
for(l=0;l<=dim;l++)
printf("-");
printf("\n");
printf("o tasta, va rog!\n");
getch();
}
void dame(int dim,int linie, int memorie[])

// aici este funcţia care face efectiv backtracking-ul (suntem la

// linia(nivelul) linie.

// dim ne spune cât de mare-i tabla

// memorie tine minte damele deja poziţionate. Ea este stiva


{
if(linie==dim)
// daca am terminat
afiseaza(dim,memorie);
//afişăm
else
//altfel avem de lucru
for(memorie[linie]=0;memorie[linie]<dim;memorie[linie]++)

//încercam toate coloanele de la stânga la dreapta


if(!ataca(linie,memorie))

//daca e o poziţie valida trecem la nivelul următor


dame(dim,linie+1,memorie);

//când ne întoarcem din apelul recursiv înseamnă că am revenit

// la acest nivel şi încercăm următoarea coloana


}
void main()
{
int n,memorie[100];
printf("dimensiunea tablei de joc=");
scanf("%d",&n);
dame(n,0,memorie);// prima linie are numărul zero

end = clock();
time_spent = (double)(end - begin) / CLOCKS_PER_SEC;
}

#define __STDC_WANT_LIB_EXT1__ 1
#include <time.h>
#include <stdio.h>
 
int main(void)
{
time_t t = time(NULL);
printf("UTC:  %s", asctime(gmtime(&t)));
printf("local: %s", asctime(localtime(&t)));
 
#ifdef __STDC_LIB_EXT1__
struct tm buf;
printf("UTC:  %s", asctime(gmtime_s(&t, &buf)));
printf("local: %s", asctime(localtime_s(&t, &buf)));
#endif
}
Programul va afisa :
UTC: Tue May 07 18:12:09 2015
local: Tue May 07 13:12:09 2015
UTC: Tue May 07 18:12:09 2015
local: Tue May 07 13:12:09 2015

Modelul general al metodei programãrii dinamice


Dezvoltarea unui algoritm de programare dinamicã poate fi descrisã de urmãtoarea succesiune
de paşi:
 se caracterizeazã structura unei soluţii optime
 se defineşte recursiv valoarea unei soluţii optime
 se calculeazã de jos în sus valoarea unei soluţii optime.
Dacã pe lângã valoarea unei soluţii optime se doreşte şi soluţia propriu-zisã, atunci se mai
efectueazã urmãtorul pas:
 din informaţiile calculate se construieşte de sus în jos o soluţie optimã.
Acest pas se rezolvã în mod natural printr-un algoritm recursiv, care efectueazã o
parcurgere în sens invers a secvenţei optime de decizii calculate anterior prin algoritmul de
programare dinamicã.
Exemplu :Problema inmultirii inlantuite optime a matricilor se poate rezolva si prin
urmatorul algoritm recursiv:
Algoritmul in Turbo Pascal :
function rminscal(i, j)
    {returneaza numarul minim de inmultiri scalare pentru a calcula produsul matricial returneza
Mi Mi+1 ... Mj}
     if i = j then return 0
     q
     for k = i to j<1 do
          q = min(q, rminscal(i, k)rminscal(k=1, j)-d[i+1]d[k]d[ j])
     return q
unde tabloul d[0 .. n] este global.

Algoritmul in C :
#include <stdio.h>
#include<time.h>
double start,stop; //variabile care vor determina timpulde executie a programului

main()
{int i,j,n,k;
start=clock(); //inceputul calcului timpului
printf("Introdu dimendiunea matriciilor:\n");
scanf("%d",&n); //citim dimensiunea matricilor
printf("\n");
int a[n][n], b[n][n], c[n][n]; // declalarea matricilor 1,2 si matricea care se
formeaza
printf("Introdu valorile tabloului a[]:\n"); //citim valolire pentru tabloul 1
for (i=0;i<n;i++){ //parcurge pina la numarul de elemente
for (j=0;j<n;j++){ //contorul parcurge pina la numarul de elemente
printf("a[%d][%d]=",i,j);
scanf("%d",&a[i][j]);}} //citim valoarea pentru fiecare element al tabloului
printf("\n");
printf("Introdu valorile tabloului b[]:\n"); //citim valolire pentru tabloul 2
for (i=0;i<n;i++){ //parcurge pina la numarul de elemente
for (j=0;j<n;j++){ //contorul parcurge pina la numarul de elemente
printf("b[%d][%d]=",i,j);
scanf("%d",&b[i][j]); //citim valoarea pentru fiecare element al tabloului b
}}
for (i=0;i<n;i++){ //parcurge pina la numarul de elemente
for (j=0;j<n;j++){ //contorul parcurge pina la numarul de elemente
c[i][j] = 0; //initial tabloul c este declarat ca liber, 0
for (k=0;k<n;k++){ ////contorul parcurge pina la numarul de elemente
c[i][j] += a[i][k] * b[k][j]; //formeza elementele tabloului C pe baza tabloului 1 si 2
}}
}
printf("\nMatricea C are forma:\n"); //afisarea matricei C
for (i=0;i<n;i++){ //contorul parcurge pina la numarul de elemente
printf("\n\n\n"); //afiseza doua rinduri libere , formind astfel o forma de matrice
for (j=0;j<n;j++){ //parcurge pina la numarul de elemente
printf("\t%3d",c[i][j]); //afiseaza fiecare element al tabloului format
}}
stop=clock(); printf("\n\nTimpul de executie a programului=%lf\n",(stop-start)/CLOCKS_PER_SEC);
} //afiseaza timpul de executie a programului

2.8 Metoda Branch and Bound


Metoda Branch and Bound foloseste la rezolvarea problemelor la care domeniul în care se
caută soluţia este foarte mare şi nu se cunoaşte un alt algoritm care să conducă mai rapid la
rezultat. Problemele care pot fi abordate prin această metodă pot fi modelate într-un mod
asemănător celui folosit la metoda Backtracking. Se pleacă de la o configuraţie iniţială şi se
reţine şirul de operaţii prin care aceasta este transformată într-o configuraţie finală dacă aceasta
este dată, în alte cazuri se cere configuraţia finală ştiind că trebuie să verifice anumite condiţii de
optim.
Diferenţa dintre cele două metode constă în faptul că metoda Backtracking la fiecare etapă
selectează un singur succesor după care se face verificarea condiţiilor de continuare, iar metoda
Branch and Bound generează la fiecare pas toate configuraţiile posibile (toţi succesorii) care
rezultă din cea curentă şi le stochează într-o listă. Se alege apoi un element din listă după un
criteriu ce depinde de problemă şi se reia procesul de expandare.
Alegerea optimă a unui element din această listă pentru expandare se face cu ajutorul unei
funcţii f = g + h în care g este o funcţie care măsoară lungimea drumului parcurs de la
configuraţia iniţială până la nodul curent iar h este o funcţie (euristică) care estimează efortul
necesar pană se ajunge la soluţie şi este specifică fiecărei probleme. Alegerea funcţiei h este
foarte importantă din punct de vedere a vitezei de execuţie a programului.
Se lucrează cu două liste: lista open în care se reţin configuraţiile neexpandate încă şi lista
close care le memorează pe cele expandate. Soluţia se construieşte folosind un arbore care se
parcurge pe baza legăturii tată.
Nodurile sunt înregistrări care cuprind următoarele informaţii:
• configuraţia la care s-a ajuns, t
• valorile funcţiilor g si h
• adresa nodului tată
• adresa înregistrării următoare în lista open sau close
• adresa înregistrării următoare în lista succesorilor

Algoritmul:
1. Înregistrarea corespunzătoare configuraţiei iniţiale este încărcată în open cu g=0 şi f=h
2. Atât timp cât nu s-a selectat spre expandare nodul corespunzător configuraţiei finale şi lista
open este nevidă, se execută următoarele
3. Se selectează din lista open nodul t cu f minim
4. Se expandează acest nod obţinând o listă liniară simplu înlănţuită cu succesorii săi
5. Pentru fiecare succesor din această listă se execută
6. Se ataşează noul g, obţinut ca sumă între valoarea lui g a configuraţiei t şi costul expandării
7. Se testează dacă acest succesor aparţine listei open sau close şi în caz afirmativ, se verifică
dacă valoarea lui g este mai mică decât cea a configuraţiei găsite în listă
8. În caz afirmativ, nodul găsit este direcţionat către actualul părinte (prin fixarea legăturii tată) şi
se ataşează noul g, iar dacă acest nod se găseşte în close, este trecut în open
9. În caz că acest nod nu se găseşte în open sau close, este introdus în lista open
10. Dacă s-a selectat pentru expandare nodul corespunzător configuraţiei finale atunci se trasează
folosind o procedură recursivă drumul de la configuraţia iniţială la cea finală utilizând legătura
tată
11. Dacă ciclul se încheie deoarece lista open este vidă înseamnă că problema nu are soluţie
Obs: Algoritmul se poate implementa folosind o singură listă iar diferenţierea dintre nodurile
expandate şi cele neexpandate se va face după un câmp special. Apare însă dezavantajul că
operaţiile de căutare se fac într-o listă mai lungă. Acest dezavantaj se poate elimina folosind o
structură specială numită tabelă hash. În unele cazuri se poate folosi un arbore binar.

2.9. Tabel de sinteza:


Tabelul urmator reprezintă o sinteza a informației.

Metoda Condiţii de aplicare. Timpul de lucru (în Construire soluzţiei


general).
Backtracking Şir cu elemente luînd Exponenţial. incremental
valori în mulţimi finite.
Divide et Impera Soluţia se poate k
construi pe baza  n * log n Top-down
soluţiilor
subprogramelor.
Greedy Probleme de
optimizare care Polinomial. Incremental
respectă principiul
optimizării şi al
alegerii Greedy.
Programarea Probleme de
dinamică optimizare care Polinomial. Bottom-up
respectă principiul
optimizării.
Branch & bound Probleme de Diferă funcţile de Parcurgere least-
optimizare dificile în problemă. cost
care aplicarea
celorlalte metode nu
este adevarată.

Analiza complexităţii şi eficienţei algoritmilor ale Metodelor Tehnicii


Programarii
3. Importanţa analizei şi implementării algoritmilor eficienţi
Din experienţa anterioară putem defini un algoritm ca o descriere a unui proces de calcul, care
din nişte date iniţiale produce nişte rezultate interesante. Descrierea se face în termenii unor
operaţii elementare (operaţii aritmetice, comparaţii, decizii, etc.).
Aparent modul în care rezolvăm problemele depinde de operaţiile pe care le avem la-
ndemînă. În cazul acesta se iveşte natural întrebarea dacă anumite operaţii nu sunt mai expresive
decât altele. Acest lucru este adevărat, după cum se vede şi din proliferarea limbajelor de
programare; un astfel de limbaj defineşte practic o colecţie de operaţii elementare.

3.1. Timpul de execuţie al algoritmilor. Este dat de relatie de recurenta


De multe ori, pentru rezolvarea unei probleme, trebuie ales un algoritm dintre mai mulţi
posibili, două criterii principale de alegere fiind contradictorii:
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.
Daca programul urmează a fi rulat de un număr mare de ori, având şi un număr mare de date
de prelucrat, trebuie ales algoritmul care duce la o execuţie mai rapidă. Chiar în această situaţie,
ar trebui implementat mai înainte algoritmul mai simplu şi calculată reducerea de timp de
execuţie pe care ar aduce-o implementarea algoritmului complex.
Timpul de rulare al unui program depinde de următorii factori:
- datele de intrare;
- calitatea codului generat de compilator;
- natura şi viteza de execuţie a instrucţiunilor programului;
- complexitatea algoritmului care stă la baza programului.
Deci timpul de rulare e o funcţie de intrarea sa, de cele mai multe ori, nedepinzând de valorile
de la intrare, ci de numărul de date şi operaţii.
In continuare vom nota cu T(n) timpul de execuţie al unui algoritm destinat rezolvării unei
probleme de dimensiune n. Pentru a estima timpul de execuţie trebuie stabilit un model de calcul
şi o unitate de măsură.
Vom consideră un model de calcul (numit şi maşină de calcul cu acces aleator) caracterizat
prin:
• Prelucrările se efectuează în mod secvenţial.
• Operaţiile elementare sunt efectuate în timp constant indiferent de valoarea operanzilor.
• Timpul de acces la informaţie nu depinde de poziţia .
• *Timpul de execuţie al întregului algoritm se obţine însumând timpii de execuţie a
prelucrărilor componente.
Importanţa celui mai defavorabil caz
In aprecierea şi compararea algoritmilor interesează în special cel mai defavorabil caz deoarece
furnizează cel mai mare timp de execuţie relativ la orice date de intrare de dimensiune fixă. Pe
de altă parte pentru anumiţi algoritmi cazul cel mai defavorabil este relativ frecvent.
În ceea ce priveşte analiza celui mai favorabil caz, aceasta furnizează o margine inferioară a
timpului de execuţie şi poate fi utilă pentru a identifica algoritmi ineficienţi (dacă un algoritm are
un cost mare în cel mai favorabil caz, atunci el nu poate fi considerat o soluţie acceptabilă).

Timp mediu de execuţie


Uneori, cazurile extreme (cel mai defavorabil şi cel mai favorabil) se întâlnesc rar, astfel ca
analiza acestor cazuri nu furnizează suficientă informaţie despre algoritm.
In aceste situaţii este utilă o altă măsură a complexităţii algoritmilor şi anume timpul mediu
de execuţie. Acesta reprezintă o valoare medie a timpilor de execuţie calculată în raport cu
distribuţia de probabilitate corespunzătoare spaţiului datelor de intrare.

3.2. Ordin de creştere


Pentru a aprecia eficienţa unui algoritm nu este necesară cunoaşterea expresiei detaliate a
timpului de execuţie. Mai degrabă interesează modul în care timpul de execuţie creşte o dată cu
creşterea dimensiunii problemei. O măsură utilă în acest sens este ordinul de creştere. Acesta
este determinat de termenul dominant din expresia timpului de execuţie. Când dimensiunea
problemei este mare valoarea termenului dominant depăşeşte semnificativ valorile celorlalţi
termeni astfel că aceştia din urmă pot fi neglijaţi.
Întrucât problema eficienţei devine critică pentru probleme de dimensiuni mari se face analiza
complexităţii pentru cazul când n este mare î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.
Există următoarele cazuri când ordinul de creştere a timpului de execuţie, nu e cel mai bun
criteriu de apreciere a performantelor unui algoritm:
 dacă un program se rulează de puţine ori, se alege algoritmul cel mai uşor de
implementat;
 dacă întreţinerea trebuie făcută de o altă persoană decât cea care l-a scris, un algoritm
simplu, chiar mai puţin eficient, e de preferat unuia performant, dar foarte complex şi
greu de înţeles;
 există algoritmi foarte eficienţi, dar care necesită un spaţiu de memorie foarte mare,
astfel încât folosirea memoriei externe le diminuează foarte mult performanţele.

3.3. Complexitate asimptotică


Pentru a ferifica cit de bun este un algoritm trebuie să cădem de acord asupra unei metode
prin care măsurăm calităţile unui algoritm; putem măsura timpul lui de execuţie pentru o anumită
problemă, sau cantitatea de memorie folosită, sau numărul de instrucţiuni care descriu
programul, sau poate o altă dimensiune.
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ă:
- Avem un algoritm de timp n log n;
- Am demonstrat că nu se poate mai bine de atît.
- 3.6. Exemple de analizei empirice
- 1. Mai jos se prezintă modul de evaluare a timpului de execuţie a funcției de sortare a
elementelor unui tablou de dimensiune n prin metoda "bubble":
-
- #include<stdlib.h>
- #include<time.h>
- #include<stdio.h>
-
- double start , stop; //varivile care determin ă timpul de execuție a
programului
- int main()
- {start=clock(); //începe calculul timpului de execuție
- int i=0,j=0,schimb=0,n=0;
- int V[100]; //vectorul care stochează elemenetele sortate
- start:printf("Introduceti numarul de elemente n al sirului\n");
- scanf("%d",&n); //citim numarul de elemente
- if(n>100) //daca numarul de elemente este mai
mare ca 100
- {
- printf("Sirul poate avea maxim 100 elemente\n"); //eroare
- goto start; //citim numărulde elemente din
nou
- }
- for (i=0;i<n;i++) //parcurge pină la numarul de elemente
- {
- printf("a[%d]=",i+1);
- scanf("%d",&V[i]); //citim valoarea elementului i al vectorului
- }
- printf("\nAti introdus vectorul:\n"); //afisează vectorul introdus
- for (i=0;i<n;i++) //parcurge toate elementele ale
tabloului
- {
- printf("%d ", V[i]); //afișează elementul respectiv al tabloului
- }
- printf("\n\nIn continuare sortam sirul...\n"); //sortarea ,,bubble,,
- getchar();
- printf("\nVectorul sortat este:\n");
- for(i=0;i<=n-2;i++) //parcurge pina la penultimul element al
tabloului
- {
- for(j=i+1;j<=n-1;j++) //parcurge pină la ultimul element altabloului
- {
- if(V[i]>V[j]) //verifică care este mai mare, i sau i+1 elementul
- {
- schimb=V[i]; //cel mai mare stochează variabilei schimb
- V[i]=V[j]; //elementul curent este primul in vector
- V[j]=schimb; //succesor este celaalt numar
- }}
- }
- for(i=0;i<=n-1;i++) //parcurge pină la penultimul element al tabloului
- {
- printf("%d ", V[i]); //afișează vectorului sortat
- }
- stop=clock();printf("Timpul de executie=%lf s\n", (stop-start)/CLOCKS_PER_SEC);
getch();
- return 0; //calculul timupului se termină și afișează timpul de
execuție
- }
-

- Pentru instrucţiunea if , timpul este suma dintre cel pentru evaluarea condiţiei, O(1) şi cel
al secvenţei ce se execută la condiţie adevărată, tot O(1), calculat mai sus, deci O(1),
acesta fiind deci şi timpul pentru o iteraţie a instrucţiunii for. Pentru cele n - i iteraţii ale
lui for , timpul de execuţie este O((n-i)).
-
- Modalitatea de evaluare a timpului de execuţie al unei proceduri recursive, este ilustrată
prin cea a funcţiei de calcul al factorialului:
- Algoritmul in C:
- int fact(int n) //funcția care calculeza factorialul unui întreg
- {
- if (n==0) //dacă elemental introdus este 0
- return 1; //eroare, ieșirea forțată din program
- else //dacă nu
- return n*fact(n-1); //returnează factoriarul numarului
- }
- int main(void)
- {
- int nr,i,n,f, sum=0; //suma inițială este 0
- printf("\t\tProgramul calculeaza factorialul unui numarul");
- printf("\nIntroduceri numarul: ");
- scanf("%d",&nr); //citim numarul care trebuie calculate
factoriarul
- if (nr==0){ //dacă numarul introdus este 0
- printf("Nu exista!\n"); //eroare
- return 0;} //ieșirea din program
- n=nr; //atribuie lui n numarul citit
- f=fact(nr); //apel la funcția pentru calcula factorialul
- printf("\nFactorialul lui %d este %d\n",nr,f); //afișează factorialul
numarului n
- return 0;
- }
-
-

-
- Dimensiunea intrării este aici n, valoarea numărului al cărui factorial se calculează şi se
notează cu T(n), timpul de rulare al funcţiei main().
- Timpul de rulare pentru liniile de citire este O(1), iar pentru linia for este O(1) + T(n - 1),
deci pentru variabilele c si d neprecizate:
- T(n) = c + T(n - 1), pentru n > 1 sau T(n) = d , pentru n  1.Pentru n > 2, se obţine T(n) =
2c + T(n - 2). Pentru n > 3, expandând pe T(n - 2), se obţine T(n) = 3c + T(n - 3). Deci, în
general T(n) = ic + T(n - i), pentru n > i.
- În final, când i = n - 1, se obţine T(n) = (n - 1)c + T(1) = (n - 1)c + d.
-
-
- 4. Compararea Metodelor de Programare
- Se consideră un triunghi format din n linii, fiecare linie i conţinând i numere întregi. Să
se determine cea mai mare sumă de numere aflate pe un drum între numărul de pe prima
linie şi un număr de pe ultima linie. Fiecare număr din acest drum este situat sub
precedentul, la stânga sau la dreapta acestuia.
-
-
- 4.1. Metoda Divide et Impera
- #include <stdio.h>
- #include<time.h>
-
- double start , stop; //declaram variabilele start si stop
-
- int n; int a[30][30];
- void citire() { int i,j; //funcția pentru citirea datelor
- printf("Introduceti numarul de elemente=");
- scanf("%d",&n); //citim numarul de elemente
- for (i=1;i<=n;i++) //parcurge pina la n
- for (j=1;j<=i;j++) //parcurge pina la i
- {printf("a[%d,%d]=",i,j);
- scanf("%d",&a[i][j]); //citimvaloarea pentru elementul i,j
- }}
-
- void afisare() { int i,j; //funția pentru afișarea datelor
- for (i=1;i<=n;i++) { //parcurge pina la n
- printf("\n");for (j=1;j<=i;j++) printf("%5d",a[i][j]); printf("\n");} //afișează toate
elementele
- }
- int suma (int i, int j) { int s,d,S; if (i<=n) { s=suma(i+1,j);
d=suma(i+1,j+1);
- if (s>d) S=a[i][j]+s; else S=a[i][j]+d; } else S=0; return S; }
-
- void main()
- { start=clock();
- citire(); printf("\n"); afisare(); printf("\n"); printf("\nSuma maxima=
%d\n\n",suma(1,1));
- stop=clock();printf("%Timpul de executie=%lf s", (stop-start)/CLOCKS_PER_SEC);
getch(); }
-

-
-
-
- 4.2 Metoda Greedy
- #include<time.h>
- #include <stdio.h>
- double start , stop;
- int n,S; int a[50][50];
-
- void citire() { int i,j; printf("Introduceti numarul de elemente="); //fucncția pentru
citirea datelor
- scanf("%d",&n); //citim numarul de elemente
- for (i=1;i<=n;i++) for (j=1;j<=i;j++) { printf("a[%d,%d]=",i,j);
- scanf("%d",&a[i][j]); //citim valoarea elementului de pe poziția i,j }
}
- void afisare() { int i,j; //funcția pentru afișarea datelor
- for (i=1;i<=n;i++) { for (j=1;j<=i;j++) printf("%5d",a[i][j]); printf("\n"); } }
- void suma () { int i,j; S=a[1][1]; j=1;
- for (i=2;i<=n;i++) { if (a[i][j]>a[i][j+1]) S+=a[i][j];
- else { S+=a[i][j+1]; j++; } } }
- void main()
- { start=clock(); //incepe calculul timpului de execuție
- citire(); printf("\n"); afisare(); printf("\n"); suma(); printf("\nsuma
maxima==%d\n\n",S); //maxim devine egal cu S
- stop=clock();printf("Timpul de executie=%lf s", (stop-start)/CLOCKS_PER_SEC);
getch();
- } //afișează timpul de executie a programului
-

-
-
-
- 4.3. Metoda Backtracking

- #include<stdio.h>
- #include <time.h>

- double start,stop;

- int n,S,Smax; int a[50][50],x[50];

- void citire() { int i,j; printf("Introduceti numarul de elemente=");

- scanf("%d",&n); //citim numarulde elemente

- for (i=1;i<=n;i++) for (j=1;j<=i;j++) { printf("a[%d,%d]=",i,j);

- scanf("%d",&a[i][j]); //citim valoarea elementului pepozitia i,j } }

- void afisare() { int i,j; //funcția pentru afisarea datelor

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

- printf("\n");

- for (j=1;j<=i;j++) printf("%5d",a[i][j]); //afișează elementul

- printf("\n"); } }

- void afis() { S=0;

- for (int i=1;i<=n;i++) { S+=a[i][x[i]]; }

- if (S>Smax) Smax=S; }

- int valid(int k) //funcția ce verifică dacă poatea fi format


triunghiul

- { int cod; if (k>1)

- if (x[k]==x[k-1] || x[k]==x[k-1]+1) cod=1; //dacă este adevar cod devine 1

- else cod=0; //dacă cod nu se modifica

- else cod=1; return cod; //cod devine 1 și returneaza valoarea lui cod

- }
-
- void back(int k) { for (int i=1;i<=k;i++) { x[k]=i; if (valid(k)) if (k==n) afis(); else
back(k+1); } }

- void main() {start=clock(); //incepe calcul timpului

- citire(); printf("\n");printf("\n"); afisare(); printf("\n"); Smax=0; back(1);


printf("\nsuma maxima=%d\n",Smax);

- stop=clock(); //se termina calcului timpului de execuție a programului

- }
-

-
-

- 4.4.Metoda programarii dinamice


-
- #include <stdio.h>
- double start,stop;
-
- int n; int a[50][50],s[50][50];
- void citire(){ int i,j; printf("Introduceti numarul de elemente=");
- scanf("%d",&n); //citim numarul de elemente
- for (i=1;i<=n;i++) for (j=1;j<=i;j++) { printf("a[%d,%d]=",i,j);
- scanf("%d",&a[i][j]); //citim valoarea pentru fiecare element al
tabloului } }
-
- void afisare() { int i,j; //funcția pentru afișare adatelor tabloului
- for (i=1;i<=n;i++) { printf("\n"); for (j=1;j<=i;j++) printf("%5d",a[i][j]);
printf("\n"); } }
-
- void suma () { int i,j; //funcția formează suma elementelor
- for (i=1;i<=n;i++) s[n][i]=a[n][i]; //stochează valorile din a în tabloul s
- for (i=n-1;i>0;i--) for (j=1;j<=i;j++) //parcurge pînă la elementul i al
tabloului
- { s[i][j]=a[i][j]+s[i+1][j]; if (s[i+1][j]<s[i+1][j+1]) s[i][j]=a[i][j]+s[i+1][j+1];
} }
-
- void main() {
- start=clock(); //incepe calcului timpului de execuție a
programului
- citire(); printf("\n"); //apel la funcția de citire
- afisare(); printf("\n"); //apel la funcția de afișare a datelor
- suma(); printf("\nSuma maxima=%d\n",s[1][1]); //afișeaza suma
formată
- stop=clock(); //se termină calculul timpului de execuție a
progrfamului
- }
-

-
-
-
- Compararea metodelor dupa timpul de executie:
-
Enunt problema Date Timp de executie (secunde)
de test Bactracking Greedy Programare dina Divide et im
Suma maxima in N=5 4,906 3,920 5,492 5,017
triunghi N=10 14,183 11,700 21,467 11,474
N=20 42,424 41,230 45,488 39,480
-
- Exemplu(1): Se dă o multime de numere pozitive, P şi un număr M. Se cere determinarea
unui subset a lui P a cărui sumă a elementelor să fie cel mult M.
- Listing-ul:
- #include<time.h>
- #include <stdio.h>
-
- int main() {
- double start,stop; //variabile care determină timpul de execuție
aprogramului
- int n, p[200], rez[200], k, M, i, j, sum, temp, x;
- start=clock() ; //începe calculul timpului de executie a programului
- printf("introduceti n: ");
- scanf("%d", &n); //citimnumarulde elemente a tringhiiului
- for (i=0; i<n; i++) //parcurge pina la ultimul elementul al tabloului
- { printf("p[%d]=", i);
- scanf("%d", &p[i]); } //citim valoarea fiecarui element al tabloului
- printf("Introduceti suma: ");
- scanf("%d", &M); //citim numarul M
- for (i=0; i<n-1; i++) //parcurge pin al penultimul element al
tabloului
- for (j=i+1; j<n; j++) //parcurge pina la ultimul element al tabloului
- if (p[i]>p[j]) { temp = p[i]; p[i] = p[j]; p[j] = temp; }; //compara
elementele tabloului
- k = 0; sum = 0; //atribuie variabeor valoarea 0
- for (i=0; i<n; i++) { //parcurge pina la ultimul element al
tabloului
- x = p[i]; /* alege(A,i,x) */
- if ( sum + x <= M ) { //dacă suma nu depășeste suma
introduse
- rez[k++] = x; //atribuie lui rez valoarea stocat in x
- sum += x; //la suma se adaga
valoarea lui x
- }
- else //dacă nu
- break; } //se termina iterația și iese din contor
- printf("submultimea rezultat: ");
- for (i=0; i<k; i++) printf(" %d", rez[i]); printf("\n"); //afișează rezultatul
- stop=clock(); //se termina calculu timpului de execuție a programului
- printf("Timpul de executie=%lf s", (stop-start)/CLOCKS_PER_SEC);
- getch(); //așteaptă pina la tastarea unei taste
- }

-
-
- Exemplu(2): Problema damelor de şah. Să se găsească toate soluţiile posibile de aşezare
pe tabla de şah de n linii şi n coloane a n dame, astfel încât să nu existe două dame pe
aceaşi linie, coloană sau diagonală.
- Listing-ul:
- #include <stdio.h>
- #include <math.h>
- int main()
- { int n,i,k,v;
- int x[20]; //x[i] va reprezenta coloana pe care se afla dama de pe linia i
- printf("Introduceti n:");
- scanf("%d",&n); //citim numarul de elemente
- k = 1; x[1] = 0;
- while (k > 0) { //cit timp k este mai mare ca 0
- v=0; //atribuie lui v valoarea 0
- while ((x[k] <= n-1) && (v==0)) {x[k]++; //daca conditia este adevarata
- for (v=1,i=1; (i<=k-1) && (v==1);i++)
- if ((x[k]==x[i])||(k-i == abs(x[k]-x[i]))) //verifica daca k este egal cu i
- v=0; //daca da v din nou devine 0
- }
- if (v==0) k--; //daca v nu sa modificat , decrementeaza k
- else if (k==n) { //daca nu, verifica daca k este egal cu n
- for (i=1;i<=n;i++) //parcurge pina la ultimul element al
tabloului
- printf("%d ",x[i]); printf("\n"); } //afiseaza toate elementele tabloului
- else { k++; //daca ca nu este egal cu n
- x[k] = 0; } } //fiecare elementul k al tabloului devine 0
- }
-
-
-

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