Sunteți pe pagina 1din 12

Algoritmi si structuri de date

Curs 13

Metode si tehnici de programare


Programarea Dinamic
a
Daca n cazul tehnicii Backtracking, de exemplu, se pot identifica foarte simplu problemele ce se
pot rezolva folosind aceasta metoda, nu acelasi lucru se poate spune si despre metoda programarii
dinamice. Acesta tehnica de proiectare a algoritmilor presupune rezolvarea unei probleme prin
descompunerea ei n subprobleme de dimensiune mai mica. Spre deosebire de metoda Divide
et Impera, unde subproblemele rezultate trebuie sa fie independente, n cadrul acestei metode
subproblemele care apar n descompunere nu sunt independente. Acestea se suprapun, astfel
ca solutia unei subprobleme se utilizeaza n construirea solutiilor altor subprobleme. In cazul
n care subproblemele se suprapun si s-ar folosi metoda Divide et Impera, s-ar efectua calcule
redundante, rezolvandu-se fiecare subproblema ca si cand aceasta nu ar mai fost ntalnita pana
atunci. Programarea dinamica nsa, rezolva fiecare dintre subprobleme o singura data si memoreaza solutia acesteia ntr-o structura de date, evitand astfel rezolvarea redundanta a aceleiasi
probleme.
In denumirea metodei, cuvantul programare nu se refera la scrierea de cod ntr-un anumit limbaj
informatic, ci la programarea matematica care consta n optimizarea unei functii obiectiv prin
alegerea de valori dintr-o multime anume, iar cuvantul dinamic se refera la maniera n care sunt
construite structurile n care se stocheaza solutiile subproblemelor.
Metoda programarii dinamice se foloseste n rezolvarea problemelor de optimizare. Desi o astfel
de problema poate avea mai multe solutii optime, metoda furnizeaza doar una singura. Pentru a genera toate solutiile optime, trebuie sa o combinam cu tehnica Backtracking. Deoarce
metoda programarii dinamice gaseste o solutie fara a genera toate solutiile posibile, putem gasi o
asemanare cu metoda Greedy, ambele metode exploatand proprietatea de substructura optima a
problemei (orice solutie optima a problemei initiale contine o solutie optima a unui subprobleme,
deci solutia optima a problemei se obtine prin combinarea solutiilor optime ale subproblemelor).
Rezolvarea unei probleme folosind metoda programarii dinamice presupune urmatoarele etape:
1. se identifica subproblemele n care poate fi mpartita problema globala; se stabileste modul
n care solutia problemei depinde de solutiile subproblemelor (aceasta etapa consta n
verificarea proprietatii de substructura optima);
2. se gasesc relatii de recurenta care leaga solutiile subproblemelor ntre ele si de solutia
problemei globale;
3. se dezvolta relatia de recurenta si se retin rezultatele partiale ntr-o structura de date
(tablou unidimensional, bidimensional etc.), daca acest lucru este necesar;
4. se reconstituie solutia pornind de la datele deja memorate.
Dupa determinarea solutiilor subproblemelor, se poate determina efectiv solutia problemei globale. Reconstructia solutiei problemei se face de jos n sus, mergand din subproblema n subproblema, ncepand de la problema cu valoarea optima si ajungand la subproblemele elementare.

Algoritmi si structuri de date

Curs 13

Exista trei abordari n dezvoltarea relatiilor de recurenta:


descendenta, n care valoarea de calculat se exprima prin valori calculate anterior. Aceasta
abordare se implementeaza, de regula, recursiv si, de cele mai multe ori, este ineficienta;
folosind aceasta abordare se rezolva doar subproblemele ce contribuie la solutia problemei,
nsa o subproblema este rezolvata de mai multe ori, de cate ori aceasta apare;
ascendenta, se porneste de la cazul elementar si se genereaza noi valori pe baza celor
deja calculate, conform formulei de recurenta; folosind aceasta abordare se rezolva toate
subproblemele, indiferent daca contribuie sau nu la solutia problemei, nsa o subproblema
este rezolvata o singura data;
combinata (se combina abordarea ascendenta cu cea descendenta tehnica memoizarii).
Prin aceasta abordare se rezolva doar subproblemele care contribuie la solutia problemei
si doar o singura data .
Aplicatia 1. Calculul coeficientilor binomiali C(n, k).
formula de recurenta:

0
C(n, k) = 1

C(n 1, k) + C(n 1, k 1)

Pentru rezolvarea problemei, amintim

, daca n < k,
, daca k = 0 sau k = n,
, altfel.

Varianta de implementare a formulei folosind o functie recursiva nu este recomandata, fiind una
redundanta, aceeasi valoare fiind calculata de mai multe ori (complexitate exponentiala). Alegem
sa folosim metoda programarii dinamice, valorile coeficientilor binomiali calculandu-se progresiv.
Aceste valori vor fi salvate ntr-o matrice patratica de ordin n + 1, C. Doar partea inferioara
a matricei mpreuna cu diagonala sa principala vor fi ncarcate. Vom construi triunghiului lui
Pascal pana la o pereche data (n, k). Complexitatea algoritmului este de ordinul O(nk). Daca
se cere sa se calculeze doar valoarea C(n, k) se poate folosi ca spatiu de stocare un tablou
unidimensional de dimensiune k.
#include<i o s t r e a m >
#include<f s t r e a m >
#define dim 100
using namespace s t d ;
int main ( )
{
unsigned n , k ;
unsigned C[ dim ] [ dim ] ;
cout<<n : ;
c i n >>n ;
cout<<k , k <= n : ;
c i n >>k ;
while ( k > n )
{
2

Algoritmi si structuri de date

Curs 13

cout<<k , k <= n : ;
c i n >>k ;
}
ofstream o u t f i l e ( combinari . txt ) ;
C[ 0 ] [ 0 ] = 1;
o u t f i l e <<C[ 0 ] [ 0] < < e n d l ;
for ( unsigned i = 1 ; i <= n ; i ++)
{
for ( unsigned j = 0 ; j <= i ; j ++)
{
if ( j = = 0 || j = = i )
C[ i ] [ j ] = 1 ;
else
C[ i ] [ j ] = C[ i 1 ] [ j 1] + C[ i 1 ] [ j ] ;
o u t f i l e <<C[ i ] [ j ]<< ;
}
o u t f i l e <<e n d l ;
}
o u t f i l e <<endl<<Am g e n e r a t t r i u n g h i u l l u i P a s c a l pana l a
<< p e r e c h e a data ( <<n<< , <<k<< ) <<e n d l ;
o u t f i l e <<C( <<n<< , <<k<< ) = <<C[ n ] [ k]<< e n d l ;
outfile . close ();
return 0 ;
}
Astfel, pentru n = 10 si k = 6, obtinem
combinari.txt
1
1
1
1
1
1
1
1
1
1
1

1
2 1
3 3 1
4 6 4 1
5 10 10 5 1
6 15 20 15 6 1
7 21 35 35 21 7 1
8 28 56 70 56 28 8 1
9 36 84 126 126 84 36 9 1
10 45 120 210 252 210 120 45 10 1

Am generat triunghiul lui Pascal pana la pereche data (10,6)


C(10,6) = 210
Aplicatia 2. O cladire este formata din camere dispuse ntr-un grid cu m n elemente. Intre
doua camere se poate trece doar daca acestea sunt alaturate pe linie sau pe coloana. La iesirea
din camera (m, n) se afla o comoara. Un cautator de comori intra n cladire prin camera (1, 1).
3

Algoritmi si structuri de date

Curs 13

Pentru fiecare camera n care intra i se cere sa plateasca o taxa (numar natural). Determinati
suma minima pe care trebuie sa o plateasca cautatorul de comori pentru a ajunge la comoara
mult dorita.
Vom pastra ntr-o matrice A = (aij ), i = 1, m, j = 1, n, taxele care trebuie platite la trecerea
prin fiecare camera (i, j). In matricea S = (sij ), i = 1, m, j = 1, n, vom calcula sumele partiale
de plata prin parcurgerea drumului de la camera (1, 1) la camera (i, j). Astfel, obtinem formula
de recurenta:

ai,j
, daca i = 1 si j = 1,

s
, daca i 2 si j = 1,
i1,j + ai,j
si,j =

si,j1 + ai,j
, daca j 2 si i = 1,

minim(si1,j ; si,j1 ) + ai,j , altfel.


Solutia optimala a problemei va fi calculata n sm,n .
Datele de intrare se citesc din fisierul comoara.txt astfel: pe prima linie sunt numerele naturale
m si n, separate prin spatii. Apoi pe fiecare din cele m linii, sunt n numere naturale, separate
prin spatii, corespunzatoare taxelor aferente fiecarei camere.
Data de iesire, solutia optimala a problemei, se va scrie n acelasi fisier, la sfarsit.
#include<i o s t r e a m >
#include<f s t r e a m >
using namespace s t d ;
int a [ 1 0 0 ] [ 1 0 0 ] , s [ 1 0 0 ] [ 1 0 0 ] , m, n ;
o f s t r e a m o u t f i l e ( comoara . t x t , i o s : : app ) ;
int min ( int a , int b )
{
i f ( a < b ) return a ;
e l s e return b ;
}
void comoara ( )
{
int i , j ;
s [0][0] = a [0][0];
for ( j = 1 ; j < n ; j ++)
s [ 0 ] [ j ] = s [ 0 ] [ j 1] + a [ 0 ] [ j ] ;
for ( i = 1 ; i < m; i ++)
s [ i ] [ 0 ] = s [ i 1][0] + a [ i ] [ 0 ] ;
for ( i = 1 ; i < m; i ++)
for ( j = 1 ; j < n ; j ++)
s [ i ] [ j ] = min ( s [ i 1 ] [ j ] , s [ i ] [ j 1]) + a [ i ] [ j ] ;
o u t f i l e <<Suma minima de p l a t a : <<s [m 1 ] [ n1]<<e n d l ;
}
int main ( )
4

Algoritmi si structuri de date

Curs 13

{
i f s t r e a m i n f i l e ( comoara . t x t ) ;
if (! infile )
{
c e r r << Eroare l a d e s c h i d e r e a f i s i e r u l u i <<e n d l ;
exit (1);
}
int i , j ;
i n f i l e >>m>>n ;
for ( i = 0 ; i < m; i ++)
for ( j = 0 ; j < n ; j ++)
i n f i l e >>a [ i ] [ j ] ;
i n f i l e . close ();
comoara ( ) ;
outfile . close ();
return 0 ;
}
De exemplu,
4
5
7
2
3

3
7
9
6
2

4
3
1
8

Suma minima de plata: 27


Aplicatia 3. Problema rucsacului, varianta n care nu se permite fractionarea obiectelor. Avem
la dispozitie n obiecte, fiecare cu o anumita greutate gi si un anumit castig ci , i = 1, n, care
s-ar obtine prin transportul obiectului i n ntregime. Se pune problema ce obiecte sa selectam
pentru a le transporta cu un rucsac care permite o greutate maxima G, astfel ncat sa obtinem
un castig maxim.
Am vazut ca, n acest caz, n care nu este permisa fractionarea obiectelor, metoda Greedy nu ne
furnizeaza ntotdeauna solutia optima.
Presupunem ca macar un obiect are greutatea mai mica decat G. Vom folosi tabloul bidimensional D, cu n + 1 linii si G + 1 coloane, n care vom pastra solutiile subproblemelor: D[i][j]
este cel mai bun castig obtinut pentru primele i obiecte, avand greutatea nsumata j. Solutia
se construieste astfel: daca D[n][G] este castigul maxim obtinut prin selectarea unei submultimi
de obiecte din cele n disponibile, care nu depasesc greutatea totala care poate fi transportata n
rucsac, G, atunci relatia de recurenta este:
D[n][G] = maxim(D[n 1][G], D[n 1][G gn ] + cn ).
Astfel, castigul maxim se obtine fie fara a adauga obiectul n, ramanand la castigul obtinut pentru
n 1 obiecte, fie prin adaugarea obiectului n, caz n care se adauga la castigul obtinut pentru
5

Algoritmi si structuri de date

Curs 13

n1 obiecte si greutate Ggn , cel rezultat prin transportul obiectului n. Ideea algoritmului este
deci urmatoarea: la fiecare pas, la solutia curenta ori nu adaugam deloc obiectul i, si ramanem
la castigul obtinut pentru i 1 obiecte, ori adaugam obiectul i, caz n care adaugam castigul
rezultat prin transportul lui la cel obtinut pentru primele i1 obiecte si greutate j gi . Obtinem
formula de recurenta:

, daca i = 0 sau j = 0,
0
D[i][j] = D[i 1][j]
, daca i > 0 si j < gi ,

maxim(D[i 1][j], D[i 1][j gi ] + ci ) , daca i > 0 si j gi .


#include<i o s t r e a m >
#include<f s t r e a m >
#define dim 100
using namespace s t d ;
int D[ dim ] [ dim ] , g [ dim ] , c [ dim ] , n , G, s o l [ dim ] , S [ dim ] [ dim ] ;
o f s t r e a m f o u t ( r u c s a c . t x t , i o s : : app ) ;
int max( int a , int b )
{
i f ( a > b ) return a ;
e l s e return b ;
}
// v a r i a n t a a s c e n d e n t a
void r u c s a c 1 ( )
{
int i , j ;
// t a b l o u l D are n+1 l i n i i s i G+1 c o l o a n e
for ( i = 0 ; i < n ; i ++)
{
for ( j = 0 ; j <= G; j ++)
if ( j < g[ i ])
D[ i + 1 ] [ j ] = D[ i ] [ j ] ;
else
D[ i + 1 ] [ j ] = max(D[ i ] [ j ] , D[ i ] [ jg [ i ] ] + c [ i ] ) ;
}
f o u t << C a s t i g u l t o t a l : <<D[ n ] [ G]<< e n d l ;
}
// t e h n i c a m e m o i z a r i i
int r u c s a c 2 ( int i , int j )
{
// implementare r e c u r s i v a
// v a l o r i l e i n t e r m e d i a r e n e c e s a r e o b t i n e r i i s o l u t i e i s e s t o c h e a z a
i f ( i = = 0 | | j = = 0)
6

Algoritmi si structuri de date

Curs 13

{
S [ i ] [ j ] = 0;
return S [ i ] [ j ] ;
}
else
i f ( S [ i ] [ j ] != 1)
return S [ i ] [ j ] ;
else
{
if ( j < g[ i ])
S [ i ] [ j ] = r u c s a c 2 ( i 1, j ) ;
else
S [ i ] [ j ] = max( r u c s a c 2 ( i 1, j ) ,
r u c s a c 2 ( i 1, jg [ i ] ) + c [ i ] ) ;
return S [ i ] [ j ] ;
}
}
void c o n s t r u c t S o l ( )
{
int i = n , j = G;
while (D[ i ] [ j ] >0)
{
i f (D[ i ] [ j ] = = D[ i 1 ] [ j ] )
i = i 1;
else
{
s o l [ i 1] = 1 ;
j = j g [ i 1];
i = i 1;
}
}
}
int main ( )
{
ifstream f i n ( rucsac . txt ) ;
if (! fin )
{
cout<< Eroare l a d e s c h i d e r e a f i s i e r u l u i . <<e n d l ;
exit (1);
}
f i n >>n ;
f i n >>G ;
for ( int i = 0 ; i < n ; i++ )
f i n >>g [ i ] ;
7

Algoritmi si structuri de date

Curs 13

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


f i n >>c [ i ] ;
fin . close ();
f o u t << 1 . V a r i a n t a a s c e n d e n t a . <<e n d l ;
rucsac1 ( ) ;
f o u t << O b i e c t e l e s e l e c t a t e : ;
constructSol ( ) ;
for ( int i = 0 ; i < n ; i ++)
if ( sol [ i ])
f o u t <<i+1<< ;
f o u t <<e n d l ;
f o u t <<endl<< 2 . Tehnica m e m o i z a r i i . <<e n d l ;
// i n i t i a l i z a r e a m a t r i c e i cu o v a l o a r e v i r t u a l a
for ( int i = 0 ; i <= n ; i++ )
for ( int j = 0 ; j <= G; j ++)
S [ i ] [ j ] = 1;
int optim = r u c s a c 2 ( n , G) ;
f o u t << C a s t i g u l t o t a l : <<optim<<e n d l ;
f o u t <<e n d l ;
fout . close ( ) ;
return 0 ;
}
Pentru datele de intrare: n = 3, G = 7, g1 = 5, g2 = 3, g3 = 4, c1 = 6, c2 = 3, c3 = 4, am vazut
ca metoda Greedy nu ofera solutia optima. Algoritmul implementat prin tehnica programarii
dinamice ne conduce la solutia optima a problemei.
rucsac.txt
3 7
5 3 4
6 3 4
1. Varianta ascendenta:
Castigul total: 7
Obiectele selectate: 2 3
2. Tehnica memoizarii:
Castigul total: 7
Aplicatia 4. Plata restului cu un numar minim de bancnote. Un vanzator trebuie sa dea
rest unui client o suma de bani S, avand la dispozitie bancnote de valori b1 , b2 , ..., bn n numar
nelimitat. Stiind ca vanzatorul intentioneaza sa foloseasca un numar cat mai mic de bancnote si
ca are la dispozitie ntotdeauna bancnote de valoare 1, sa se afiseze modalitatea optima de plata
a restului.
8

Algoritmi si structuri de date

Curs 13

Vom folosi tabloul unidimensional C cu S +1 componente pentru a salva solutiile subproblemelor:


C[j] reprezinta numarul minim de bancnote de tipul b1 , b2 , ..., bn folosite pentru plata sumei j.
Numarul minim de bancnote necesar platii sumei S (solutia optimala a problemei) va fi calculat
n C[S]. Daca pentru plata optima a sumei j se foloseste o bancnota de tipul bi , atunci numarul
de bancnote creste cu o unitate si se reduce corespunzator suma de plata. Astfel vom avea
C[j] = 1 + C[j bi ].
Pentru fiecare suma ramasa de plata j, j = 0, S, alegem acea bancnota bi din cele n tipuri de
bancnote, care satisface restrictia bi j si care minimizeaza 1 + C[j bi ]. Evident, daca suma
de plata este 0, atunci solutia optima a problemei este 0.
Obtinem astfel formula recursiva de calcul:
(
0
, daca j = 0,
C[j] =
minimi:bi j (1 + C[j bi ]) , daca j 1.
Pe masura ce se completeaza solutia optima, tipul bancnotei folosite este pastrat ntr-un tablou
suplimentar. Complexitatea algoritmului este de ordinul lui O(nS).
#include<i o s t r e a m >
#include<f s t r e a m >
#define dim 500
using namespace s t d ;
int C[ dim ] , b [ dim ] , n , S , s [ dim ] ;
o f s t r e a m f o u t ( r e s t . t x t , i o s : : app ) ;
int min ( int a , int b )
{
i f ( a < b ) return a ;
e l s e return b ;
}
void p l a t a S ( )
{
int i , j ;
// t a b l o u l C are S+1 e l e m e n t e
C[ 0 ] = 0;
for ( j = 1 ; j <= S ; j ++)
{
C[ j ] = INT MAX ;
for ( i = 0 ; i < n ; i ++)
i f ( b [ i ] <= j && 1 + C[ jb [ i ] ] < C[ j ] )
{
C[ j ] = 1 + C[ jb [ i ] ] ;
s [ j ] = b[ i ];
}
9

Algoritmi si structuri de date

Curs 13

}
f o u t <<Numarul minim de bancnote : <<C[ S]<< e n d l ;
}
void c o n s t r u c t S o l ( int j )
{
i f ( j > 0)
{
constructSol ( j s [ j ] ) ;
f o u t <<s [ j ]<< ;
}
}
int main ( )
{
ifstream f i n ( rest . txt ) ;
if (! fin )
{
cout<< Eroare l a d e s c h i d e r e a f i s i e r u l u i . <<e n d l ;
exit (1);
}
f i n >>n ;
f i n >>S ;
for ( int i = 0 ; i < n ; i++ )
f i n >>b [ i ] ;
fin . close ();
plataS ( ) ;
f o u t << B a n c n o t e l e s e l e c t a t e : ;
constructSol (S ) ;
fout . close ( ) ;
return 0 ;
}
Daca vrem sa platim suma S = 129, cu un numar minim de bancnote de tipul b1 = 12, b2 = 5,
b3 = 1 si b4 = 25, algoritmul Greedy corespunzator nu ne furnizeaza solutia optima. In schimb,
algoritmul proiectat folosind programarea dinamica construieste solutia optima a problemei.
rest.txt
4 129
12 5 1 25
Numarul minim de bancnote: 7
Bancnotele selectate: 25 25 25 25 5 12 12
Aplicatia 5. Cel mai lung subsir ordonat crescator (CMLSC) al unui sir oarecare. Se considera
un sir v oarecare de n elemente numere ntregi, v1 , v2 , ..., vn . Se cere sa se determine CMLSC al
10

Algoritmi si structuri de date

Curs 13

sirului.
Pentru a calcula lungimea CMLSC vom calcula mai ntai lungimile celor mai lungi subsiruri
crescatoare care ncep cu fiecare element al vectorului. Vom memora n L[k] lungimea CMLSC
care ncepe de la pozitia k si pana la sfarsitul sirului initial. Tabloul unidimensional L are n
elemente. Pentru ultimul element, L[n 1] = 1. Calculam apoi pe rand L[n 2], ..., L[1], L[0].
Lungimea CMLSC va fi data de cea mai mare valoare a vectorului L.
Vrem sa calculam L[k]. Pentru aceasta, trebuie mai ntai sa calculam lungimea CMLSC din
dreapta lui k, subsir al carui prim element este mai mare sau egal cu elementul de pe pozitia
k. Dar aceasta lungime o putem determina n acelasi mod. Obtinem urmatoarea formula de
recurenta:
(
1
, k = n 1,
L[k] =
1 + maximi:k<i<n,v[k]v[i] L[i] , k = 0, 1, ..., n 2.
Pentru a afisa si elementele CMLSC, folosim un vector suplimentar, next, n care retinem pe
pozitia k indexul primului element al subsirului folosit pentru calcularea lui L[k].
#include<i o s t r e a m >
using namespace s t d ;
void CMLSC( int v , int n )
{
int Lmax , s t a r t , i , k ;
int L = new int [ n ] ;
int next = new int [ n ] ;
L [ n1] = 1 ;
Lmax = 1 ;
s t a r t = n1;
next [ n1] = n1;
for ( k = n 2 ; k >= 0 ; k)
{
L[ k ] = 1;
next [ k ] = k ;
for ( i = k + 1 ; i < n ; i ++)
i f ( v [ k ] <= v [ i ] && L [ k ] <= L [ i ] )
{
L[ k ] = L[ i ] + 1;
next [ k ] = i ;
}
i f (L [ k ] > Lmax)
{
Lmax = L [ k ] ;
s t a r t = k ; // p o z i t i a de i n c e p u t a s u b s i r u l u i
}
}
cout<< Lungimea CMLSC: <<Lmax<<e n d l ;
cout<< Acesta e s t e : <<v [ s t a r t ]<< ;
11

Algoritmi si structuri de date

Curs 13

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


{
s t a r t = next [ s t a r t ] ;
cout<<v [ s t a r t ]<< ;
}
cout<<e n d l ;
delete L ;
delete next ;
}

int main ( )
{
int n , v ;
cout<< Lungimea s i r u l u i : ;
c i n >>n ;
v = new int [ n ] ;
cout<< E l e m e n t e l e s i r u l u i : ;
for ( int i = 0 ; i < n ; i++ )
c i n >>v [ i ] ;
CMLSC( v , n ) ;
return 0 ;
}
De exemplu, pentru n = 15 si v[ ] = {34, 1, 3, 5, 45, 23, 0, 5, 39, 40, 234, 7, 10, 56, 45}, obtinem
Lmax = 7 si CMLSC: {1, 3, 5, 23, 39, 40, 234}.

12

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