Sunteți pe pagina 1din 12

Metoda Programării Dinamice

Metoda programării dinamice este aplicabilă problemelor de optim în


care soluţia este privită ca un rezultat al unui şir de decizii d1,d2, …,dn.
Fiecare decizie depinde de deciziile deja luate şi nu este unic
determinată.
d1 d2 d3 d4 dn-1 dn

S0 S1 S2 S3 S4 Sn-1 Sn

Pentru aplicarea metodei este necesară satisfacerea principiului


optimalităţii.

Dacă d1,d2, …,dn este un şir de decizii care conduce sistemul în


mod optim din starea iniţială S0 în starea finală Sn trecând prin stările
intermediare S1, …,Sn-1, atunci trebuie îndeplinită una din condiţiile
următoare (principiul de optimalitate):

1. dk, …,dn este un şir de decizii ce conduce optim sistemul din


starea Sk-1 în starea Sn , ∀ k ∈ {1,2, …, n};
2. d1,d2, …,dk este un şir de decizii care conduce optim sistemul din
starea S0 în starea Sk-1 , ∀ k∈ {1,2, …, n};
3. dk+1, …, dn , d1,d2, …,dk sunt şiruri de decizii care conduc optim
sistemul din starea Sk în starea Sn, respectiv din S0 în starea Sk-1 ,
∀ k ∈ {1,2, …, n}.

Dacă principiul de optimalitate se verifică în forma 1, spunem că se


aplică programarea dinamică metoda înainte.
Dacă principiul de optimalitate se verifică în forma 2, spunem că se
aplică programarea dinamică metoda înapoi.
Dacă principiul de optimalitate se verifică în forma 3, spunem că se
aplică programarea dinamică metoda mixtă.

1
Programarea Dinamică reprezintă o metodă de rezolvare a problemelor pentru care soluţia
poate fi privită ca rezultatul a unor decizii (D1,D2,D3,...,Dn). Fiecare decizie trebuie să
satisfacă principiul optimalităţii.

Luarea deciziilor poate fi realizată în două moduri:

a) decizia Dx depinde de deciziile Dx+1,...,Dn. Spunem că se aplică metoda Inainte. Deciziile


se iau în ordinea : Dn, Dn-1,..., D1.

b) decizia Dx depinde de deciziile D1, D2,..., Dx-1. In acest caz se aplică metoda Inapoi.
Deciziile se iau în ordinea : D1, D2,..., Dn.

Programarea dinamică se poate aplica problemelor la care optimul


general implică optimul parţial.

În programarea dinamică se rezolvă problema combinând soluţiile


subproblemelor ca şi la metoda Divide et Impera. Când subproblemele
conţin subprobleme comune, în locul metodei Divide et Impera este
mai avantajos să aplicăm metoda programării dinamice. Să vedem ce
se întâmplă cu un algoritm Divide et Impera în această situaţie.
Descompunerea recursivă a cazurilor în subcazuri ale aceleiaşi
probleme care apoi sunt rezolvate independent, poate conduce la
calcularea de mai multe ori a aceluiaşi subcaz şi deci conduce la o
scădere a eficienţei algoritmilor. Ca exemplu justificativ, să calculăm
coeficientul biomial

C kn =C n−k1 + C kn−−11 sau, altfel spus:

 k − 1  k 
 k   + , 0 < k < n
  =  n − 1  n − 1
n 
1, in rest

care, în mod direct conduce la următorul algoritm, scris în pseudocod:

2
function C(n,k)
DACA (k=0) or (k=n) ATUNCI return 1
ALTFEL return C(n-1,k-1)+C(n-1,k)
SFDACA

De ce nu s-a folosit formula C nk = n!


k ! ( n − k )! ?

Evident că pe lângă faptul că în prima soluţie se fac numai adunări, mai


avem avantajul că algoritmul se poate programa fără dificultăţi pentru
valori mai mari ale lui n, k. Această rezolvare este imediată, dar ea are
şi un dezavantaj: o mare parte din valorile C(i,j), cu i<n, j<k sunt
calculate în mod repetat.
De exemplu, pentru: C(12,7), funcţia C(1,1) a fost invocată de 210 ori,
iar C(2,2) de 126 ori.

Rezultatele intermediare se vor memora într-un tablou de forma:


0 1 2 3 .... k −1 k
0 1
1 1 1
2 1 2 1
3 1 3 3 1
...
 k − 1 k 
n −1    
 n − 1  n − 1
k 
n  
n

Dacă rezultatele intermediare se vor memora în tabloul de mai


sus, se va obţine un algoritm mai eficient pentru că este suficient să
construim un vector de lungime k pe care îl actualizăm de la dreapta la
stânga. Funcţia de calcul este:

3
int comb(int n, int k)
{
int i,j;
int c[20][20];
for( i=0; i<=n ;i++)
{ c[i][0]=1;
c[i][i]=1;
for( j=1;j<= i-1 ;j++)
c[i][j]=c[i-1][j-1]+c[i-1][j];
}
return c[n][k];
}

Se observă că nu avem nevoie de întreaga matrice. La fiecare pas sunt


necesare doar ultimele două linii. S-a ajuns astfel la primul principiu al
programării dinamice: evitarea calculării de mai multe ori a unor
subcazuri prin memorarea rezultatelor intermediare.

Metoda Divide et Impera operează de sus în jos (top-down),


descompunând cazuri în subcazuri pe care le rezolvă separat. Ajungem
astfel la al doilea principiu al programării dinamice: metoda
programării dinamice operează de jos în sus (bottom-up). Se porneşte
de la cele mai mici subcazuri, se combină rezultatele lor şi se obţin
soluţii pentru cazuri din ce în ce mai mari până se ajunge la soluţia
cazului iniţial.
Programarea dinamică este utilizată în probleme de optimizare,
de unde al treilea principiu al programării dinamice: metoda
programării dinamice este folosită pentru optimizarea problemelor
care satisfac principiul optimalităţii, adică într-o secvenţă optimă de
decizii/alegeri, fiecare subsecvenţă trebuie să fie de asemenea optimă.

4
Un algoritm de programare dinamică poate fi descris prin următoarea
succesiune de paşi:
1. se caracterizează structura unei soluţii optime;
2. se obţine recursiv valoarea unei soluţii optime;
3. se calculează de jos în sus valoarea unei soluţii optime;

În cazul în care se doreşte şi soluţia propriu-zisă atunci avem şi:

4. din informaţiile calculate, se construieşte de sus în jos o soluţie


optimă. Pasul patru se rezolvă printr-un algoritm recursiv, care
efectuează o parcurgere în sens invers a secvenţei optime de
decizii calculate anterior prin algoritmul de programare dinamică.

5
Problema triunghiului

Se consideră un triunghi de numere. Să se calculeze cea mai mare


sumă a numerelor ce apar pe drumurile ce pleacă din vârf şi ajung la
bază astfel:
- în fiecare drum succesorul unui număr se află pe rândul de mai jos,
dedesupt sau pe diagonală la dreapta.
Se cere, de asemenea, care sunt numerele ce o alcătuiesc.

Exemplu: Pentru triunghiul


2
3 5
6 3 4
5 6 1 4

rezultatul va fi 17.

Rezolvare:
Se pot forma mai multe sume:
S1=2+3+6+5=16
S2=2+5+4+1=12
…………….
Sk=2+3+6+6=17 (care este şi suma maximă).
Se observă că se pot forma 2n-1 astfel de sume. A le lua în considerare
pe toate pentru a găsi valoarea optimă (maximă) nu este eficient.

Fie un şir de n numere (care respectă condiţiile problemei şi care


formează suma maximă). În acest şir considerăm numărul care a fost
preluat de pe linia i. Numerele între i+1 şi n, formează o sumă maximă
în raport cu sumele care se pot forma începând cu numărul preluat de
pe linia i (contrar, se contrazice ipoteza).
În această situaţie se poate aplica programarea dinamică, metoda
înainte.

6
Vom forma un triunghi, de la bază către vârf, cu sumele maxime care
se pot forma cu fiecare număr. Dacă am memorat triunghiul de numere
într-o matrice T şi calculăm sumele într-o matrice C, vom avea relaţiile
următoare:
C[n,1]:=T[n,1];
C[n,2]:=T[n,2];
…………
C[n,n]:=T[n,n];
Pentru linia i (i<n), cele i sume maxime se obţin astfel:
C[i,j]:=max{T[i,j]+C[i+1,j], T[i,j]+C[i+1, j+1]}, unde iœ{1,2,…,n-1}
şi jœ{1, …,i}.
Să rezolvăm problema propusă ca exemplu:
Linia 4 a matricii C va fi linia n a matricii T:
5 6 1 4;
Linia 3 se calculează astfel:
C[3,1]=max{6+5,6+6}=12;
C[3,2]=max{3+6,3+1}=9;
C[3,3]=max{4+1,4+4}=8;
Linia 2:
C[2,1]=max{3+12,3+9}=15;
C[2,2]=max{5+9, 5+8}=14;
Linia 1:
C[1,1]=max{2+15,2+14}=17.
Aceasta este şi cea mai mare sumă care se poate forma.

Pentru a tipări numerele luate în calcul se foloseşte o matrice numită


drum, în care, pentru fiecare i din {1,…,n-1} şi j din {1,…,i} se reţine
coloana în care se găseşte succesorul lui T[i,j].

7
#include <stdio.h>
#include <conio.h>
int n,i,j;
int t[20][20],c[20][20],drum[20][20];
void main()
{
printf("nr. linii ale triunghiului ="); scanf("%d",&n);

for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
{
printf("a[%d,%d]=",i,j); scanf("%d",&t[i][j]);
}
for(j=1;j<=n;j++) c[n][j]=t[n][j];

for(i=n-1;i>=1;i--)
{
for(j=1;j<=i;j++)
if (c[i+1][j]<c[i+1][j+1])
{
c[i][j]=t[i][j]+c[i+1][j+1];
drum[i][j]=j+1;
}
else
{
c[i][j]=t[i][j]+c[i+1][j];
drum[i][j]=j;
}
}

printf("suma maxima este=%d \n",c[1][1]);

8
i=1;
j=1;
while (i<=n)
{
printf("\t %d",t[i][j]);
j=drum[i][j];
i=i+1;
}
printf("\n");
getch();
}

Metoda Programării Dinamice

Un algoritm de programare dinamică poate fi descris prin următoarea


succesiune de paşi:

1. se identifică subproblemele problemei date


2. se alege o structură de date capabilă să reţină soluţiile
subproblemelor
3. se caracterizează substructura optimală a problemei printr-o
relaţie de recurenţă
4. pentru a determina soluţia optimă, se rezolvă relaţia de recurenţă
de jos în sus (bottom-up – se rezolvă subprogramele în ordinea
crescătoare a dimensiunii lor)

9
Problema pachetelor

Ionel descarcă de pe Internet o aplicaţie. Aplicaţia a fost împărţită în


mai multe pachete, iar pachetele trebuie descărcate într-o ordine fixată.
Se cunoaşte timpul de descărcare pentru fiecare pachet, precum şi
timpul necesar instalării fiecărui pachet.
Ionel vrea să testeze aplicaţie cât mai repede şi vrea să înceapă
instalarea înainte de a se fi terminat descărcarea tuturor pachetelor. Se
ştie că odată ce începe instalarea nu mai poate fi întreruptă (va da
eroare dacă la momentul în care este necesară instalarea unui pachet
acesta nu este complet descărcat).

Scrieţi un program care să determine numărul minim de secunde (din


momentul în care a început descărcarea) după care Ionel poate începe
instalarea aplicaţiei.

n = numărul de pachete
Inst = timpul de instalare a pachetului
D= timpul de descărcare a pachetului

1§ n § 100000
1§ Inst, D § 1000

Se cere timpul (exprimat în secunde) după care poate începe instalarea


aplicaţiei, timp calculat din momentul începerii descărcării primului
pachet.
O subproblemă:

“Determinaţi timpul minim după care poate începe instalarea aplicaţiei,


în ipoteza că aplicaţia este formată numai din pachetele i, i+1, ..., n-1
(0 § i< n), timp calculat din momentul începerii descărcării pachetului
i”

10
Soluţia fiecărei subprobleme o vom reţine într-un vector min, unde
min[i]= numărul minim de secunde după care poate începe instalarea
aplicaţiei, calculat din momentul începerii descărcării pachetului i, 0 §
i< n.

Relaţia de recurenţă:
• min[n-1]=D[n-1]
(nr. minim de secunde după care poate începe instalarea pachetului
n-1, ultimul pachet, este egal cu timpul necesar descărcării complete
a pachetului n-1)
• min[i]=D[i] + max{0, min(i+1) –Inst[i]}, " 0 § i< n-1.

Numărul minim de secunde după care poate începe instalarea


pachetelor i+1, i+2, ..., n-1 este min[i+1].
Dacă Inst[i] ¥ min[i+1], atunci min[i]=D[i] (deoarece timpul necesar
instalării pachetului i a fost suficient de mare a.i. să acopere timpul
necesar pentru demararea instalării celorlalte pachete)
Dacă Inst[i] < min[i+1], atunci min[i]=D[i]+ (min[i+1]- Inst[i]) (
trebuie adăugat nr. de secunde ce se scurge de la terminarea instalării
pachetului i până când începe instalarea celorlalte pachete)

min[0] este soluţia problemei

11
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#define NMAX 1000
#define max(x,y) ((x)>(y)?(x):(y))
FILE *f; // declararea descriptorului de fisier intrare
FILE *g; // declararea descriptorului de fisier iesire
int min[NMAX], D[NMAX], Inst[NMAX];
void main()
{
if((f=fopen("c:\\borlandc\\fin.txt","r"))==0)
{
printf("Fisierul nu poate fi deschis !");
exit(1);
}
if((g=fopen("c:\\borlandc\\fout.txt","w"))==0)
{
printf("Fisierul nu poate fi creat !");
exit(1);
}
int n, i;
fscanf(f,"%d",&n);
for(i=0;i<n;i++)
fscanf(f,"%d%d",&Inst[i],&D[i]);
fclose(f);
min[n-1]=D[n-1];
for(i=n-2;i>=0;i--)
min[i]=D[i]+max(0, min[i+1]-Inst[i]);
fprintf(g, "%d\n",min[0]);
fclose(g);
getch();
}

12

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