Professional Documents
Culture Documents
1
Capitolul I
PREZENTARE GENERALĂ
Fie n matrice A1, A2, ..., An, de dimensiuni d0xd1, d1xd2, ..., dn-1xdn.
Produsul A1xA2x...xAn se poate calcula în diverse moduri, aplicând
asociativitatea operaţiei de înmulţire a matricelor. Numim înmulţire elementară
înmulţirea a doua elemente. În funcţie de modul de parantezare diferă numărul
2
de înmulţiri elementare necesare pentru calculul produsului A1xA2x...xAn.
Determinaţi o parantezare optimală a produsului A1xA2x...xAn (costul
parantezării, adică numărul total de înmulţiri elementare sa fie minim).
Exemplu:
Pentru n=3 matrice cu dimensiunile (10,1000), (1000,10) si (10,100),
produsul A1xA2xA3 se poate calcula în doua moduri:
(A1xA2)xA3 necesitând 1000000+10000=1010000 înmulţiri elementare
A1x(A2xA3), necesitând 1000000+1000000=2000000 înmulţiri.
Reamintim ca numărul de înmulţiri elementare necesare pentru a
înmulţi o matrice A cu n linii si m coloane si B o matrice cu m linii si p coloane
este n*m*p
Soluţie:
Pentru a calcula A1xA2x...xAn, în final trebuie sa înmulţim două matrice,
deci vom paranteza produsul astfel: (A1xA2x...xAk)x(Ak+1x...xAn ). Această
observaţie se aplică si produselor dintre paranteze. Prin urmare, subproblemele
problemei iniţiale constau în determinarea parantezării optimale a produselor de
matrice de forma AixAi+1x...xAj, 1<=i<=j. Observăm că subproblemele nu sunt
independente. De exemplu, calcularea produsului AixAi+1x...xAj şi calcularea
produsului Ai+1xAi+2x...xAj+1, au ca subproblema comuna calcularea produsului
Ai+1x...xAj.
Pentru a retine soluţiile subproblemelor, vom utiliza o matrice M, cu n
linii şi n coloane, cu semnificaţia:
M[i][j] = numărul minim de înmulţiri elementare necesare pentru a
calcula produsul AixAi+1x...xAj, 1<=i<=j<=n.
Evident, numărul minim de înmulţiri necesare pentru a calcula
A1xA2x...xAn este M[1][n].
Pentru ca parantezarea să fie optimală, parantezarea produselor
A1xA2x...xAk şi Ak+1x...xAn trebuie să fie de asemenea optimală. Prin urmare
elementele matricei M trebuie sa satisfacă următoarea relaţie de recurenta:
M[i][i]=0, i={1,2,...,n}.
M[i][j]=min{M[i][k] + M[k+1][j] + d[i-1]*d[k]*d[j]}
i<=k<j
Cum interpretam aceasta relaţie de recurenta? Pentru a determina
numărul minim de înmulţiri elementare pentru calculul produsului
AixAi+1x...xAj, fixăm poziţia de parantezare k în toate modurile posibile (între i
si j-1), şi alegem variantă care ne conduce la minim. Pentru o poziţie k fixată,
costul parantezării este egal cu numărul de înmulţiri elementare necesare pentru
calculul produsului AixAi+1x...xAk, la care se adaugă numărul de înmulţiri
elementare necesare pentru calculul produsului Ak+1x...xAj si costul înmulţirii
celor doua matrice rezultate (di-1*dk*dj).
Observăm că numai jumătatea de deasupra diagonalei principale din M
este utilizată. Pentru a construi soluţia optimă este utilă şi reţinerea indicelui k,
3
pentru care se obţine minimul. Nu vom considera un alt tablou, ci-l vom reţine,
pe poziţia simetrică faţă de diagonala principala (M[j][i]).
Rezolvarea recursivă a relaţiei de recurenta de mai sus este ineficientă,
datorită faptului că subproblemele de suprapun, deci o abordare recursia ar
conduce la rezolvarea aceleiaşi subprobleme de mai multe ori. Prin urmare vom
rezolva relaţia de recurenta în mod bottom-up: (determinam parantezarea
optimală a produselor de două matrice, apoi de 3 matrice, 4 matrice, etc).
long
M[NMax][NMax];
void
dinamic()
{ int nr,
i, j, k, kmin;
long
min, Infinit=1000000000;
for
(nr=2; nr<=n; nr++) //nr =câte matrice se înmulţesc
for
(i=1; i<=n-nr+1; i++)
{j=i+nr-1;
//se înmulţesc nr matrice, de la Ai la Aj
for (k=i,
min=Infinit; k<j; k++)
//determin minimul si poziţia sa
if
(min>M[i][k]+M[k+1][j]+d[i-1]*d[k]*d[j])
{min=M[i][k]+M[k+1][j]+d[i-1]*d[k]*d[j];
kmin=k;}
M[i][j]=min; M[j][i]=kmin; }
}
Reconstituirea soluţiei optime se face foarte uşor în mod recursiv,
utilizând informaţiile reţinute sub diagonala principala în matricea M:
void
afisare(int i, int j)
{//afişează parantezarea optimală a produsului Aix...xAj
if
(i==M[j][i]) cout<<"A"<<i;
else
{cout<<"("; afişare(i,M[j][i]);
cout<<")";}
cout<<"x";
if
(j==M[j][i]+1) cout<<"A"<<j;
else
4
{cout<<"("; afişare(M[j][i]+1,j);
cout<<")";}}
1.4. Subşir crescător maximal
Fie un şir A=(a1, a2, ..., an). Numim subşir al şirului A o succesiune de
elemente din A, în ordinea în care acestea apar în A: ai1, ai2, ..., aik, unde 1<=
i1<i2<...<ik. Determinaţi un subşir crescător al şirului A, de lungime maximă.
Exemplu:
Pentru A=(8,3,6,50,10,8,100,30,60,40,80) o soluţie poate fi:
( 3,6, 10, 30,60, 80).
Soluţie
Fie Ai1=(ai1<=ai2<= ...<= aik) cel mai lung subşir crescător al lui şirului A.
Observăm ca el coincide cu cel mai lung subşir crescător al şirului (ai1, ai1+1, ...,
an). Evident Ai2=(ai2<=ai3<= ...<= aik) este cel mai lung subşir crescător al lui (ai2,
ai2+1, ..., an), etc. Prin urmare, o subproblemă a problemei iniţiale constă în
determinarea celui mai lung subşir crescător care începe cu ai, i={1,.., n}.
Subproblemele nu sunt independente: pentru a determina cel mai lung subşir
crescător care începe cu ai, este necesar sa determinam cele mai lungi subşiruri
crescătoare care încep cu aj, ai<=aj, j={i+1,.., n}.
Pentru a retine soluţiile subproblemelor vom considera doi vectori
suplimentari l şi poz, fiecare cu cate n componente, având semnificaţia:
l[i]=lungimea celui mai lung subşir crescător care începe cu a[i];
poz[i]=pozitia elementului care urmează după a[i] în cel mai lung subşir
crescător care începe cu a[i], dacă un astfel de element există, sau -1 dacă un
astfel de element nu exista.
Relaţia de recurenta care caracterizează substructura optimală a
problemei este:
l[n]=1; poz[n]=-1;
l[i]=max{1+l[j]|a[i]<=a[j]}
j=i+1,n
poz[i]= indicele j pentru care se obţine maximul l[i].
Rezolvam relaţia de recurenţă în mod bottom-up:
int i, j;
l[n]=1; poz[n]=-1;
for (i=n-1; i>0; i--)
for (l[i]=1,
poz[i]=-1, j=i+1; j<=n; j++)
if (a[i] <=
a[j] && l[i]<1+l[j])
{l[i]=1+l[j];
poz[i]=j;}
5
Pentru a determina soluţia optima a problemei, determinam maximul
din vectorul l, apoi afişăm soluţia, începând cu poziţia maximului si utilizând
informaţiile memorate în vectorul poz:
//determin maximul din vectorul l
int max=l[1], pozmax=1;
for (int i=2; i<=n; i++)
if (max<l[i]) {max=l[i]; pozmax=i;}
cout<< "Lungimea celui mai lung subşir crescător: " <<max;
cout<<"\nCel mai lung subşir:\n";
for (i=pozmax; i!=-1; i=poz[i])
cout<<a[i]<<' ';
Soluţie
Vom reţine triunghiul într-o matrice pătratica T, de ordin n, sub
diagonala principala. Subproblemele problemei date constau în determinarea
sumei maxime care se poate obţine din numere aflate pe un drum între numărul
T[i][j], până la un număr de pe ultima linie, fiecare număr din acest drum fiind
situat sub precedentul, la stânga sau la dreapta sa. Evident, subproblemele nu
sunt independente: pentru a calcula suma maxima a numerelor de pe un drum de
la T[i][j] la ultima linie, trebuie sa calculăm suma maximă a numerelor de pe un
drum de la T[i+1][j] la ultima linie si suma maximă a numerelor de pe un drum
de la T[i+1][j+1] la ultima linie.
Pentru a retine soluţiile subproblemelor, vom utiliza o matrice
suplimentara S, pătratica de ordin n, cu semnificaţia S[i][j]= suma maxima ce se
poate obţine pe un drum de la T[i][j] la un element de pe ultima linie, respectând
condiţiile problemei.
Evident, soluţia problemei va fi S[1][1].
Relaţia de recurenta care caracterizează substructura optimală a
problemei este:
S[n][i]=T[n][i], i={1,2,...,n}
6
S[i][j]=T[i][j]+max{S[i+1][j], S[i+1][j+1]}
Rezolvăm relaţia de recurenta în mod bottom-up:
int i,j;
for (i=1; i<=n; i++) S[n][i]=T[n][i];
for (i=n-1; i>0; i--)
for (j=1; j<=i; j++)
{S[i][j]=T[i][j]+S[i+1][j];
if (S[i+1][j]<S[i+1][j+1])
S[i][j]=T[i][j]+S[i+1][j+1]);}
Exercitiu
Afişaţi si drumul în triunghi pentru care se obţine soluţia optimă.
Fie X=(x1, x2, ..., xn) si Y=(y1, y2, ..., ym) două şiruri de n, respectiv m
numere întregi. Determinaţi un subşir comun de lungime maximă.
Exemplu
Pentru X=(2,5,5,6,2,8,4,0,1,3,5,8) si Y=(6,2,5,6,5,5,4,3,5,8) o soluţie
posibilă este: Z=(2,5,5,4,3,5,8)
Soluţie
Notăm cu Xk=(x1, x2, ..., xk) (prefixul lui X de lungime k) si cu Yh=(y1,
y2, ..., yh) prefixul lui Y de lungime h. O subproblemă a problemei date constă în
determinarea celui mai lung subşir comun al lui Xk, Yh. Notăm cu LCS(Xk,Yh)
lungimea celui mai lung subşir comun al lui Xk, Yh. Utilizând aceste notaţii,
problema cere determinarea LCS(Xn,Ym), precum şi un astfel de subşir.
Observaţie 1. Daca Xk=Yh atunci LCS(Xk,Yh)=1+LCS(Xk-1,Yh-1).
2.Daca Xk Yh atunci LCS(Xk,Yh)=max(LCS(Xk-1,Yh),
LCS(Xk,Yh-1)).
Din observaţia precedentă deducem ca subproblemele problemei date nu
sunt independente şi că problema are substructură optimală.
Pentru a reţine soluţiile subproblemelor vom utiliza o matrice cu n+1
linii si m+1 coloane, denumita lcs. Linia şi coloana 0 sunt utilizate pentru
iniţializare cu 0, iar elementul lcs[k][h] va fi lungimea celui mai lung subşir
comun al şirurilor Xk si Yh.
Vom caracteriza substructura optimală a problemei prin următoarea
relaţie de recurentă:
7
Rezolvăm relaţia de recurenţă în mod bottom-up:
for (int k=1; k<=n; k++)
for (int h=1; h<=m; h++)
if (x[k]==y[h])
lcs[k][h]=1+lcs[k-1][h-1];
else
if (lcs[k-1][h]>lcs[k][h-1])
lcs[k][h]=lcs[k-1][h];
else
lcs[k][h]=lcs[k][h-1];
8
Capitolul II
ALGORITMI DE PROGRAMARE DINAMICĂ
TREI PRINCIPII FUNDAMENTALE ALE PROGRAMĂRII
DINAMICE
0 5 ∞ ∞
5 0 1 55
D0 = L =
3 0∞ 5 0
1 5∞ 5 0
obţinem succesiv
De obicei dorim să aflăm nu numai lungimea celui mai scurt drum, dar
şi traseul său. În această situaţie, vom construi o a doua matrice P, iniţializată cu
zero. Bucla cea mai interioară a algoritmului devine
if D[i,k]+D[k,j]<D[i,j] then D[i,j]←D[i,k]+D[k,j]
P[i,j]←k
Când algoritmul se opreşte, P[i,j] va conţine vârful din ultima iteraţie
care a cauzat o modificare în D[i,j]. Pentru a afla prin ce vârfuri trece cel mai
scurt drum de la i la j, consultăm elementul P[i,j]. Dacă P[i,j]=0, atunci cel mai
10
scurt drum este chiar muchia (i,j). Dacă P[i,j]=k, atunci cel mai scurt drum de la
i la j trece prin k şi urmează să consultăm recursiv elementele P[i,k] şi P[k,j]
pentru a găsi şi celelalte vârfuri intermediare.
Un arbore binr în care fiecare fârf conţine o valoare numită cheie este
un arbore de căutare, dacă cheia fiecărui vârf neterminal este mai mare sau egală
cu cheia descendenţilor săi stângi şi mai mică sau egală cu cheia descendenţilor
săi drepţi. Dacă cheile arborelui sunt distincte, aceste inegalităţi sunt, în mod
evident, stricte.
Figura de mai sus este un exemplu de arbore de căutare *, conţinând
cheile A, B, C,…,H. vârfurile pot conţine şi alte informaţii în afară de chei, la
care să avem acces prin intermediul cheilor. Această structură de date este utilă,
deoarece permitee o căutare eficientă a valorilor în arbore:
11
Cu o mulţime dată de chei, se pot construi mai mulţi arbori de
căutare(fig. de sus). Pentru a căuta o cheie X în arborele de căutare, X va fi
comparată la început cu cheia rădăcinei arborelui. Dacă X este mai mică decât
cheia rădăcinii, atunci se continuă căutarea în subarborele stâng. Dacă X este
egală cu cheia rădăcinii, atunci căutarea se incheie cu succes. Dacă X este mai
mare decât cheia rădăcinii, atunci se cotinuă căutarea în subarborele drept. Se
continuă astfel recursiv accest proces.
Generăm un arbore binar, conform următoarei metode recursive:
-rădăcina este etichetată cu (1,n)
-dacă un vârf este etichetat cu (i,j), i este mai mic decât j, atunci fiul său stâng va
fi etichetat cu (i,r[i,j]-1) ;i fiul său drept cu(r[i,j]+1,j)
-vârfurile terminale sunt etichetate cu (i,i)
plecând de la acest arbore, arborele de căutare optim se obţine
schimbând etichetele (i,j), i<j, în cr[i,j], iar etichetele (i,i) în ci.
12
2.4. Arborele optim
Vom rezolva problema obţinerii arborelui optim în cel mai simplu caz
posibil. Având în vedere specificul diferit al operaţiilor de organizare faţă de
celelalte operaţii efectuate asupra grafurilor, am considerat util să încapsulăm
optimizarea într-o clasă pe care o vom numi structura pentru optimizarea
arborilor.
Funcţionalitatea ei constă în:
-iniţializarea unui tablou cu adresele vârfurilor în ordinea crescătoare a
probabilităţilor cheilor.
-stabilirea de noi legăturiîntre vârfuri, astfel încât arborele să fie optim.
Principala cauză pentru care a fost aleasăaceastă implementare este că sunt
necesare doar operaţii de modificare a legăturilor. Deplasarea unui vârf
înseamnă nu numai deplasarea cheii, ci şi a informaţieiasociate.
13
se altereze. Cele două operaţii sunt diferite în privinţa complexităţii. Ştergerea
este mai dificilă şi mult mai diferită de operaţiile obişnuite.
Complexitatea funcţiei de ştergere este tipică pentru structurile de
căutare. Aceste structuri tind să devină atât de compacte încât ştergerea fiecărei
chei necesită reparaţii destul de complicate. De aceea se preferă de cele mai
multe ori o ştergere leneşă, prin care vârful este doar marcat ca fiind şters,
ştergerea fizică realizându-se cu ocazia unor reorganizări periodice.
15
Capitolul III
ALGORITMI DIVIDE ET IMPERA
TEHNICA DIVIDE ET IMPERA
function divimp(x)
{returnează o soluţie pentru cazul x}
if x este suficient de mic then return adhoc(x)
{descompune x în subcazurile x1, x2, …, xk}
for i . 1 to k do yi . divimp(xi)
{recompune y1, y2, …, yk în scopul obţinerii soluţiei y pentru x}
return y
16
unde adhoc este subalgoritmul de bază folosit pentru rezolvarea micilor
subcazuri ale problemei în cauza (in exemplul nostru, acest subalgoritm este A).
Un algoritm divide et impera trebuie să evite descompunerea recursivă a
subcazurilor “suficient de mici”, deoarece, pentru acestea, este mai eficienta
aplicarea directa a subalgoritmului de baza. Ce înseamnă însă “suficient de
mic”?
In exemplul precedent, cu toate ca valoarea lui n0 nu influentează
ordinul timpului, este influenţată însă constanta multiplicativa a lui n ln 3 , ceea ce
poate avea un rol considerabil în eficienta algoritmului. Pentru un algoritm
divide et impera oarecare, chiar dacă ordinul timpului nu poate fi îmbunătăţit,
se doreşte optimizarea acestui prag în sensul obţinerii unui algoritm cât mai
eficient. Nu există o metodă teoretică generală pentru aceasta, pragul optim
depinzând nu numai de algoritmul în cauză, dar şi de particularitatea
implementării. Considerând o implementare dată, pragul optim poate fi
determinat empiric, prin măsurarea timpului de execuţie pentru diferite valori
ale lui n0 şi cazuri de mărimi diferite.
În general, se recomandă o metodă hibridă care constă în:
i) determinarea teoretică a formei ecuaţiilor recurente;
ii) găsirea empirică a valorilor constantelor folosite de aceste ecuaţii,
în funcţie de implementare.
Revenind la exemplul nostru, pragul optim poate fi găsit rezolvând ecuaţia:
t A (n) = 3t A (n / 2) + t (n)
Empiric, găsim n0 ≅ 67, adică valoarea pentru care nu mai are important
dacă aplicăm algoritmul A în mod direct, sau dacă continuam descompunerea.
Cu alte cuvinte, atâta timp cât subcazurile sunt mai mari decât n0, este bine să
continuăm descompunerea. Dacă continuăm însă descompunerea pentru
subcazurile mai mici decât n0, eficienta algoritmului scade.
Observăm ca metoda divide et impera este prin definiţie recursivă.
Uneori este posibil să eliminam recursivitatea printr-un ciclu iterativ.
Implementata pe o maşina convenţionala, versiunea iterativa poate fi ceva mai
rapida (în limitele unei constante multiplicative). Un alt avantaj al versiunii
iterative ar fi faptul că economiseşte spaţiul de memorie. Versiunea recursivă
foloseşte o stiva necesară memorării apelurilor recursive. Pentru un caz de
mărime n, numărul apelurilor recursive este de multe ori în .(log n), uneori chiar
în .(n).
18
function iterbin1(T[1 .. n], x)
{căutare binara iterativa}
if n = 0 or x < T[1] then return 0
i ←1; j ← n
while i < j do
{T[i] = x < T[ j+1]}
k ← (i+j+1) div 2
if x < T[k] then j ← k-1
else i ← k
return i
Fie T[1 .. n] un tablou pe care dorim să-l sortăm crescător. Prin tehnica
divide et impera putem proceda astfel: separăm tabloul T în două părţi de
19
mărimi cât mai apropiate, sortăm aceste părţi prin apeluri recursive, apoi
interclasăm soluţiile pentru fiecare parte, fiind atenţi să păstrăm ordonarea
crescătoare a elementelor.
2(2 k −1 + 2 k −2 + ... + 2 + 1) = 2 ⋅ 2 k = 2n
O soluţie neinspirată:
Deşi eficient în privinţa timpului, algoritmul de sortare prin interclasare
are un handicap important în ceea ce priveşte memoria necesară. Într-adevăr,
orice tablou de n elemente este sortat intr-un timp în È(n log n), dar utilizând un
spaţiu suplimentar de memorie* de 2n elemente. Pentru a reduce consumul de
memorie, în implementarea acestui algoritm nu vom utiliza variabilele
intermediare U şi V de tip tablou<T>, ci o unică zonă de auxiliara de n
elemente.
Convenim să implementam procedura mergesort ca membru private al
clasei parametrice tablou<T>. Invocarea acestei proceduri se va realiză prin
funcţia membră:
Functia mergesort():
21
template <class T>
void tablou<T>::mergesort( int st, int dr, T *x ) {
if ( dr - st > 1 ) {
// mijlocul intervalului
int m = ( st + dr ) / 2;
// sortarea celor doua parti
mergesort( st, m );
mergesort( m, dr );
// pregatirea zonei x pentru interclasare
int k = st;
for ( int i = st; i < m; ) x[ i++ ] = a[ k++ ];
for ( int j = dr; j > m; ) x[ --j ] = a[ k++ ];
// interclasarea celor doua parti din x în zona a
i = st; j = dr - 1;
for ( k = st; k < dr; k++ )
a[ k ] = x[ j ] > x[ i ]? x[ i++ ]: x[ j-- ];
}
}
lista<E>
template <class E>
lista<E>& lista<E>::sort() {
if ( head )
head = mergesort( head );
return *this;
}
22
Conform algoritmului mergesort, lista se împarte în doua părţi egale, iar
după sortarea fiecăreia se realizează interclasarea. Împărţirea listei în cele doua
părţi egale nu se poate realiza direct, ca în cazul tablourilor, ci în mai mulţi paşi.
Astfel, vom parcurge lista pana la sfârşit, pentru a putea determina
elementul din mijloc. Apoi stabilim care este elementul din mijloc şi, în final,
izolăm cele două părţi, fiecare în cate o listă în funcţia mergesort():
void f( ) {
mergesort( (nod<int> *)0 );
}
Clasa lmsort<E> foloseşte membrii privaţi atât din clasa lista<E>, cât şi
din clasa nod<E>, deci trebuie declarată friend în ambele.
d fiind o altă constantă. Expresia i2+(n-i-1)2 îşi atinge maximul atunci când i
este 0 sau n-1. Deci,
t ( n) ≤ dn + c + c( n − 1) 2 = cn 2 + c / 2 + n( d − 2c) + 3c / 2
Dacă luăm c = 2d, obţinem t (n) ≤ ci2 + c / 2 . Am arătat că, dacă c este suficient de
mare, atunci t (n) ≤ ci2 + c / 2 pentru orice n ≥ 0, adică, t є O( n 2 ). Analog se arată
ca t єΩ( n 2 ).
Am arătat, totodată, care este cel mai nefavorabil caz: atunci când, la
fiecare nivel de recursivitate, procedura pivot este apelată o singură dată. Dacă
elementele lui T sunt distincte, cazul cel mai nefavorabil este atunci când iniţial
tabloul este ordonat crescător sau descrescător, fiecare partiţionare fiind total
neechilibrată. Pentru acest cel mai nefavorabil caz, am arătat că algoritmul
quicksort necesită un timp în ( n 2 ).
Ce se întâmplă însă în cazul mediu? Intuim faptul că, în acest caz,
subcazurile sunt suficient de echilibrate. Pentru a demonstra aceasta proprietate,
vom arăta că timpul necesar este în ordinul lui n log n, ca şi în cazul cel mai
favorabil.
Presupunem că avem de sortat n elemente distincte şi că iniţial ele pot
să apară cu probabilitate egala în oricare din cele n! permutări posibile. Operaţia
de pivotare necesită un timp liniar. Apelarea procedurii pivot poate poziţiona
primul element cu probabilitatea 1/n în oricare din cele n poziţii. Timpul mediu
pentru quicksort verifica relaţia
n
t (n) ∈ Θ( n) + 1 / n∑ (t (l − 1) + t ( n − 1))
l =1
Mai precis, fie n0 şi d două constante astfel încât pentru orice n > n0,
avem
29
Prin analogie cu mergesort, este rezonabil să presupunem ca t . O(n log n)
şi să aplicăm tehnica inducţiei constructive, căutând o constantă c, astfel încât
t(n) ≤ cn lg n.
Deoarece i lg i este o funcţie nedescrescatoare, avem
n
n −1 n
x2 lg e 2 n2 lg e 2
∑i lg i ≤ ∫ x ln xdx =
2
lg x −
4
x ≤
2
lg n −
4
n
i = n0 x = n0 +1 x =n0 +1
pentru n0 = 1.
Ţinând cont de aceasta margine superioara pentru
n −1
∑ i lg i
i = n0
Rezultă că timpul mediu pentru quicksort este în O(n log n). Pe lângă
ordinul timpului, un rol foarte important îl are constanta multiplicativă. Practic,
constanta multiplicativa pentru quicksort este mai mică decât pentru heapsort
sau mergesort. Dacă pentru cazul cel mai nefavorabil se acceptă o execuţie ceva
mai lenta, atunci, dintre tehnicile de sortare prezentate, quicksort este algoritmul
preferabil.
Pentru a minimiza şansa unui timp de execuţie în .(n2), putem alege ca
pivot mediana şirului T[i], T[(i+j) div 2], T[ j]. Preţul plătit pentru această
modificare este o uşoara creştere a constantei multiplicative.
30
#{i є{1, …, n} | T[i] < m} < .n/2.
este deci echivalentă cu condiţia
p ← pseudomed(T)
unde algoritmul pseudomed este:
function pseudomed(T[1 .. n])
{găseşte o aproximare a medianei lui T}
s ← n div 5
array S[1 .. s]
for i ← 1 to s do S[i] ← adhocmed5(T[5i-4 .. 5i])
return selection(S, (s+1) div 2)
32
În concluzie, m aproximează mediana lui T, fiind al k-lea cel mai mic
element al lui T, unde k este aproximativ intre 3n/10 şi 7n/10. O interpretare
grafică ne va ajuta să înţelegem mai bine aceste relaţii să ne imaginam
elementele lui T dispuse pe cinci linii, cu posibila excepţie a cel mult patru
elemente.
Presupunem că fiecare din cele .n/5. coloane este ordonată
nedescrescător, de sus in jos. De asemenea, presupunem ca linia din mijloc
(corespunzătoare tabloului S din algoritm) este ordonata nedescrescător, de la
stânga la dreapta. Elementul subliniat corespunde atunci medianei lui S, deci lui
m. Elementele din interiorul dreptunghiului sunt mai mici sau egale cu m.
Dreptunghiul conţine aproximativ 3/5 din jumătatea elementelor lui T, deci în
jur de 3n/10 elemente.
Presupunând ca folosim “p . pseudomed(T)”, adică pivotul este
pseudomediana, fie t(n) timpul necesar algoritmului selection, în cazul cel mai
nefavorabil, pentru a găsi al k-lea cel mai mic element al unui tablou de n
elemente. Din inegalitatile
Deducem relaţia
33
pentru n є N + . Prin inducţie constructivă, putem demonstra că exista constantă
reală pozitivă a astfel încât f(n) = an pentru orice n . N. Deci, fєO(n). Pe de alta
parte, există constanta reală pozitivă c, astfel încât t(n) ≤ cf(n) pentru orice n є
N. Este adevărată atunci şi relaţia t є O(n). Deoarece orice algoritm care rezolva
problema selecţiei are timpul de execuţie în Ω(n), rezulta t єΩ.(n), deci, tєΘ(n).
Generalizând, vom încerca să aproximăm mediana nu numai prin
împărţire la cinci, ci prin împărţire la un întreg q oarecare, 1 < q ≤ n. Din nou,
pentru n suficient de mare, tablourile U şi V au cel mult 3n/4 elemente fiecare.
Relatia (*) devine
Dacă 1/q + 3/4 < 1, adică dacă numărul de elemente asupra cărora
operează cele doua apeluri recursive din (**) este în scădere, deducem, intr-un
mod similar cu situaţia când q = 5, ca timpul este tot liniar. Deoarece pentru
orice q = 5 inegalitatea precedentă este verificată, rămâne deschisă problema
alegerii unui q pentru care să obţinem o constanta multiplicativa cât mai mica. În
particular, putem determina mediana unui tablou în timp liniar, atât pentru cazul
mediu cât şi pentru cazul cel mai nefavorabil. Faţă de algoritmul “naiv”, al cărui
timp este în ordinul lui n log n, îmbunătăţirea este substanţială.
34
doi, Alice şi Bob aleg la întâmplare cate un întreg A, respectiv B, mai mici decât
p, fără să-şi comunice aceste numere. Apoi, Alice calculează a = gA mod p şi
transmite rezultatul lui Bob; similar, Bob transmite lui Alice valoarea b = gB
mod p. în final, Alice calculează x = bA mod p, iar Bob calculează y = aB mod
p. Vor ajunge la acelaşi rezultat, deoarece x = y = gAB mod p. Aceasta valoare
este deci cunoscuta de Alice şi Bob, dar rămâne necunoscuta lui Eva. Evident,
nici Alice şi nici Bob nu pot controla direct care va fi aceasta valoare. Deci ei nu
pot folosi acest protocol pentru a schimba in mod direct un anumit mesaj.
Valoarea rezultata poate fi însă cheia unui sistem criptografic convenţional.
Interceptând convorbirea telefonica, Eva va putea cunoaşte în final
următoarele numere: p, q, a şi b. Pentru a-l deduce pe x, ea trebuie să găsească
un întreg A', astfel încât a = gA' mod p şi să procedeze apoi ca Alice pentru a-l
calcula pe x' = bA' mod p. Se poate arata ca x' = x, deci ca Eva poate calcula
astfel corect secretul lui Alice şi Bob.
Calcularea lui A' din p, g şi a este cunoscută ca problema algoritmului
discret şi poate fi realizată de următorul algoritm:
function dlog(g, a, p)
A . 0; k . 1
repeat
A . A+1
k . kg
until (a = k mod p) or (A = p)
return A
function dexpo1(g, A, p)
a.1
for i . 1 to A do a . ag
35
return a mod p
function dexpo2(g, A, p)
a.1
for i . 1 to A do a . ag mod p
return a
divide et impera în care se testează în mod recursiv dacă exponentul curent este
par sau impar.
function dexpo(g, A, p)
if A = 0 then return 1
if A este impar then a . dexpo(g, A-1, p)
return (ag mod p)
else a . dexpo(g, A/2, p)
return (aa mod p)
Dacă M(p) este limita superioară a timpului necesar înmulţirii modulo p a două
numere naturale mai mici decât p, atunci calcularea lui dexpo(g, A, p) necesita
un timp în O(M(p) h(A)). Mai mult, se poate demonstra că timpul este în O(M(p)
log A), ceea ce este rezonabil. Ca şi în cazul căutării binare, algoritmul dexpo
este mai curând un exemplu de simplificare decât de tehnica divide et impera.
Vom înţelege mai bine acest algoritm, dacă consideram şi o versiune
iterativa a lui.
36
function dexpoiter1(g, A, p)
c . 0; a . 1
{fie A A reprezentarea binara a lui A}
for i . k downto 0 do
c . 2c
a . aa mod p
if Ai = 1 then c . c + 1
a . ag mod p
return a
A k k-1... 0
function dexpoiter2(g, A, p)
n . A; y . g; a . 1
while n > 0 do
if n este impar then a . ay mod p
y . yy mod p
n . n div 2
return a
37
3.8. Înmulţirea matricelor
unde
38
Este uşor de verificat ca matricea produs C se obţine astfel:
t ( n) ∈7t (n / 2) + Θ( n 2 )
39
3.9. Înmulţirea numerelor întregi mari
Pentru anumite aplicaţii, trebuie să consideram numere întregi foarte mari. Dacă
aţi implementat algoritmii pentru generarea numerelor lui Fibonacci, probabil că
v-aţi confruntat deja cu aceasta problemă. Acelaşi lucru s-a atunci când s-au
calculat primele 134 de milioane de cifre ale lui π. În criptologie, numerele
întregi mari sunt de asemenea extrem de importante. Operaţiile aritmetice cu
operanzi întregi foarte mari nu mai pot fi efectuate direct prin hardware, deci nu
mai putem presupune, că până acum, ca operaţiile necesită un timp constant.
Reprezentarea operanzilor în virgulă flotantă ar duce la aproximări nedorite.
Suntem nevoiţi deci să implementăm prin software operaţiile aritmetice
respective.
În cele ce urmează, vom da un algoritm divide et impera pentru
înmulţirea întregilor foarte mari. Fie u şi v doi întregi foarte mari, fiecare de n
cifre zecimale (convenim să spunem ca un întreg k are j cifre dacă k < 10 j , chiar
dacă k < 10 j −1 ). Dacă s = n / 2 , reprezentăm pe u şi v astfel:
function inmultire(u, v)
n ← cel mai mic întreg astfel încât u şi v să aibă fiecare n cifre
if n este mic then calculează în mod clasic produsul uv
return produsul uv astfel calculat
s ← n div 2
w ← u div 10 s ; x ← u mod 10 s
y ← v div 10 s ; z ← v mod 10 s
return inmultire(w, y) × 10 2 s
+ (inmultire(w, z)+inmultire(x, y)) × 10 s
+ inmultire(x, z)
40
Înmulţirile sau împărţirile cu 10 2 s şi 10 s , ca şi adunările, sunt executate
într-un timp liniar. Acelaşi lucru este atunci adevărat şi pentru restul împărţirii
întregi, deoarece
u mod 10 s = u - 10 s w, v mod 10 s = v - 10 s y
Notam cu td (n) timpul necesar acestui algoritm, în cazul cel mai nefavorabil,
pentru a înmulţi doi întregi de n cifre. Avem
Deoarece înmulţirea întregilor mari este mult mai lentă decât adunarea,
încercăm să reducem numărul înmulţirilor, chiar dacă prin aceasta mărim
numărul adunărilor. Adică, încercăm să calculăm wy, wz+xy şi xz prin mai puţin
de patru înmulţiri. Considerând produsul r = (w+x)(y+z) = wy + (wz+xy) + xz
observăm ca putem înlocui ultima linie din algoritm cu:
r ← inmult(w+x, y+z)
p ← inmult(w, y); q←inmult(x, z)
return 102sp + 10s(r-p-q) + q
41
Demonstraţi că procedura binsearch se termină într-un număr finit de
paşi (nu ciclează).
i, j ← 1
U[N+1], V[M+1] . +8
for k ←1 to N+M do
if U[i] < V[j] then T[k] ←U[i]
i ←i+1
else T[k] ← V[j]
j . ←j+1
43
procedure funny-sort(T[i .. j])
if T[i] > T[ j] then interschimba T[i] şi T[ j]
if i < j-1 then k ← ( j-i+1) div 3
funny-sort(T[i .. j-k])
funny-sort(T[i+k .. j])
funny-sort(T[i .. j-k])
44
Funcţiile maxim şi minim determina, prin cate o singura comparaţie,
maximul, respectiv minimul, a două elemente.
Putem deduce că atât fmaxmin1, cât şi fmaxmin2 necesită un timp în
È(n)
pentru a găsi minimul şi maximul intr-un tablou de n elemente.
Constanta multiplicativa asociata timpului în cele doua cazuri diferă însă.
Notând cu C(n) numărul de comparaţii între elemente ale tabloului T efectuate
de procedura fmaxmin2, obţinem recurentă
45
tm(n)∈O(n) + max{tm (i) | i ≤ n/2 }
Demonstrati ca tm∈O(n)
a= g A′ mod p = g A mod p
Solutie: x 15 = ((( x 2 ) 2 ) 2 ) 2 x −1
Găsiţi un algoritm divide et impera pentru a calcula un termen oarecare
din şirul lui Fibonacci.
46
Demonstraţi ca algoritmul lui Strassen necesita un timp în O(nlg 7),
folosind de această dată metoda iteraţiei.
Solutie: Fie doua constante pozitive a şi c, astfel încât timpul pentru algoritmul
lui Strassen este:
procedure star(x, y, r)
if r > 0 then star(x-r, y+r, r div 2)
star(x+r, y+r, r div 2)
star(x-r, y-r, r div 2)
star(x+r, y-r, r div 2)
box(x, y, r)
Care este rezultatul, dacă box(x, y, r) apare înaintea celor patru apeluri
recursive?
Arătaţi ca timpul de execuţie pentru un apel star(a, b, c) este în Θ(c 2 ) .
Demonstraţi ca pentru orice întregi m şi n sunt adevărate următoarele
proprietăţi:
47
Elaboraţi un algoritm divide et impera pentru a calcula cel mai mare
divizor comun a doi întregi, evitând calcularea restului împărţirii întregi. Folosiţi
proprietăţile de mai sus.
Găsiţi o structură de date adecvată, pentru a reprezenta numere întregi
mari pe calculator. Pentru un întreg cu n cifre zecimale, numărul de biţi folosiţi
trebuie să fie în ordinul lui n. Înmulţirea şi împărţirea cu o putere pozitiva a lui
10 (sau alta baza, dacă preferaţi) trebuie să poată fi efectuate într-un timp liniar.
Adunarea si scăderea a două numere de n, respectiv m cifre trebuie să
poată fi efectuate intr-un timp în Θ (n+m). Permiteţi numerelor să fie şi negative.
48
Capitolul IV
PROGRAMARE DINAMICĂ
49
proces determinist. În cazul unui proces stohastic, se foloseşte
în mod corespunzător noţiunea de strategie optimă.
Procesele dinamice pot fi continue sau discrete. Un exemplu de
proces discret este următorul: o întreprindere trebuie să-şi
întocmească planul de aprovizionare anual pentru un anumit
material; se consideră 12 perioade (luni) şi pentru fiecare
perioadă se stabileşte cantitatea de aprovizionat, astfel ca pe
întregul an să rezulte un cost total minim. Procesele dinamice
discrete pot avea orizontul limitat (în exemplu de mai sus 12 perioade) sau
nelimitat.
4.1. Introducere
50
2) Venitul rezultat dintr-o activitate nu depinde de alocările făcute în alte
activităţi.
3) Venitul total este egal cu suma veniturilor individuale.
Problema fundamentală constă în a repartiza resursa
între activităţile concurente de aşa manieră încât venitul total
sa fie MAXIM.
51
Obiectivul nostru este acela de a maximiza funcţia
V(x1,x2,…,xN) din (1) pe mulţimea tuturor ALOCĂRILOR (x1,x2,
…,xN) care satisfac restricţia (2) şi condiţiile de nenegativitate
(3).
52
plafonul maxim S - aşa că rezultatele nu vor fi simple mărimi
numerice, ci nişte FUNCŢII reprezentând DEPENDENŢA
VENITULUI MAXIM faţă de volumul de resursă alocată.
În termeni mai precişi, pentru fiecare întreg 1 = n = N şi
fiecare 0 = s = S vom considera problema de alocare unidimensionalã
similară cu (P):
Etapa I (INIŢIALIZARE):
Pentru fiecare 0 = s = S definim f1(s) = g1(s)
53
Etapa n (1<n<N): Definim funcţia fn în fiecare 0 = s = S după
formula (5) şi notăm cu xn*(s) acea valoare a variabilei xn în
care se realizează EFECTIV maximul din (5)
:
Etapa N: Calculăm f (S)= max [f -1(S-xn)+gN(xN)] N N 0 = xN =
S
Etapa finală (de determinare a alocării OPTIMALE (x1*,…,xN*)).
Componentele acesteia se determina DIN APROAPE ÎN APROAPE
“de la sfârşit la început” astfel:
xN*=xN*(S)
Pentru (.) 1 = n = N
xn*=xn*(s - sn), unde sn=xn+1*+…+xN*
EXEMPLU NUMERIC
REZOLVARE:
1. Reprezentaţi în graficul suma investită – profitul realizat
pentru cele 4 pieţe.
2. Dependenţa între profit şi suma investită nu este dată printr-
o expresie analitică ci prin câteva valori corespunzătoare unor
nivele ale investiţiei exprimate prin valori întregi. Din acest
motiv, suma totală va fi împărţită de aşa manieră încât
investiţia în fiecare zonă să fie exprimată tot printr-un număr
întreg, evident nenegativ.
54
Reprezentând grafic dependenţa investiţie - profit
probabil constatăm ca ea nu este liniară şi că pe măsură ce
suma investită creşte, curba corespondentă are tendinţa de
aplatizare ca urmare a efectului de saturare.
Modelul matematic nu diferă de cel prezentat în general decât
prin cerinţa ca variabilele sa ia numai valori întregi, dată fiind
maniera particulară în care s-au dat funcţiile de utilitate. El
este:
Să se determine x*1, x*2, x*3, x*4 care maximizează profitul
Π(x1,x2,x3,x4)=g1(x1)+g2(x2)+g3(x3)+g4(x4)
cu restricţia x1+x2+x3+x4=10
şi condiţia xi ≥ 0, întregi, i=1, 2, 3, 4
unde xi = suma prevăzută a se investi în piaţa i, iar gi(xi) este
profitul probabil rezultat din aceasta investiţie.
Pentru fiecare număr întreg 0 ≤ s ≤10 şi 1 ≤ n ≤ 4 definim fn(s)
= profitul maxim rezultat din investirea a s $ în primele 1, 2,…, n pieţe.
Conform teoriei generale:
55
sosesc la încărcare în containere de diferite mărimi, fiecare
marfă fiind aşezată totuşi în containere de aceeaşi mărime. Un
container în care se află marfa i, i = 1,…, N are o anumită
greutate Wi şi o anumită valoare Vi. Există un plafon W al
greutăţii încărcăturii. Câte containere din fiecare tip de marfă
trebuie încărcate – în limita greutăţii maxime W, astfel încât
valoarea încărcăturii sa fie maximă?
Modelul matematic
n = N se calculează numai
Exemplu numeric:
APLICAŢIE
57
Programul următor este realizat în Turbo Pascal şi prezintă toate
posibilităţile de ieşire a unei bile din centrul unei table de şah cu înălţimi
diferite.
uses crt,graph,menu,windows;
const NMAX=50;
Length=420;
Recursiv=false;
NDir=8;
dir_lin:array[1..NDir] of integer=(-1,-1, 0, 1, 1, 1, 0,-1);
dir_col:array[1..NDir] of integer=( 0, 1, 1, 1, 0,-1,-1,-1);
type hights=array[1..NMAX,1..NMAX] of word;
var open:boolean;
n:byte;
tablou:hights;
poz_lin,poz_col:byte;
cel:word;
solution,more_solution:boolean;
trace:array[1..5000,1..2] of word;
a:array[1..5000] of byte;
{Initializeaza modul grafic, deseneaza spatiul de lucru (chenar, butoane)}
procedure initgraf;
var gm,mode:integer;
i,j:byte;
begin
gm:=detect;
initgraph(gm,mode,'');
if graphresult<>grok then
begin
writeln('Eroare la initializarea modului grafic!');
halt;
end;
port[$60]:=243;
delay(1000);
port[$60]:=0;
fastwindow(0,0,getmaxx,getmaxy,10);
panel(12,12,getmaxx-23,getmaxy-23);
{frame(20,20,440,440,5);}
frame(470,20,150,60,5);
shadowwrite(480,30,'Program realizat');
shadowwrite(520,45,'de');
shadowwrite(485,60,' VATIA ALIN');
frame(520,100,70,130,5);
butf:=nil;
creazabuton(530,110,30,green,'Open',cmopen);
creazabuton(530,150,30,green,'Run',cmrun);
58
creazabuton(530,190,30,green,'Exit',cmies);
scriebutoane;
open:=false;
end;
{Afiseaza un mesaj pe ecran si asteapta apasarea unei taste}
procedure mesaj(text:string;x,y:word);
var p:pointer;
dx,dy,size:word;
ch:char;
begin
dx:=textwidth(text)+20;
dy:=textheight(text)+10;
size:=imagesize(x,y,x+dx,x+dy);
getmem(p,size);
getimage(x,y,x+dx,x+dy,p^);
panel(x,y,dx,dy);
shadowwrite(x+10,y+5,text);
ch:=readkey;
if ch=#0 then ch:=readkey;
putimage(x,y,p^,normalput);
end;
{Afiseaza numele fisierului curent}
procedure nume_fisier(name:string);
begin
setfillstyle(solidfill,_darkgray);
bar(480,400,600,450);
hole(480,400,120,50);
shadowwrite(490,410,name);
end;
{Afiseaza bila}
procedure bila(l,c:word);
var x,y:word;
begin
x:=27+cel*(c-1);
y:=27+cel*(l-1);
pieslice(x+cel div 2,y+cel div 2,0,360,cel div 2);
end;
{Afiseaza reteaua}
procedure grid(x,y:word);
var i,j:word;
begin
setfillstyle(solidfill,_darkgray);
bar(20,20,470,460);
cel:=Length div n;
frame(20,20,cel*n+14,cel*n+14,5);
setcolor(0);
for i:=1 to n do
for j:=1 to n do
rectangle(27+cel*(i-1),27+cel*(j-1),27+cel*i,27+cel*j);
bila(poz_lin,poz_col);
end;
{Citeste datele din fisier}
59
procedure citire_date(name:string);
var f:text;
i,j:byte;
begin
assign(f,name);
{$I-}
reset(f);
{$I+}
if ioresult<>0 then
begin
{file not found}
mesaj('Fisierul nu poate fi deschis!',100,100);
exit;
end;
readln(f,n,poz_lin,poz_col);
for i:=1 to n do
for j:=1 to n do
read(f,tablou[i,j]);
close(f);
open:=true;
nume_fisier(name);
grid(20,20);
end;
{Se afiseaza o solutie}
procedure afisare(m:word);
var i,x,y:word;
ch:char;
len:byte;
sir:string[10];
begin
solution:=true;
for i:=2 to m do
begin
x:=27+cel*(trace[i,2]-1);
y:=27+cel*(trace[i,1]-1);
setfillstyle(solidfill,red);
bar(x+1,y+1,x+cel-2,y+cel-2);
str(i-1,sir);
len:=textwidth(sir);
x:=27+cel*(trace[i,2]-1)+(cel-len) div 2;
len:=textheight(sir);
y:=27+cel*(trace[i,1]-1)+(cel-len) div 2;
outtextxy(x,y,sir);
end;
ch:=readkey;
if ch=#0 then ch:=readkey;
if ch=#27 then more_solution:=false;
for i:=2 to m do
begin
x:=27+cel*(trace[i,2]-1);
y:=27+cel*(trace[i,1]-1);
setfillstyle(solidfill,_darkgray);
60
bar(x+1,y+1,x+cel-2,y+cel-2);
end;
end;
{Backtracking nerecursiv}
procedure back_normal;
var i:word;
gata:boolean;
begin
trace[1,1]:=poz_lin;trace[1,2]:=poz_col;
i:=2;a[i]:=0;
while (i>1) do
begin
gata:=false;
while (a[i]+1<=NDir) and not(gata) do
begin
a[i]:=a[i]+1;
trace[i,1]:=trace[i-1,1]+dir_lin[a[i]];
trace[i,2]:=trace[i-1,2]+dir_col[a[i]];
if (tablou[trace[i,1],trace[i,2]]<tablou[trace[i-1,1],trace[i-1,2]]) then gata:=true;
end;
if gata then
if (trace[i,1]=1) or (trace[i,1]=n) or (trace[i,2]=1) or (trace[i,2]=n) then afisare(i)
else
begin
inc(i);
a[i]:=0;
end
else i:=i-1;
if not(more_solution) then break;
end;
end;
{Backtracking recursiv}
procedure back(l,c,step:word);
var ln,cn:word;
i:byte;
begin
trace[step,1]:=l;
trace[step,2]:=c;
if (l=1) or (c=1) or (l=n) or (c=n) then afisare(step)
else
begin
for i:=1 to NDir do
begin
ln:=l+dir_lin[i];
cn:=c+dir_col[i];
if more_solution and (tablou[ln,cn]<tablou[l,c]) then back(ln,cn,step+1);
if not(more_solution) then break;
end;
end;
end;
{Apeleaza rezolvarea problemei fie prin metoda recursiva fie cea normala}
61
procedure run;
begin
solution:=false;
more_solution:=true;
if Recursiv then back(poz_lin,poz_col,1)
else back_normal;
if not(solution) then mesaj('Nu avem solutie!',100,100)
else mesaj('S-au terminat solutiile',100,100);
end;
{Prelucreaza evenimentele aparute de la tastatura}
procedure work(var cmd:word);
var name:string;
begin
case cmd of
cmopen:begin
name:=inputf('Nume fisier:',100,100,12);
if name<>'.' then
citire_date(name);
end;
cmrun:if not(open) then mesaj('Nu avem date de intrare!',100,100)
else run;
cmies:;
end;
end;
begin
initgraf;
repeat
getevent(comand);
prelbut(comand);
if comand<>nocomand then work(comand);
until comand=cmies;
iesire;
end.
62
63