Sunteți pe pagina 1din 8

Note de laborator Specializare

Algoritmi si structuri de date 2 Info 1

Laborator 6
Descriere: Programare dinamica - 1

1. Introducere

Problemele rezolvabile prin programare dinamica sunt in general probleme de optimizare, care se
pot descompune in subprobleme de aceeasi natura, dar de dimensiuni mai mici. Aceste
subprobleme nu sunt insa independente, ele avand “sub-subprobleme” comune.

O problema este abordabila folosind tehnica programarii dinamice daca satisface principiul de
optimalitate enuntat in continuare.

Fie secventa de stari S0, S1, ..., Sn ale sistemului. Daca d1, d2, ..., dn este un sir optim de decizii care
duc la trecerea sistemului din starea S0 ın starea Sn, atunci pentru orice i (1 ≤ i ≤ n) si di+1, di+2, ..., dn
este un sir optim de decizii care duc la trecerea sistemului din starea Si ın starea Sn.

Mai exact, conditiile de aplicare a programarii dinamice pentru o problema data sunt:

• Problema sa aiba o substructura optimala, adica solutia optima sa includa solutiile optime
ale subproblemelor rezultate din descompunerea problemei initiale.

• Sa existe o suprapunere a subproblemelor ce trebuie rezolvate, adica solutia pentru aceeasi


subproblema este utilizata de mai multe ori, pentru ca intervine in mai multe subprobleme.

Asemanator cu metoda ”divide et impera”, programarea dinamica rezolva problemele prin


combinarea solutiilor subproblemelor rezultate in urma descompunerii problemei initiale.
Deosebirea consta in faptul ca programarea dinamica foloseste o matrice pentru a memora solutiile
subroblemelor rezolvate prin descompunere, pentru a nu se repeta ulterior calculul acelorasi solutii.

Metoda programarii dinamice este de multe ori o alternativa mai eficienta decat un algoritm de tip
Backtracking sau de tip Divide et Impera pentru aceeasi problema.

Matricea folosita in programarea dinamica este o matrice de costuri optime pentru subproblemele
generate, iar costul optim al problemei initiale se afla in ultimul element din matrice calculat, care
se afla de obicei pe ultima coloana, ultima linie sau ultima coloana, prima linie.

Foarte importanta este ordinea de calcul a elementelor matricei de costuri unele din altele,
incepandu-se cu prima linie sau cu prima coloana sau cu diagonala matricei; aceste elemente
reprezinta costul (exact) al unor subprobleme banale, care nu se mai pot descompune in altele mai
simple. De altfel cuvatul “programare” din expresia “programare dinamica” are sensul de
planificare a calculelor in procesul de rezolvare a problemei si subliniaza importanta pe care o are
ordinea efectuarii calculelor in matricea de costuri.

2. Dezvoltarea algoritmilor de programare dinamica

Programarea dinamica se aplica problemelor care au mai multe solutii, fiecare din ele cu cate o
valoare, si la care se doreste obtinerea unei solutii optime (minime sau maxime).
ASD

Algoritmul pentru rezolvarea unei probleme folosind programarea dinamica se dezvolta in 4 etape:

• caracterizarea unei solutii optime (identificarea unei modalitati optime de rezolvare, care
satisface principiului optimalitatii)
• definirea recursiva a valorii unei solutii optime
• calculul efectiv al valorii unei solutii optime folosind o structura de date (de obicei tablou)
• reconstituirea unei solutii pe baza informatiei calculate.

3. Exemplu de rezolvare a unei probleme prin programare dinamica

Problema propusa nu este o problema de optimizare, dar permite ilustrarea modului de abordare a
metodei programarii dinamice pe un exemplu simplu.

Calculul combinarilor de n obiecte luate cate k se poate face folosind relatia de recurenta:

C(n,k) = C(n-1,k) + C(n-1,k-1) pentru 0 < k < n


C(n,k) = 1 pentru k=0 sau k=n

Aceasta relatie de recurenta exprima descompunerea problemei C(n,k) in subprobleme mai simple
(cu valori mai mici pentru n si k).

Metoda Divide et Impera transpune direct relatia intr-o functie recursiva, reducand problema
initiala la probleme tot mai mici.

public static int combinari(int n, int k) {

//cazuri de baza
if (n == k) return 1; //C(n,n) = 1
if (k == 0) return 1; //C(n,0) = 1
if (k > n) return 0; //C(n,k) = 0

//cazul inductiv : C(N,k) = C(N-1,k-1) + C(N-1,k)


return (combinari(n-1,k-1) + combinari(n-1,k));
}

Dezavantajul acestei abordari rezulta din numarul mare de apeluri recursive, dintre care o parte nu
fac decat sa recalculeze aceleasi valori (metoda combinari se apeleaza de mai multe ori cu aceleasi
valori pentru parametrii n si k). Arborele acestor apeluri pentru n=5 si k=3 este urmatorul:
C(5,3)

C(4,3) C(4,2)

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

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

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


ASD

Dintre aceste 19 apeluri numai 11 sunt necesare (sunt diferite).

Metoda programarii dinamice creaza o matrice cu valorile C(i,j) a carei completare incepe cu prima
coloana ( C(i,0)=1 ) si continua cu linia a doua ( C(i,1) ), linia a treia ,s.a.m.d. Matricea creata este
cunoscuta si sub numele de triunghiul lui Pascal :
k
0 1 2 3
0 1 0 0 0
n 1 1 1 0 0
2 1 2 1 0
3 1 3 3 1

Mai multe detalii la adresa


http://mathworld.wolfram.com/PascalsTriangle.html

O functie care completeaza aceasta matrice triunghiular inferioara arata astfel:

/* creare matrice triunghi Pascal */


void pdcomb (int n) {
int i,j;
for (i=0;i<=n;i++)
c[i][0]=1; // coloana 0
for (i=1;i<=n;i++)
for (j=1;j<=n;j++)
if (i==j)
c[i][j]=1; // diagonala principala
else
if (i <j)
c[i][j]=0; // deasupra diagonalei
else
c[i][j]=c[i-1][j]+c[i-1][j-1]; // sub diagonala
}

Obs. Spre deosebire de metoda Divide et Impera, care rezolva subproblemele de sus in jos, metoda
programarii dinamice rezolva subproblemele de jos in sus, adica incepe cu cele de dimensiune mai
mica si continua pana la problema de dimensiune maxima, care este problema initiala.

4. Inmultirea optimala a matricelor

Consideram n matrice A1,A2, ...,An, de dimensiuni d0 × d1, d1 × d2, ..., dn−1×dn. Produsul
A1×A2×...×An se poate calcula in diverse moduri, aplicand asociativitatea operatiei de ınmultire a
matricelor. Numim ınmultire elementara ınmultirea a doua elemente. In functie de modul de
utilizare al parantezelor difera numarul de ınmultiri elementare necesare pentru calculul produsului
A1 × A2 × ... × An. Se cere o solutie optima astfel incat costul, adica numarul total de ınmultiri
elementare pentru calcularea produsului A1 × A2 × ... × An, sa fie minim.

Exemplu

Pentru 3 matrici, cu dimensiunile (10, 1000), (1000, 10) si (10, 100), produsul A1 × A2 × A3 se
poate calcula ın doua moduri:

• (A1 × A2) × A3 necesitand 1000000+10000=1.010.000 ınmultiri elementare


• A1 × (A2 × A3), necesitand 1000000+1000000=2.000.000 ınmultiri.
ASD

Obs. Numarul de ınmultiri elementare necesare pentru a ınmulti o matrice A, avand n linii si m
coloane, cu o matrice B, avand m linii si p coloane, este n ∗ m∗ p.

1. Pentru a calcula A1×A2×...×An, ın final trebuie sa ınmultim doua matrici, deci vom diviza
produsul astfel: (A1 ×A2 ×...×Ak)×(Ak+1 ×...×An). Aceasta observatie se aplica si produselor
dintre paranteze. Prin urmare, subproblemele problemei initiale constau ın determinarea
parantezarii optimale a produselor de matrice de forma Ai ×Ai+1 ×... ×Aj , 1 ≤ i ≤ j ≤ n.

Obs Subproblemele nu sunt independente. De exemplu, calcularea produsului


Ai×Ai+1×...×Aj ¸si calcularea produsului Ai+1×Ai+2×...×Aj+1, au ca subproblema comuna
calcularea produsului Ai+1 ×... ×Aj .

2. Pentru a retine soluliile subproblemelor, vom utiliza o matrice M, cu n linii si n coloane, cu


semnificatia: M[i][j] = numarul minim de ınmultiri elementare necesare pentru a calcula
produsul Ai ×Ai+1 ×... ×Aj , 1 ≤ i ≤ j ≤ n. Evident, numarul minim de ınmultiri necesare
pentru a calcula A1 ×A2 × ... ×An este M[1][n].

3. Pentru ca parantezarea sa fie optimala, parantezarea produselor A1 × A2 × ...× Ak si Ak+1 ×


... × An trebuie sa fie de asemenea optimala. Prin urmare elementele matricei M trebuie sa
satisfaca urmatoarea relatie de recurenta:

Interpretarea relatiei de recurenta: Pentru a determina numarul minim de ınmultiri


elementare pentru calculul produsului Ai×Ai+1×...×Aj , fixam pozitia de parantezare k ın
toate modurile posibile (ıntre i si j −1), si alegem varianta care ne conduce la minim. Pentru
o pozitie k fixata, costul parantezarii este egal cu numarul de ınmultiri elementare necesare
pentru calculul produsului Ai×Ai+1×...×Ak, la care se adauga numarul de ınmultiri
elementare necesare pentru calculul produsului Ak+1 × ... × Aj si costul ınmultirii celor doua
matrice rezultate (di−1 × dk × dj ).

Observam ca numai jumatatea de deasupra diagonalei principale din M este utilizata. Pentru
a construi solutia optima este utila si retinerea indicelui k, pentru care se obtine minimul. Nu
vom considera un alt tablou, ci il vom retine, pe pozitia simetrica fata de diagonala
principala (M[j][i]).

4. Rezolvarea recursiva a relatiei de recurenta este ineficienta, datorita faptului ca


subproblemele se suprapun, deci o abordare recursiva ar conduce la rezolvarea aceleiasi
subprobleme de mai multe ori. Prin urmare vom rezolva relatia de recurenta ın mod bottom-
up: (determinam parantezarea optimala a produselor de doua matrice, apoi de 3 matrice, 4
matrice, etc).

Rezolvare

import java.io.*;

class InmOptimalaMatrice{

static int nmax=20;


static int m[][]=new int[100][100];
static int d[]=new int[100];
static int n,i,j,k,imin,min,v;
ASD

public static void paranteze(int i,int j){


int k;

if(i<j){
k=m[j][i];

if(i!=k){
System.out.print("(");
paranteze(i,k);
System.out.print(")");
}
else paranteze(i,k);

System.out.print(" * ");

if(k+1!=j){
System.out.print("(");
paranteze(k+1,j);
System.out.print(")");
}
else paranteze(k+1,j);
}
else
System.out.print("A"+i);
}//paranteze

public static void main(String[]args) throws IOException{

BufferedReader br=new BufferedReader(new InputStreamReader(System.in));


System.out.print("numarul matricelor: ");
n=Integer.parseInt(br.readLine());

System.out.println("Dimensiunile matricelor:");

for(i=1;i<=n+1;i++){
System.out.print("d["+i+"]=");
d[i]=Integer.parseInt(br.readLine());
}

//recurenta
for(i=n;i>=1;i--)
for(j=i+1;j<=n;j++){
min=m[i][i]+m[i+1][j]+d[i]*d[i+1]*d[j+1];
imin=i;

for(k=i+1;k<=j-1;k++){
v=m[i][k]+m[k+1][j]+d[i]*d[k+1]*d[j+1];

if(min>v){
min=v;
imin=k;
}
}
m[i][j]=min;
m[j][i]=imin;
}//for i,j

System.out.println("Numarul minim de inmultiri este: "+m[1][n]);


System.out.print("Ordinea inmultirilor: ");
paranteze(1,n);

System.out.println();
}//main
}//class
ASD

5. Subsir crescator maximal

Fie un sir A = (a1, a2, ..., an). Numim subsir al sirului A o succesiune de elemente din A, ın ordinea
ın care acestea apar ın A: ai1, ai2 , ..., aik , unde 1 ≤ i1 < i2 < ... < ik ≤ n. Se cere determinarea unui
sub sir crescator al sirului A, de lungime maxima. De exemplu, pentru

A = (8, 3, 6, 50, 10, 8, 100, 30, 60, 80)

o solutie poate fi

(3, 6, 10, 30, 60, 80).

ƒ Fie Ai1 = (ai1 ≤ ai2 ≤ ... ≤ aik ) cel mai lung subsir crescator al sirului A. Observam ca el
coincide cu cel mai lung subsir crescator al sirului (ai1, ai1+1, ..., an). Evident Ai2 = (ai2 ≤ ai3
≤ ... ≤ aik ) este cel mai lung subsir crescator al lui (ai2, ai2+1, ..., an), etc. Prin urmare, o
subproblema a problemei initiale consta in determinarea celui mai lung subsir crescator care
ıncepe cu ai, i = {1, .., n}.

Subproblemele nu sunt independente: pentru a determina cel mai lung subsir crescator care
ıncepe cu ai, este necesar sa determinam cele mai lungi subsiruri crescatoare care ıncep cu
aj , ai ≤ aj , j = {i+ 1, .., n}.

ƒ Pentru a retine solutiile subproblemelor vom considera doi vectori lg si poz, fiecare cu n
componente, avand semnificatia:
lg[i] =lungimea celui mai lung subsir crescator care ıncepe cu a[i];
poz[i] =pozitia elementului care urmeaza dupa a[i] ın cel mai lung subsircrescator
care ıncepe cu a[i], daca un astfel de element exista, sau −1 daca uastfel de
element nu exista.

ƒ Relatia de recurenta care caracterizeaza substructura optimala a problemei este:

unde poz[i] = indicele j pentru care se obtine maximul lg[i].

5.1 Exemplu numeric

Sa consideram urmatorul sir de 5 numere:

a= {2,5,3,4,1}

Vom nota cu lg[i] lungimea subsirului crescator maximal dintre a[1] si a[i] (terminat in pozitia i),
deci:

lg[1]=1, lg[2]=2, lg[3]=2, lg[4]=3, lg[5]=1

Ideea programarii dinamice este ca aceste lungimi pot fi calculate unele din altele, cu o relatie de
recurenta, in loc de a calcula separat fiecare lg[i]. Lungimea unui subsir crescator terminat in
pozitia i poate fi cu 1 mai mare ca lungimea unui subsir crescator terminat in pozitia j ( cu j<i ),
daca a[i] >= a[j], deoarece a[i] se adauga subsirului terminat in pozitia j. Pentru calculul lui lg[i]
ASD

vom cauta printre lg[1],lg[2],..lg[i-1] valoarea maxima dintre cele care satisfac conditia a[j] <= a[i].
Relatia de recurenta poate fi exprimata astfel:

lg[i] = 1 + max ( lg[j]) (pentru j<i si a[j]<=a[i])

lg[1]=1 deoarece subsirul a[1] are lungimea 1

Aplicarea acestei relatii se face prin doua cicluri suprapuse: unul pentru fiecare i=2,n si altul pentru
fiecare j=1,i-1. Evolutia principalelor variabile pentru exemplul numeric:

a[1]=2, a[2]=5, a[3]=3, a[4]=4, a[5]=1

este urmatoarea

i j a[j] a[i] lg[j] max lg[i] a[j]<=a[i] lg[1] lg[2] lg[3] lg[4] lg[5]
2 1 2 5 1 1 2 2<5 1 2
3 1 2 3 1 1 2 2<3
3 2 5 3 2 1 2 5>3 1 2 2
4 1 2 4 1 1 2 2<4
4 2 5 4 2 1 2 5>4
4 3 3 4 2 2 3 3<4 1 2 2 3
5 1 2 1 1 0 1 2>1
5 2 5 1 2 0 1 5>1
5 3 3 1 2 0 1 3>1
5 4 4 1 3 0 1 4>1 1 2 2 3 1

Determinarea subsirului crescator cu lungime maxima se face intr-o etapa ulterioara, examinand
vectorul lg de la stanga la dreapta pentru valori crescatoare, pana la valoarea maxima.

Rezolvare
import java.io.*;

class SubsirCrescatorMaximal{

static int n;
static int[] a;
static int[] lg;
static int[] poz;

public static void main(String []args) throws IOException{

int i,j;

StreamTokenizer st=new StreamTokenizer(


new BufferedReader(new FileReader("subsir.in")));
PrintWriter out = new PrintWriter (new BufferedWriter(
new FileWriter("subsir.out")));

st.nextToken(); n=(int)st.nval;

a=new int[n+1];
lg=new int[n+1];
poz=new int[n+1];
ASD

for(i=1;i<=n;i++) {
st.nextToken();
a[i]=(int)st.nval;
}

int max,jmax;

// rezolvam relatia de recurenta in mod bottom-up


lg[n]=1;
for(i=n-1;i>=1;i--){
max=0;
jmax=0;
for(j=i+1;j<=n;j++)
if((a[i]<=a[j])&&(max<lg[j])) {
max=lg[j];
jmax=j;
}

if(max!=0) {
lg[i]=1+max;
poz[i]=jmax;
}
else lg[i]=1;
}

/*Pentru a determina solutia optima a problemei, determinam valoarea maxima


din vectorul lg, apoi afisam solutia, incepand cu pozitia maximului si
utilizand informatiile memorate in vectorul poz:
*/

max=0;
jmax=0;
for(j=1;j<=n;j++)
if(max<lg[j]){
max=lg[j];
jmax=j;
}
out.println(max);

int k;
j=jmax;
for(k=1;k<=max;k++) {
out.print(a[j]+" ");
j=poz[j];
}
out.close();
}//main
}//class

Exemplu date de intrare


10
8 3 6 50 10 8 100 30 60 80

Probleme

1. Modificati programul pentru un subsir crescator maximal pentru a calcula cel mai lung sir
crescator (in ordine lexicografica) a unui sir de litere.
2. Realizati un program care calculeaza primele n linii din triunghiul lui Pascal folosind
metoda programarii dinamice.

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