Sunteți pe pagina 1din 131

Programare dinamică

1
Structura

• Ce este programarea dinamică ?

• Etapele principale în aplicarea programării dinamice

• Relații de recurență: dezvoltare ascendentă vs.dezvoltare descendentă

• Aplicații ale programării dinamice

2
2
Ce este programarea dinamică?
• Este o tehnică de proiectare a algoritmilor pentru rezolvarea
problemelor care pot fi descompuse în subprobleme care se
suprapun – poate fi aplicată problemelor de optimizare care
au proprietatea de substructură optimă

• Particularitatea metodei constă în faptul că fiecare


suproblemă este rezolvată o singură dată iar soluția ei este
stocată (într-o structură tabelar ă) pentru a putea fi ulterior
folosită pentru rezolvarea problemei inițiale.

Obs.
• Programarea dinamică a fost dezvoltată de către Richard
Bellman in 1950 ca metodă generală de optimizare a proceselor
de decizie.
• In programarea dinamică cuvântul programare se referă
la planificare și nu la programare în sens informatic.
• Cuvântul dinamic se referă la maniera în care sunt construite
tabelele în care se rețin informațiile referitoare la soluțiile parț iale.
3
Ce este programarea dinamică?
• Programarea dinamică este corelată cu tehnica divizării întrucât se
bazează pe divizarea problemei inițiale în subprobleme. Există
însă câteva diferențe semnificative între cele două abordări:
– divizare: subproblemele în care se divide problema inițială sunt
independente, astfel că soluția unei subprobleme nu poate fi utilizată
în construirea soluției unei alte subprobleme
– programare dinamică: subproblemele sunt dependente (se suprapun)
astfel că soluția unei subprobleme se utilizează în construirea
soluțiilor altor subprobleme (din acest motiv este important ca soluția
fiecărei subprobleme rezolvate să fie stocată pentru a putea fi
reutilizată)
• Programarea dinamică este corelată și cu strategia căutării
local optimale (greedy) întrucât ambele se aplică problemelor
de optimizare care au proprietatea de substructură optimă

4
Structura

• Ce este programarea dinamică ?

• Etapele principale în aplicarea programării dinamice

• Relații de recurență: dezvoltare ascendentă


vs.dezvoltare descendentă

• Aplicații ale programării dinamice

5
Etapele principale în aplicarea programării
dinamice
1. Se analizeaza structura soluției: se stabilește modul in care
soluția problemei depinde de soluțiile subproblemelor. Această
etapă se referă de fapt la verificarea proprietății de substructură
optimă și la identificarea problemei generice (forma generală a
problemei inițiale și a fiecărei subprobleme).

2. Identificarea relației de recurență care exprimă legătura între


soluția problemei și soluțiile subproblemelor. De regulă in relația
de recurență intervine valoarea criteriului de optim.

3. Dezvoltarea relației de recurență. Relația este dezvoltată în


manieră ascendentă astfel încât să se construiască tabelul cu
valorile asociate subproblemelor

4. Construirea propriu-zisă a soluției – se bazează pe informațiile


determinate în etapa anterioară.
Algoritmica - Curs 11 6
6
Structura

• Ce este programarea dinamică ?

• Etapele principale în aplicarea programării dinamice

• Relații de recurență: dezvoltare ascendentă


vs.dezvoltare descendentă

• Aplicații ale programării dinamice

7
Dezvoltarea relațiilor de recurență
Există două abordări principale:

• Ascendentă (bottom up): se pornește de la cazul particular și se


generează noi valori pe baza celor existente.

• Descendentă (top down): valoarea de calculat se exprimă prin


valori anterioare, care trebuie la rândul lor calculate. Această
abordare se implementează de regulă recursiv (și de cele mai
multe ori conduce la variante ineficiente – eficientizarea se
poate realiza prin tehnica memoizării (cursul următor))

8
Dezvoltarea relațiilor de recurență
Exemplu 1. Calculul celui de al m-lea element al secvenței Fibonacci
f1=f2=1; fn=fn-1+fn-2 for n>2
Efficiența:
0 if m<=2
Abordare descendentă:
T(m) =
T(m-1)+T(m-2)+1 if m>2
fib(m)
IF (m=1) OR (m=2) THEN T:
RETURN 1 0 0 1 2 4 7 12 20 33 54 …
ELSE
RETURN fib(m-1)+fib(m-2) Fibonacci:
ENDIF 1 1 2 3 5 8 13 21 34 55 …
f apartine lui O(phin),
n

phi=(1+sqrt(5))/2
Complexitate exponențială!
9
10
Dezvoltarea relatiilor de recurenta
Exemplu 1. Calculul celui de al m-lea element al secvenței Fibonacci
f1=f2=1; fn=fn-1+fn-2 for n>2 Eficienta:
Abordare ascendentă: T(m)=m-2 => complexitate liniara
fib(m) Obs: eficienta în timp este platită
f[1]←1; f[2] ← 1; prin utilizarea unui spațiu
adițional. Dimensiunea
FOR i ← 3,m DO
spațiului adițional poate fi
f[i] ← f[i-1]+f[i-2] semnificativ redusă
ENDFOR
fib(m)
RETURN f[m] f1 ← 1; f2 ← 1;
FOR i ← 3,m DO
f2 ← f1+f2; f1 ← f2-f1;
ENDFOR
RETURN f2
11
12
Dezvoltarea relatiilor de recurenta
Exemplu 2. Calculul coeficienților binomiali C(n,k) (combinări de
n luate câte k)
0 dacă n<k
C(n,k)= 1 dacă k=0 sau n=k
C(n-1,k)+C(n-1,k-1) altfel
Efficiența:
Abordare descendenta:
Dim pb: (n,k)
comb(n,k) Op. dominantă: adunare
IF (k=0) OR (n=k) THEN T(n,k)=0 dacă k=0 sau k=n
RETURN 1 T(n-1,k)+T(n-1,k-1)
ELSE Nr adunări = nr noduri în
RETURN comb(n-1,k)+comb(n-1,k-1) arborele de apeluri recursive
ENDIF T(n,k) >= 2 min{k,n-k}
T(n,k) Ω(2 min{k,n-k} )
13
Dezvoltarea relatiilor de recurenta
Exemplu 2. Calculul coeficienților binomiali C(n,k)
0 daca n<k
C(n,k)= 1 daca k=0 sau n=k
C(n-1,k)+C(n-1,k-1) altfel
Abordare descendentă: construirea triunghiului lui Pascal
0 1 2 … k-1 k
0 1
1 1 1
2 1 2 1

k 1 … 1

n-1 1 C(n-1,k-1) C(n-1,k)
n 1 C(n,k)

14
Dezvoltarea relatiilor de recurenta
Algoritm:
Eficienta:
Comb(n,k)
FOR i←0,n DO
FOR j ← 0,min{i,k} DO Dim pb: (n,k)
IF (j=0) OR (j=i) THEN Op. dominanta: adunarea
C[i,j] ← 1
ELSE
C[i,j] ← C[i-1,j]+C[i-1,j-1] T(n,k)=(1+2+…+k-1) +(k+…+k)
ENDIF =k(k-1)/2+k(n-k+1)
ENDFOR
ENDFOR T(n,k) (nk)
RETURN C[n,k]

Obs. Dacă trebuie calculat doar C(n,k) este suficient să se utilizeze


un tablou cu k elemente ca spațiu suplimentar

15
Aplicații ale programării dinamice
Cel mai lung subșir strict crescător
Fie a1,a2,…,an o secvență. Să se determine cel mai lung subșir
având proprietatea aj1<aj2<…<ajk (un subșir strict crescator
având numărul de elemente maxim).

Exemplu:
a = (2,5,1,3,6,8,2,10,4)

Subșiruri strict crescătoare de lungime 5 (lungimea


maximă): (2,5,6,8,10)
(2,3,6,8,10)
(1,3,6,8,10)

16
Cel mai lung subșir strict crescător

1. Analiza structurii solutiei.

Fie s=(aj1, aj2,…,aj(k-1) ,ajk ) soluția optimă. Inseamnă c ă nu există nici un


element în a[1..n] aflat după ajk care să fie mai mare decât ajk . In
plus nu există element în șirul inițial având indicele cuprins între
j(k-1) și jk iar valoarea cuprinsă între valorile acestor elemente ale
subșirului s (s nu ar mai fi soluție optim ă). Aratăm că s’=(aj1,
aj2,…,aj(k-1) ) este soluție optimă pentru problema determinării celui
mai lung subșir care se termină în aj(k-1) . Pp ca s’ nu este
optimal. Rezultă că există un subșir s” de lg. mai mare. Adaugând
la s” elementul ajk s-ar obține o soluție mai bun ă decât s,
implicând c ă s nu este optim. Se ajunge astfel la o contradicție,
deci s’ este solutie optima a subproblemei determinării unui
subșir crescător care se termină în aj(k-1)
Deci problema are proprietatea de substructura optima

17
Cel mai lung subșir strict crescător

2. Construirea unei relatii de recurenta


Fie Bi numarul de elemente al celui mai lung subsir strict crescator
care se termina in ai

1 if i=1
Bi =
1+ max{Bj | 1<=j<=i-1, aj<ai}

Exemplu:
a = (2,5,1,3,6,8,2,10,4)
B = (1,2,1,2,3,4,2,5,3)

18
Cel mai lung subșir strict crescător

3. Dezvoltarea relației de recurență calculB(a[1..n])


B[1]←1
1 if i=1 FOR i ← 2,n
Bi = DO max ← 0
FOR j ← 1,i-1 DO
1+max{Bj | 1<=j<=i-1, aj<ai}
IF a[j]<a[i] AND max<B[j]
THEN max ← B[j]
Complexitate: θ(n2) ENDIF
ENDFOR B[i]
← max+1
ENDFOR
RETURN B[1..n]

19
Cel mai lung subșir strict crescător
construire(a[1..n],B[1..n])
4. Construirea solutiei m←1
FOR i ← 2,n DO
IF B[i]>B[m] THEN m ← i ENDIF
Se determina maximul lui
B ENDFOR
k ← B[m]
s[k] ← a[m]
Se construieste s succesiv
WHILE B[m]>1
pornind de la ultimul
DO i ← m-1
element
WHILE a[i]>=a[m] OR B[i]<>B[m]-1
DO i ← i-1
Complexitate: θ(n)
ENDWHILE
m ← i; k ← k-1; s[k] ← a[m]
ENDWHILE
RETURN s[1..k]

20
Cel mai lung subșir strict crescător
calculB(a[1..n]) B[1]:=1; construire(a[1..n],B[1..n],P[1..n])
P[1]:=0 FOR i:=2,n DO m:=1
max:=0 FOR i:=2,n DO
P[i]:=0 IF B[i]>B[m] THEN m:=i ENDIF
FOR j:=1,i-1 DO ENDFOR
IF a[j]<a[i] AND max<B[j] k:=B[m]
THEN max:=B[j] s[k]:=a[m]
P[i]:=j WHILE P[m]>0 DO
ENDIF m:=P[m] k:=k-1
ENDFOR s[k]:=a[m]
B[i]:=max+1
ENDFOR RETURN B[1..n] ENDWHILE
P[i] este indicele elementului RETURN s[1..k]
ce il precede pe a[i] in subsirul
optim. Utilizarea lui P[1..n]
simplifica construirea solutiei

21
Cel mai lung subșir comun
Fiind date două șiruri (secvențe) a1,…, an si b1,…,bm să
se determine un subșir c1,…c k care satisface:
• Este subșir comun al șirurilor a și b, adică există i1,…,ik
si j1,…,jk astfel incât
c1=ai1=bj1, c2=ai2=bj2, … , ck=aik=bjk
• k este maxim (cel mai lung subșir comun)

Obs : această problemă este un caz particular al problemelor din


bioinformatică unde se analizeaza similaritatea dintre doua
șiruri de nucleotide (ADN) sau aminoacizi (proteine) – cu cât
au un subș ir comun mai lung cu atât sunt mai similare cele
doua șiruri inițiale

22
Cel mai lung subsir comun
Exemplu: Variantă a problemei: determinarea
celei mai lungi subsecvențe
comune de elemente consecutive
a: 2 1 4 3 2
b: 1 3 4 2
Exemplu:
a: 2 1 3 4 5 b: 1
Subșiruri comune:
342
1, 3 1, 2
Subsecvențe comune:
4, 2 1,
1, 3 3, 4 1, 3, 4
3, 2 1,
4, 2

23
Cel mai lung subsir comun
1. Analiza structurii unei soluții optime

Fie P(i,j) problema determinarii celui mai lung sub șir comun al șirurilor
a[1..i] și b[1..j]. Dacă a[i]=b[j] atunci soluția optimă conține acest
element comun iar restul elementelor este reprezentat de soluț
ia optimă a subproblemei P(i-1,j-1) (adica determinarea celui
mai lung subșir comun al șirurilor a[1..i-1] respectiv b[1..j-1].
Dacă a[i]<>b[j] atunci soluția optimă coincide cu cea mai bună
dintre soluțiile subproblemelor P(i-1,j) respectiv P(i,j-1).

2. Deducerea relatiei de recurență. Fie L(i,j) lungima soluției optime


a problemei P(i,j). Atunci:
0 dacă i=0 sau j=0
L[i,j]= 1+L[i-1,j-1] dacă a[i]=b[j]
max{L[i-1,j],L[i,j-1]} altfel
24
Cel mai lung subșir comun
Exemplu:

0 1 2 3 4
a: 2 1 4 3 2
b: 1 3 4 2
0 0 0 0 0 0
1 0 0 0 0 1
2 0 1 1 1 1
0 dacă i=0 sau j=0 L[i,j]= 1+L[i- 3 0 1 1 2 2
1,j-1] dacă a[i]=b[j]
4 0 1 2 2 2
max{L[i-1,j],L[i,j-1]} altfel
5 0 1 2 2 3

25
Cel mai lung subsir comun
Dezvoltarea relației de recurență:
calcul(a[1..n],b[1..m])
FOR i:=0,n DO L[i,0]:=0 ENDFOR
0 dacă i=0 sau j=0 L[i,j]= FOR j:=1,m DO L[0,j]:=0
1+L[i-1,j-1] dacă a[i]=b[j] ENDFOR FOR i:=1,n DO
max{L[i-1,j],L[i,j-1]} altfel FOR j:=1,m DO
IF a[i]=b[j]
THEN L[i,j]:=L[i-1,j-1]+1
ELSE L[i,j]:=max(L[i-
1,j],L[i,j-1])
ENDIF
ENDFOR ENDFOR
RETURN L[0..n,0..m]

26
Cel mai lung subșir comun
Construirea solutiei (varianta Observatii:
recursiva):
• a, b, c si k sunt
Construire(i,j) variabile globale
IF i>=1 AND j>=1 • Inainte de apelul functiei,
THEN IF a[i]=b[j] variabila k se initializeaza
THEN construire(I- (k:=0)
1,j-1) k:=k+1 • Functia de construire
se apeleaza prin
c[k]:=a[I]
construire(n,m)
ELSE
IF L[i-1,j]>L[i,j-1]
THEN construire(i-1,j)
ELSE construire (i,j-1)
ENDIF ENDIF ENDIF
27
28
Programare dinamică
Şir de decizii. Principiul de optim. Relaţii de recurenţă.

Metoda progamării dinamice se poate aplica problemelor în care soluţia este


rezultatul unui şir finit de decizii optim dintr-un anumit punct de vedere. Se
presupune că avem un sistem care se poate afla în mai multe stări posibile şi în
urma fiecărei decizii trece dintr-o stare în alta.
Se presupune, în plus, că problema verifică una din următoarele
condiţii, numite principii de optimalitate (vom nota cu Si stările şi Di deciziile):

(1) Dacă şirul D1,...,Dn duce sistemul în mod optim din S0 în Sn, atunci: pentru
orice 1<=k<=n, şirul Dk,...,Dn duce sistemul în mod optim din Sk-1 în Sn.

(2) Dacă şirul D1,…,Dn duce sistemul în mod optim din S0 în Sn, atunci: pentru
orice 1<=k<=n, şirul D1,...,Dk duce sistemul în mod optim din S0 în Sk.

(3) Dacă şirul D1,…,Dk duce sistemul în mod optim din S0 în Sn, atunci: pentru
orice 1<=k<=n, şirul D1,...,Dk duce sistemul în mod optim din S0 în Sk, iar şirul
D(k+1),...,Dn duce sistemul în mod optim din Sk în Sn(evident, ultima cerinţă se
pune doar pentru k<n).

În notaţiile de mai sus S0,...,Sn sunt nişte stări oarecare din mulţimea stărilor
posibile, iar cu Di sistemul trece din S(i-1) în Si.

Oricare din principiile de optimalitate de mai sus exprimă faptul că


optimul total implică optimul parţial.
Evident, optimul parţial nu implică neapărat optimul total; de exemplu e
clar că oricum am alege două oraşe X şi Y, dacă cel mai scurt drum dintre ele
trece printr-un anumit oraş Z, atunci porţiunile din acest drum cuprinse între X şi
Z, respectiv Z şi Y, sunt cele mai scurte drumuri între oraşele respective; asta
înseamnă că dacă compunem cel mai scurt drum între Bucureşti şi Cluj cu cel
mai scurt drum între Cluj şi
Suceava obţinem cel mai scurt drum între Bucureşti şi Suceava (poate exista un
drum mai scurt între Bucureşti şi Suceava care nu trece prin Cluj).
Deci oricare din principiile de optimalitate afirmă doar că optimul total
poate fi găsit printre optimele parţiale, nu indică însă şi care din ele e. Totuşi
asta înseamnă că putem căuta optimul total doar printre optimele parţiale, ceea
ce reduce considerabil căutarea.

Modul de căutare a optimului total printre optimele parţiale depinde de forma


în care este îndeplinit principiul de optimalitate şi se face pe baza unor relaţii de
recurenţă deduse din structura problemei.
Mai exact:
* dacă este îndeplinit în forma (1), spunem că se aplică metoda înainte; în
acest caz, pe baza unor relaţii de recurenţă se calculează optimurile de la stările
mai depărtate de final în funcţie de optimurile de la stările mai apropiate de final
şi se determină deciziile ce leagă aceste optime între ele (se merge deci de la

29
sfârsit către început); în final se află optimul total, apoi se determină şirul de
decizii care îl realizează compunând deciziile calculate anterior mergând de la
început către sfârşit.

* dacă este îndeplinit în forma (2), spunem că se aplică metoda înapoi; în


acest caz calculele recurente se fac de la început către sfârşit, iar în final se află
optimul total şi se determină şirul de decizii care îl realizează compunând
deciziile de la sfârşit către început.

* dacă este îndeplinit în forma (3), spunem că se aplică metoda mixtă; în


acest caz pe baza unor relaţii de recurenţă se calculează optimurile între stările
mai îndepărtate între ele în funcţie de cele între stări mai apropiate între ele şi
se determină deciziile care interconectează aceste optimuri; în final se află
optimul total, apoi se determină şirul de decizii care îl realizează mergând de la
capete spre interior (se determină prima si ultima decizie, apoi o decizie
intermediară, apoi câte o decizie intermediară între cele două perechi
succesive, etc.).
Deci metoda programării dinamice se poate aplica cu următoarele abordări:
 Metoda înainte – pentru rezolvare se pleacă de la starea finală;
 Metoda înapoi - pentru rezolvare se pleacă de la starea iniţială;
 Metoda mixtă – o combinaţie a primelor două.

30
Programare dinamică
1 Metoda înainte
Problemă:
Se consideră un triunghi de numere naturale cu n linii; prima linie conţine un
număr a doua linie două,..., ultima linie n numere, liniile începând din aceeaşi
coloană stângă, de exemplu (n=4):

2
35
634
5614
Dorim să aflăm cea mai mare sumă care se poate obţine astfel: plecăm de la
numărul din linia 1, apoi la fiecare pas următorul număr adunat se află pe linia
următoare, dedesubtul său imediat în dreapta numărului anterior.
Exemple de sume corect construite:

2 + 3 + 6 + 5 = 16
2 + 5 + 4 + 1 = 12
2+3+6+6=17(care este şi suma maximă)

Constatăm că se pot forma 2 la puterea n-1 sume de acest fel şi deci un


algoritm care să le determine pe toate pentru a o afla pe cea maximă ar fi de
complexitate exponenţială(deci ineficient).
Observăm însă că dacă x1,x2,...,xn este un şir de numere ce respectă enunţul
şi produce suma maximă, atunci pentru orice 1<= i <=n şirul pornind cu numarul
xi (fixat).
Astfel se verifică principiul de optimalitate in forma (1) şi putem aplica
programarea dinamică,metoda înainte.
Pentru relaţiile de recurenţă aferente notăm:
-x[i][j] numărul aflat în triunghi pe linia i si coloana j (1<= i<=n ,1<=j<=i);
-s[i][j] cea mai mare sumă care se poate obţine cu un şir ca în enunţ ce
începe cu numărul din linia i si coloana j;
-u[i][j] coloana (j sau j+1) a numărului de pe linia i+1 care urmează
numărului din
linia i si coloana j într-un şir ca în enunţ de sumă maximă ce începe cu
acesta;convenim că u[n][j]=0,1<=j<=n.
Ne putem imagina că avem un sistem în care stările sunt pozitii (i,j) în triunghi
iar u[i][j] o decizie cu care ajungem din starea (i,j) în starea (i+1,u[i][j]).
Relaţiile de recurenţă sunt următoarele :

s[n][j] = x[n][j] (1<=j<=n)


u[n][j] = 0 (1<=j<=n)
s[i][j] = x[i][j] + max {s[i+1][k] | j<=k<=j+1} (1<=j<=n,1<=j<=i)
u[i][j]= acel k pentru care se atinge maximul mai sus (1<=j<=n,1<=j<=i)

Matricile inferior triunghiulare s si u se vor calcula pe baza relaţiilor de


recurenţă anterioare de jos în sus :

31
Algoritm descris în pseudocod
pentru j←1,n-1 execuă
s[n][j]←x[n][j]
u[n][j]←0
sfpentru
pentru i←n-1,1 -1 execută
pentru j←1,i-1 execută
dacă s[i+1][j] ≥ s[i+1][j+1] atunci
s[i][j]←x[i][j]+s[i+1][j]
u[i][j]←j
altfel
s[i][j]←x[i][j]+s[i+1][j+1]
u[i][j]←j+1
sfdacă
sfpentru
sfpentru

În final s[1][1] dă suma maximă posibilă pentru şiruri de n numere ca în


enunţ iar un şir de sumă maximă se obţine astfel :

j←1
pentru i ← 1,n
scrie x[i][j]
j ←u[i][j]
sfpentru

Complexitatea algoritmului de mai sus este O(n2),deci polinomială.

32
Programare dinamică
2 Metoda înapoi
Problemă:
Se dă un şir (vector) de numere naturale x1,…,xn; se cere să se determine un
subşir al său xi1,…,xik (1 <= i1<…<ik<= n) astfel încât xi1+…+xik se divide cu n şi
este maximă cu această proprietate; se cere şi determinarea sumei respective.
Exemplu:
Dacă şirul dat este 2,3,4,9,3 (n=5),
Atunci suma maximă este 15 iar un subşir care o realizează este 2, 4, 9.
Observăm că întodeauna există subşiruri pentru care suma elementelor se
divide cu n.
Într-adevar,dacă calculăm sumele x1,x1+x2,...,x1+...+xn,obtinem n numere
naturale;dacă vreunul se divide cu n,atunci subşirul corespunzător satisface
cerinţa; dacă niciunul nu se divide cu n,atunci resturile lor la n sunt n numere de
la 1 la n+1,deci exista printre ele două care dau acelaşi rest la n; dacă acestea
sunt x1+...+xi şi x1+…+xj,i<j,atunci x(i+1),x(i+2),…,xj este un subşir a cărui sumă a
elementelor se divide cu n.
Totuşi numărul total al subşirurilor nevide ale lui x 1,...,xn este (2n)-1 (ele se
asimilează cu submulţimile nevide ale mulţimii indicilor 1,...,n) iar un algoritm
care să le genereze pe toate pentru a-l alege pe cel optim ar fi de complexitate
exponenţială (deci ineficient).
Observăm însă că dacă xi1,...,xik (1 <=i1<…<ik <=n) este un subşir de sumă
maximă divizibilă cu n (care dă prin împartire la n restul 0) atunci avem
următoarele posibilităţi:
- i1 = ik = n (adică subşirul se reduce la ultimul număr,xn);atunci xn % n = 0;
- i1<ik = n;atunci dacă notam p= (xi1+…+xi(k+1) ) % n, va rezulta că xi1,...,xi(k+1)
este un subşir al şirului x1,...,x(n+1) (chiar al şirului x1 ,...,xi(k+1)) de sumă
maximă care dă prin împarţire la n restul p;
- ik<n;atunci xi1,…,xik este un subşir al şirului x1,...,x(n-1) de sumă maximă
care dă prin împarţire la n restul 0.
Deci se verifică principiul de optimalitate în forma (2),dar condiţiile nu sunt
îndeplinite întocmai,deoarece a doua variantă de mai sus arată că subşirul lui
x1,...,xn de sumă maximă care prin împărţire la n dă restul 0 depinde de un
subşir al şirului x1,...,xn+1 de suma maximă care prin împărţire la n dă un rest p,
0<= p <= n-1,nu neapărat p =0.
Totuşi, dacă considerăm o problemă mai generală,aceea de a determina
pentru orice 0<=p<=n-1 câte un subşir al lui x1,...,xn de sumă maximă care prin
împărţire la n dă restul p,atunci ansamblul optimelor pentru subşiruri ale lui
x1,...,xn (p variind de la 0 la n-1) depinde de ansamblul optimelor pentru
subşiruri ale lui x1,...,x(n-1) (iarăşi p variind de la 0 la n-1), ceea ce justifică
aplicarea metodei înapoi.În continuare vom rezolva problema
generală.Menţionăm că pentru anumiţi p de la 0 la n-1 s-ar putea să nu existe
subşiruri ale lui x1,...,xn a căror sumă modulo n să dea p (de exemplu n=2, x 1=4,
x2=6, p=1).
Pentru a stabili relaţiile de recurenţă aferente notăm:
-s[i][k] - suma maxima care împarţită la n dă restul k şi care se poate realiza
cu un subşir al şirului x1,..., xi (1<=i<=n, 0<=k<=n-1);

33
dacă nu există nuci un subşir cu această proprietate convenim să punem
s[i][k]=0;
-m[i][k] - mulţimea indicilor unui subşir care realizează s[i][k].
Relaţiile de recurenţă sunt urmatoarele:
s[1][k]=x1, m[1][k]={1} (k=x1 % n)
s[1][k]=0, m[1][k]={} (k≠x1 % n)
s[i][k]= maximul între următoarele valori, fiecare luându-se în consideraţie
doar pentru acei k ce verifică condiţiile din paranteze:
xi (dacă xi % n=k)
s[i-1][k] (pentru orice k)
s[i-1][p]+xi (dacă 0<=p<=n-1 şi (s[i-1][p]+xi) % n=k) (2<= i<=n,
0<=k<=n-1)
m[i][k]={i} sau m[i-1][k] sau m[i-1][p]U{i}, în funcţie de varianta care a dat
maximul de mai sus (dacă sunt mai multe, se face o alegere) (2<=i<=n, 0<=k<=n-
1)
Observăm că valoarea s[j][k]=0 convenită în cazul când nu există subşiruri
ale lui x1,..., xj a căror sumă să dea prin împărţire la n restul k nu alterează
calculele de mai sus; într-adevăr, dacă s[i-1][k]=0, el nu afectează maximul de
mai sus întrucât acesta oricum trebuie să fie >=0 (e maximul unor sume de
numere naturale); de asemenea, dacă s[i-1][p]=0, a treia variantă pentru
maximul de mai sus se reduce la prima; în fine, dacă nu există subşiruri ale lui
x1, ..., xi a căror sumă să dea prin împărţire la n restul k, din calculul de mai sus
va rezulta s[i][k]=0 (la calcularea maximului va participa doar varianta a doua).
În final suma maximă divizibilă cu n căutată este s[n][0] iar un subşir al lui x1,
..., xn care o realizează este xi1, ..., xik, unde m[n][0]={i1, …, ik} (i1<…<ik).
Putem organiza eficient calculele folosind doi vectori de numere s, s 1 şi doi
vectori de mulţimi (codificate de exemplu ca vectori caracteristici) m si m 1 astfel:
Algoritm descris în pseudocod
pentru k←0,n-1 execută
s[k]←0 m[k]←{}
sfpentru
s[x[1]%n]←x[1] m[x[1]%n]←{1};
pentru i←2,n-1 execută
s1←s m1←m //s1, m1 reţin stările vechi; s, m vor fi cele noi
dacă s[x[i]%n]<x[i] atunci
s[x[i]%n]←x[i]; m[x[i]%n]←{i}
sfdacă
pentru p←0,n+1 execută
dacă(s[(s1[p]+x[i])%n]<s1[p]+x[i]) atunci
s[(s1[p]+x[i])%n]←s1[p]+x[i];
m[(s1[p]+x[i])%n]←m1[p]U{i}
sfdacă
sfpentru
sfpentru
Complexitatea algoritmului de mai sus este O(n2) sau O(n3) dacă ţinem cont că
o atribuire între submulţimi ale lui {1, ...,n} este O(n);

34
Programare dinamică
3 Metoda mixtă
Problemă:

Se consideră problema înmulţirii optimale a unui şir de matrice. Fie


matricele A1, A2, ..., An, unde Ai are dimensiunile (di-1, di), pentru i = 1, 2, ..., n.. Să
se calculeze produsul R =A1 x A2 x ... x An prin înmulţiri repetate a câte două
matrice şi efectuând un număr minim de operaţii.
Deoarece înmulţirea matricelor este asociativă, matricea produs A1A2A3
poate fi calculată prin ((A1A2)A3), sau (A1(A2A3)). Dacă matricele au dimensiunile
5x3, 3x2 şi respectiv 2x7, atunci calculând în ordinea ((A1A2)A3) efectuăm 100
înmulţiri, iar în ordinea (A1(A2A3)) efectuăm 147 înmulţiri. (Numărul operaţiilor de
înmulţire pentru a calcula produsul B x C este p*q*r, dacă dimensiunile matricelor B
şi C sunt (p, q) respectiv (q, r). Considerăm aici aplicarea algoritmului uzual de
înmulţire a două matrice.)
Problema pusă se reduce la găsirea acelei ordini de asociere pentru care
numărul înmulţirilor să fie minim.
Vom determina varianta optimă de asociere a şirului de matrice fără a
calcula toate posibilităţile de asociere (deoarece numărul lor este mare). Pentru
aceasta notăm cu Ci,j numărul minim de înmulţiri (elementare) necesare calculării
produsului AiAi+1 ... Aj, pentru 1  i  j  n.
Se observă că:
a) Ci,i = 0;
b) Ci,i+1 = di-1 . di . di+1;
c) C1,n este valoarea minimă căutată;
d) Este verificat principiul optimalităţii:
Ci,j = min {Ci,k+Ck+1,j + di-1.dk.dj  i  k < j }
pentru că asocierile sunt de forma (AiAi+1...Ak)(Ak+1Ak+2...Aj). (Relaţia este adevărată
deoarece parantezarea (AiAi+1...Ak) trebuie să fie optimă pentru ca (AiAi+1...Aj) să fie
la rându-i optimă). Deci pentru rezolvare se aplică principiul (3) – metoda mixtă.
Costul optimal C1,n poate fi calculat apelând funcţia recursivă:
Funcţia C(i, j) este:
Dacă i = j atunci C ← 0
altfel
Dacă i = j-1 atunci C ← di-1 . di . di+1
altfel
min ← C(i, i) + C(i+1, j) + di-1 . di . dj
Pentru k ←i+1, j-1 execută
val ← C(i, k) + C(k+1, j) + di-1 . dk . dj
Dacă val < min atunci
min ← val

35
sfdacă
sfpentru
C ← min
sfdacă
sfdacă
sf-C
funcţie care interpretează relaţia Ci,j = min {Ci,k+Ck+1,j + di-1.dk.dj  i  k < j }.
Să observăm în primul rând că în urma acestui calcul nu obţinem decât
costul minim pentru înmulţire. Dacă notăm cu Sij valoarea k pentru care se obţine
minimul (Cij = Ci,k+Ck+1,j + di-1.dk.dj) în calculul de mai sus, atunci vom şti că pentru
produsul AiAi+1...Aj este optim să efectuăm (AiAi+1...Ak)(Ak+1Ak+2...Aj).
În al doilea rând să remarcăm că funcţia recursivă efectuează calcule
redundante. De exemplu, pentru calcularea lui C(1, 4) se efectuează calcule după
cum indică figura următoare:

C(1,4)

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

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

Pentru evitarea calculării de mai multe ori a acestor valori ale funcţiei C, putem
proceda după cum urmează:
Subalgoritmul InmulţireOptimă(n, d, C, S) este:
{Dimensiunile matricelor sunt: di-1x di, i = 1, ..., n}
{Rezultatele sunt matricele C şi S descrise mai sus.}
Pentru i ←1, n execută
Cij ← 0
sfpentru
Pentru l ←2, n execută {diagonala superioară l din
matrice}
Pentru i ←1, n-l+1 execută {linia de pe acea diagonala}
Fie j ← i + l -1 {Elementul Cij, i =1,...,n-l+1}
Cij ← Infinit {Ci,j = min {Ci,k+Ck+1,j + di-1.dk.dj 
ik<j}
Pentru k ←i, j-1 execută
cost ← Cik + Ck+1,j + di-1.dk.dj
Dacă cost < Cij atunci
Cij ← cost {Valoarea pentru costul
minim}
Sij ← k {Indică poziţia
parantezării}
sfdacă
36
sfpentru
sfpentru
sfpentru

Matricele C şi S se calculează în ordinea diagonalelor, după cum indică


figura următoare:

1 2 3 4 l=2
1
2
l=3

3 l=4
4

Ciclul cu contorul l calculează diagonalele superioare ale matricelor C şi S.


Pentru un l dat, ciclul cu contorul i fixează liniile i care au elemente pe acea
diagonală. Indicele j fixează coloana corespunzătoare liniei fixate, iar ciclul cu
contorul k corespunde formulei Ci,j = min {Ci,k+Ck+1,j + di-1.dk.dj  i  k < j }.
Odată calculată matricea S care indică parantezarea, construirea unei
soluţii optime
R ← A1A2...An se efectuează apelând ProdusSirMatrice(R, A, S, 1, n), unde:

Subalgoritmul ProdusSirMatrice(A, S, i, j) este: {Calculează optim R ←


AiAi+1...Aj}
Dacă j > i atunci
ProdusSirMatrice(X, A, S, i, Sij) {X ← AiAi+1...Ak, cu k =
Sij}
ProdusSirMatrice(Y, A, S, Sij+1, j) {Y ← Ak+1Ak+2...Aj}
Fie R ← Produs(X, Y) { R ← X Y}
altfel
Fie R ← Ai
sfdacă
sfSubalgoritm

Subalgoritmul ProdusSirMatrice foloseşte matricea S calculată. Deoarece


valoarea lui k=S1n indică unde este parantezarea optimă pentru A1A2...An, aplicăm
metoda divizării calculând produsele A1A2...Ak şi Ak+1Ak+2...An şi combinăm
rezultatele (înmulţind matricele rezultate).
Complexitatea algoritmului este o(n3) deoarece sunt trei cicluri for
imbricate.

Concluzii

37
Am văzut deci că pentru a rezolva o problemă prin programare dinamică
trebuie pus în evidenţă principiul de optimalitate ((1),(2) sau (3)) pe care aceasta
îl verifică şi releţiile de recurenţă care cuantifică modul de obţinere a optimurilor
„mai generale” din optimuri “mai particulare” (şi prin ce decizii).
În general maniera de a demonstra verificarea unui principiu de optimalitate
si determinarea relaţiilor de recurenţă aferente se face foarte diferit de la o
problema la alta (nu se pot da nişte prescripţii generale) şi de multe ori e foarte
dificil.
Rezolvarea problemelor prin programare dinamică (folosind acele calcule
recurente) se face însă în timp polinomial, deoarece fiecare optim „mai general”
se calculează din optimele „mai particulare” făcând o căutare în timp polinomial,
iar aceste optime odată calculate nu se mai recalculează ulterior ci se trece la
calcularea optimelor „şi mai generale”.
De aceea metodele de programare dinamică se doresc a fi o alternativă la
metoda backtracking: este clar că problemele abordabile prin backtracking se
încadrează în tiparul problemelor abordabile prin programare dinamică – stările
sunt poziţiile 0<= i <= n până la care s-a completat o soluţie, starea iniţială este
vectorul vid (i=0), starea finală este vectorul complet (i=n), iar o decizie constă
în alegerea unei valori v pentru poziţia i (ea duce sistemul din starea de
completare pană la poziţia i-1 în starea de completare pană la pozitia i). Dacă
pentru o astfel de problemă se reuşeşte demonstrarea unui principiu de
optimalitate(şi determinarea relaţiilor de recurenţă aferente), problema se va
rezolva prin metoda de programare dinamică corespunzatoare(înainte, înapoi
sau mixtă) folosind relaţiile de recurenţă evidenţiate, în timp polinomial. Dacă nu
se reuşeste acest lucru, problema se va rezolva prin backtraking (care este o
metodă universală), oţinând un algoritm ce poate ajunge (în cazul cel mai
nefavorabil) exponenţial.

38
1.
O tabla de sah se citeste ca o matrice n*n in care pozitiile libere au
valoarea 0, iar piesele sunt marcate prin valoarea 1.
Pe prima linie pe coloana js se afla un pion. Sa se determine drumul
pe care poate ajunge pionul pe ultima linie luand un numar maxim de
piese.
Pozitia initiala a pionului se considera libera.
Pionul aflat in pozitia i,j se poate deplasa astfel:
- in pozitia i+1,j daca e libera
- in pozitia i+1, j-1 daca e piesa in aceasta pozitie
- in pozitia i+1, j+1 daca e piesa in aceasta pozitie

Exemplu:
53
00000
01010
01111
00011
01011

Drumul optim este:


13
22
33
44
55
Pe acest drum pionul ia 4 piese.
2.
In fisierul m.in se afla pe prima linie un numar natural n cel mult
20000,iar pe liniile urmatoare o matrice patratica de dimensiune n

39
care contine doar elemente 0 si 1.
Sa se determine cel mai mare patrat care contine doar valori 1. Se for
afisa in fisierul text m.out urmatoarele valori separate prin spatiu:
latura patratului, linia si coloana coltului stanga sus al patratului.
Daca exista mai mult astfel de patrate se va afisa doar cel mai de sus.
Exemplu:
5
11000
11101
11111
11100
10111
Se afiseaza 3 2 1
3.
Se dau n(n+1)/2 numere naturale aranjate intr-un triunghi format din
elementele de sub si de pe diagonala unei matrici patratice de ordin
n.
Se calculeaza sume pornind din elementul de pe prima linie prin
deplasari in vecinii de sub si din dreapta. Gasiti suma maxima care se
poate calcula astfel si care sunt valorile din care se obtine suma
maxima.
Exemplu:
n=4
2
35
634
5614
suma maxima este 17
si se obtine din valorile 2 3 6 6
4.
Se citeste o matrice patratica de ordin n formata din numere naturale.
Se calculeaza sume pornind de pe prima linie prin deplasari in vecinii
de sub si din dreapta. Gasiti suma maxima care se poate calcula
astfel si care sunt valorile din care se obtine suma maxima.
Exemplu:
n=4
7158
3561
6348
5614
suma maxima este 23
si se obtine din valorile 5 6 8 4
5.
Se citeste o matrice patratica de ordin n formata din numere naturale.
Se calculeaza sume pornind de pe prima linie prin deplasari pe linia
urmatoare in unul dintre cei 3 vecini de pe aceeasi coloana sau de pe

40
cele 2 alaturate. Gasiti suma maxima care se poate calcula astfel si
care sunt valorile din care se obtine suma maxima.
Exemplu:
n=4
8158
3561
6348
5614
suma maxima este 26
si se obtine din valorile 8 6 8 4
6.
Calculati cate submultimi cu k elemente are o multime cu n elemente.
7.
Un paianjen a tesut o panza de forma dreptunghiulara formata din n
linii orizontale si m linii verticale.
Calculati in cate moduri poate el merge din coltul stanga-sus in coltul
dreapta-jos facand un numar minim de pasi. (n+m-2)
Exemple:
pentru n=3 si m=3 exista 6 moduri
pentru n=1 si m=5 exista un singur mod

8.
Se citeste un numar natural n si apoi un vector cu n elemente numere
intregi. Determinati secventa din vector care are suma elementelor
maxima.
Exemplu:
n=9
-2 1 -3 3 -1 4 -6 2 3
secventa de suma maxima este 3 -1 4 si are suma 6
9.
Subsir crescator maximal.
Se citeste un numar n si apoi un sir de n numere intregi. Gasiti cel
mai lung subsir al sirului citit care are proprietatea ca elementele sunt
in ordine crescatoare.
Daca exista mai multe subsiruri de lungime maxima se va afisa unul

41
dintre ele.
Exemplu:
date.in
9
423052698
date.out
23569
10.
Se citeste un numar n si apoi 2 siruri formate din cate n cuvinte
fiecare. Primul sir de cuvinte stabileste ordinea initiala, iar al doilea
este o permutare a primului (aceleasi cuvinte, dar in alta ordine).
Gasiti cel mai lung subsir de cuvinte din cel de-al doilea sir care are
proprietatea ca are cuvintele in ordinea din primul sir de cuvinte. Se
va afisa numarul maxim de cuvinte si apoi cuvintele.
Daca exista mai multe subsiruri de lungime maxima se va afisa unul
dintre ele.
Exemplu:
date.in
5
platon kant marx stalin havel
marx stalin kant platon havel
date.out
3
marx stalin havel
11.
O tabla de sah se citeste ca o matrice n*n in care pozitiile libere au
valoarea 0, iar piesele sunt marcate prin valoarea 1.
Sa se determine drumul pe care poate ajunge un pion de pe prima
linie pe ultima linie luand un numar maxim de piese. Pe prima linie nu
sunt piese si pionul poate porni din orice pozitie de pe prima linie
Pozitia initiala a pionului se considera libera.
Pionul aflat in pozitia i,j se poate deplasa astfel:
- in pozitia i+1,j daca e libera
- in pozitia i+1, j-1 daca e piesa in aceasta pozitie
- in pozitia i+1, j+1 daca e piesa in aceasta pozitie

Exemplu:
5
00000
01010
01111
00011
01011

Drumul optim este:


11

42
22
33
44
55
Pe acest drum pionul ia 4 piese.
12. :
Cladirea Finantelor publice este formata din birouri dispuse intr-un
dreptunghi cu nXm elemente. Intre doua birouri se poate trece daca
sunt alaturate pe linie sau pe coloana.
Pentru fiecare birou se cunoaste valoare taxei care trebuie platita in
acel birou (valoare naturala). Un contribuabil intra in cladire prin
biroul 1,1 si trebuie sa o parareasca prin biroul n,m. Calculati suma
minima a taxelor pe care le poate plati contribuabilul de la intrare
pana la iesirea din cladire.
Exemplu:
n=4, m=3, dispunerea birourilor si taxa din fiecare:
372
643
631
622
Valoarea minima pe care o poate plati contribuabilul este 18
(corespunde parcurgerii birourilor cu taxele: 3 7 2 3 1 2)
13.
Se da o matrice patratica de ordin n care contine numere naturale si
care are liniile si coloanele numerotate de la 1 la n. Se citeste apoi un
numar natural m si n perechi de pozitii din matrice de forma (i1, j1) si
(i2,j2) astfel incat i1 sa fie mai mic decat i2 si j1 sa fie mai mic decat
j2.
Calculati si afisati pentru fiecare pereche de pozitii suma elementelor
din matrice aflate in submatricea care are coltul stanga-sus in (i1,j1)
si coltul dreapta-jos in (i2,j2).
Exemplu:
date.in
3
121
361
136
3
2233
2133
1113
date.out
16 20 4
14. (Be a Smart Raftsman)
Sunteţi membri ai unui echipaj de rafting care constă din N ≤ 10
participanţi. Puteţi naviga pe râu, şi scopul vostru este să treceţi de
43
M ≤ 1000 curenţi consecutivi şi să ajungi de la punctul de început
la punctul de sfârşit în timp minim. Cel de-al i-lea curent se
caracterizează prin greutatea critică ci, iar al j-lea participant este
caracterizat greutatea sa wj. În cazul în care pluta trece prin al i-
lea curent cu oameni la bord cu greutatea totală mai mare de ci, ea
se răstoarnă. Această parte a rafting-ului este cea mai interesantă,
dar este nevoie de timp suplimentar pentru a te urca pe plută după
răsturnare. Deci, uneori este mai bine să se urce un grup de
oameni cu greutate totală care nu depăşeşte greutatea critică a
plutei, în timp ce restul parcurg distanţa pe jos.
Mai formal, vom considera M + 1 puncte P0, P1, ..., PM, în cazul
în care P0 este începutul şi PM este punctul final. Fiecare dintre
punctele intermediare Pi (1 ≤ i ≤ M-1) este situat între i-lea şi (i +
1)-lea curent. Dacă pluta se răstoarnă, sunt necesare Di minute
pentru a ajunge de la Pi-1 la Pi, altfel sunt necesare di minute. Cel
de-al j-lea participant poate merge pe jos de la Pi-1 la Pi în tj
minute, iar pentru a se urca sau a coborî din plută are nevoie de sj
minute. Înainte de fiecare curent, grupul vostru este împărţit în
două părţi. Prima parte trece prin curent cu pluta, iar a doua parte
merge pe mal spre următorul punct. Cei care ajung primii îi
aşteaptă pe toţi ceilalţi. Apoi, unii participanţi care sunt pe plută
se pot da jos, în timp ce alţi participanti care sunt pe mal se pot
urca pe plută. Această activitate începe când ajung pluta şi toţi cei
de pe mal la punctul stabilit. Timpul total necesar pentru această
acţiune este egal cu suma valorilor sj pentru toate persoanele care
schimbă locul (persoanele urcă şi coboară pe rând). Nimeni nu
poate începe deplasarea la următorul punct, până când nu s-au
mutat toţi membrii.
Incepeţi de la punctul de P0 cu tot grupul pe mal şi trebuie să
terminaţi la punctul PM cu toţi participanţii şi pluta pe mal. Nu
aveţi voie să părăsiţi pluta la început sau într-un un punct
intermediar şi să mergeţi pe jos spre linia de sosire fără ea. Aveţi
posibilitatea să urcaţi tot grupul de salvare în plută, dar nu puteţi
lăsa pluta goală în timpul călătoriei printr-un curent.
Sarcina voastră este să calculaţi timpul minim necesar pentru a
ajunge la linia de sosire.
date.in

44
2 3
50 5 1
70 20 1
30 15 10
60 100 10
70 100 10
date.out
51

REZOLVARI

1.
#include<fstream>
using namespace std;
ifstream fin("pion.in");
ofstream fout("pion.out");
int n,i,j, a[50][50], c[50][50],b[50][50],is,js;
void citire()
{ int i,j;
fin>>n>>js;
is=1;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++) fin>>a[i][j];
}
void pd()
{ int i,j;
b[is][js]=1;
for(i=2;i<=n;i++)
{
for(j=1;j<=n;j++)
{ if(a[i][j]==0)
{ if(b[i-1][j]>0) { c[i][j]=c[i-1][j];
b[i][j]=1; }
}
else if(c[i-1][j-1]>c[i-1][j+1])
{
if(b[i-1][j-1]>0) {c[i][j]=c[i-
1][j-1]+1;b[i][j]=1; }
else if(b[i-1][j+1]>0)
{c[i][j]=c[i-1][j+1]+1;b[i][j]=1; }
}
else {
if(b[i-1][j+1]>0) {c[i][j]=c[i-
1][j+1]+1;b[i][j]=1; }
else if(b[i-1][j-1]>0)
{c[i][j]=c[i-1][j-1]+1;b[i][j]=1; }
}
}
}

45
}
void drum(int i, int j)
{
if(i==1) fout<<i<<" "<<j<<endl;
else { if(a[i][j]==0) drum(i-1,j);
else if(c[i-1][j-1]+1==c[i][j])
drum(i-1,j-1);
else drum(i-1,j+1);
fout<<i<<" "<<j<<endl;
}
}
void afis()
{
int max=0,jm;
for(j=1;j<=n;j++) if(c[n][j]>max) { max=c[n][j]; jm=j; }
fout<<max<<endl;
drum(n,jm);
}

int main()
{ citire();
pd();
afis();
fin.close();
fout.close();
return 0;
}

2.
#include<fstream>
using namespace std;

int n, a[3][20001],maxx,im,jm;
ifstream fin("m.in");
ofstream fout("m.out");

void Read();
void Write();

int main()
{
Read();
Write();
fin.close();
fout.close();
return 0;
}

void Read()
{ int i,j,m,x;

46
fin>>n;
for(i=1;i<=n;i++)
{
for(j=1;j<=n;j++)
{ fin>>x;
if(i==1) a[i][j]=x;
else
{ if(x==0) a[2][j]=0;
else if(j==1) a[2][j]=x;
else
{
m=a[1][j-1];
if(a[1][j]<m) m=a[1][j];
if(a[2][j-1]<m) m=a[2][j-1];
a[2][j]=m+1;
if(a[2][j]>maxx) { maxx=a[2][j];
im=i;
jm=j;
}
}
}
a[1][j-1]=a[2][j-1];
}
a[1][n]=a[2][n];
}
}

void Write()
{ fout<<maxx<<" "<<im-maxx+1<<" "<<jm-maxx+1;
}

3.
#include <fstream>
using namespace std;
ifstream fin ("date.in");
ofstream fout ("date.out");

void citire(int &n, int a[100][100])


{
int i,j;
fin>>n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)

47
fin>>a[i][j];
}

void drum(int n, int a[100][100], int s[100][100], int i, int j)


{
if(i>1)
{
if(s[i][j]-s[i-1][j-1]==a[i][j]) drum (n,a,s,i-1,j-1);
else drum (n,a,s,i-1,j);
fout<<a[i][j]<<" ";
}
else fout<<a[1][1]<<" ";
}

int main()
{
int i,j,a[100][100],n,s[100][100],maxx;
citire(n,a);
s[1][1]=a[1][1];
for(i=2;i<=n;i++)
for(j=1;j<=i;j++)
if(j==1) s[i][j]=s[i-1][j]+a[i][j];
else if(j==i) s[i][j]=s[i-1][j-1]+a[i][j];
else if(s[i-1][j]<s[i-1][j-1]) s[i][j]=a[i][j]+s[i-1][j-1];
else s[i][j]=a[i][j]+s[i-1][j];

maxx=0;
int mj;
for(j=1;j<=n;j++) if(s[n][j]>maxx) { maxx=s[n][j]; mj=j;}
fout<<maxx<<endl;
drum(n,a,s,n,mj);
fin.close();
fout.close();
return 0;
}

4.

48
#include <fstream>
using namespace std;
ifstream fin ("date.in");
ofstream fout ("date.out");

void citire(int &n, int a[100][100])


{
int i,j;
fin>>n;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
fin>>a[i][j];
}

void drum(int n, int a[100][100], int s[100][100], int


i, int j)
{
if(i>0)
{
if(s[i][j]-s[i-1][j-1]==a[i][j]) drum (n,a,s,i-
1,j-1);
else drum (n,a,s,i-1,j);
fout<<a[i][j]<<" ";
}
}

int main()
{
int i,j,a[100][100],n,s[100][100],maxx;
citire(n,a);
for(j=1;j<=n;j++) s[1][j]=a[1][j];
for(i=2;i<=n;i++)
for(j=1;j<=n;j++)
if(j==1) s[i][j]=s[i-1][j]+a[i][j];
else if(s[i-1][j]<s[i-1][j-1])
s[i][j]=a[i][j]+s[i-1][j-1];
else s[i][j]=a[i][j]+s[i-1][j];

maxx=0;
int mj;
for(j=1;j<=n;j++) if(s[n][j]>maxx) { maxx=s[n][j];
mj=j;}
fout<<maxx<<endl;
drum(n,a,s,n,mj);
fin.close();
fout.close();
return 0;
}

5.

49
#include <fstream>
using namespace std;
ifstream fin ("date.in");
ofstream fout ("date.out");

void citire(int &n, int a[100][100])


{
int i,j;
fin>>n;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
fin>>a[i][j];
}

void drum(int n, int a[100][100], int s[100][100], int


i, int j)
{
if(i>0)
{
if(s[i][j]-s[i-1][j+1]==a[i][j]) drum (n,a,s,i-
1,j+1);
else if(s[i][j]-s[i-1][j-1]==a[i][j]) drum
(n,a,s,i-1,j-1);
else drum (n,a,s,i-1,j);
fout<<a[i][j]<<" ";
}
}

int main()
{
int i,j,a[100][100]={0},n,s[100][100]={0},maxx;
citire(n,a);
for(j=1;j<=n;j++) s[1][j]=a[1][j];
for(i=2;i<=n;i++)
for(j=1;j<=n;j++)
if(s[i-1][j]<=s[i-1][j-1] && s[i-
1][j+1]<=s[i-1][j-1]) s[i][j]=a[i][j]+s[i-1][j-1];
else if(s[i-1][j]>=s[i-1][j-1] && s[i-
1][j]>=s[i-1][j+1]) s[i][j]=a[i][j]+s[i-1][j];
else s[i][j]=a[i][j]+s[i-1][j+1];

maxx=0;
int mj;
for(j=1;j<=n;j++) if(s[n][j]>maxx) { maxx=s[n][j];
mj=j;}
fout<<maxx<<endl;
drum(n,a,s,n,mj);
fin.close();
fout.close();
return 0;
}

50
6.
#include <fstream>
using namespace std;
ifstream fin ("date.in");
ofstream fout ("date.out");

int main()
{
unsigned long int a[500][500]={0};
int i,j,k,n;
fin>>n>>k;
a[0][0]=1;
for(i=1;i<=n;i++)
for(j=0;j<=i;j++)
if(j==0 || j==i) a[i][j]=1;
else a[i][j]=a[i-1][j]+a[i-1][j-1];

fout<<a[n][k]<<endl;
fin.close();
fout.close();
return 0;
}
7.
#include<fstream>
using namespace std;
ifstream fin ("date.in");
ofstream fout ("date.out");

int main()
{
unsigned long int a[500][500]={0};
int i,j,m,n;
fin>>n>>m;
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
if(i==1 || j==1) a[i][j]=1;
else a[i][j]=a[i-1][j]+a[i][j-1];

fout<<a[n][m]<<endl;
fin.close();
fout.close();
return 0;
}
8.
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");

51
int main()
{
int n,i,a[10000],s[10000]={0},maxx,im,jm;
fin>>n;
for(i=1;i<=n;i++)
fin>>a[i];
maxx = a[1];
for(i=1;i<=n;i++)
{
s[i]=a[i];
if(s[i]<s[i-1]+a[i]) s[i]=s[i-1]+a[i];
if(s[i]>maxx)
{ maxx=s[i];
jm=i;
}
}
fout<<maxx<<endl;
im=jm;
while(im>0 && s[im]>=0) im--;
im++;
for(i=im;i<=jm;i++) fout<<a[i]<<" ";
return 0;
}
9.
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int n, A[10000], L[10000], maxt, pm;

void afis(int k, int m)


{
if(m>0)
{
int i=k-1;
while(L[i]!=m-1) i--;
afis(i,m-1);
fout<<A[k]<<" ";
}
}

int main()
{
int i,j,maxx;
fin>>n;
for(i=1;i<=n;i++) fin>>A[i];
L[1]=1;
for(i=2;i<=n;i++)
{

52
maxx=0;
for(j=1;j<i;j++)
if(A[j]<=A[i] && L[j]>maxx) maxx=L[j];
L[i]=maxx+1;
if(L[i]>maxt)
{
maxt=L[i];
pm=i;
}
}
afis(pm,maxt);
return 0;
}
10.

#include <fstream>
#include <cstring>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int n, A[10000], L[10000], maxt, pm;
char O[10000][50], S[10000][50];

void citire()
{
int i;
fin>>n;
for(i=1;i<=n;i++) fin>>O[i];
for(i=1;i<=n;i++) fin>>S[i];
}

int poz(int k)
{
int i;
for(i=1;i<=n;i++)
if(strcmp(O[i],S[k])==0) return i;
return 0;
}

void afis(int k, int m)


{
if(m>0)
{
int i=k-1;
while(L[i]!=m-1) i--;
53
afis(i,m-1);
fout<<O[A[k]]<<" ";
}
}

int main()
{
int i,j,maxx;
citire();
for(i=1;i<=n;i++) A[i]=poz(i);
L[1]=1;
for(i=2;i<=n;i++)
{
maxx=0;
for(j=1;j<i;j++)
if(A[j]<=A[i] && L[j]>maxx) maxx=L[j];
L[i]=maxx+1;
if(L[i]>maxt)
{
maxt=L[i];
pm=i;
}
}
fout<<maxt<<endl;
afis(pm,maxt);
return 0;
}

11.
#include<fstream>
using namespace std;
ifstream fin("pion.in");
ofstream fout("pion.out");
int n,i,j, a[50][50], c[50][50];
void citire()
{ int i,j;
fin>>n;
is=1;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++) fin>>a[i][j];
}
void pd()
{ int i,j;
for(i=2;i<=n;i++)

54
for(j=1;j<=n;j++)
if(a[i][j]==0) c[i][j]=c[i-1][j];
else if(c[i-1][j-1]>c[i-1][j+1])
c[i][j]=c[i-1][j-1]+1;
else c[i][j]=c[i-1][j+1]+1;

}
void drum(int i, int j)
{
if(i==1) fout<<i<<" "<<j<<endl;
else { if(a[i][j]==0) drum(i-1,j);
else if(c[i-1][j-1]+1==c[i][j])
drum(i-1,j-1);
else drum(i-1,j+1);
fout<<i<<" "<<j<<endl;
}
}
void afis()
{
int max=0,jm;
for(j=1;j<=n;j++) if(c[n][j]>max) { max=c[n][j]; jm=j; }
fout<<max-1<<endl;
drum(n,jm);
}

int main()
{ citire();
pd();
afis();
fin.close();
fout.close();
return 0;
}

12.
#include<fstream>
using namespace std;
ifstream fin ("date.in");
ofstream fout ("date.out");

int main()
{
int a[100][100],s[100][100]={0};
int i,j,m,n;
fin>>n>>m;
for(i=1;i<=n;i++)
for(j=1;j<=m;j++)
{
fin>>a[i][j];
if(i==1 && j==1) s[i][j]=a[i][j];

55
else if(i==1) s[i][j]=s[i][j-1]+a[i][j];
else if(j==1) s[i][j]=s[i-1][j]+a[i][j];
else if(s[i-1][j]<s[i][j-1])
s[i][j]=s[i-1][j]+a[i][j];
else s[i][j]=s[i][j-1]+a[i][j];
}

fout<<s[n][m]<<endl;
fin.close();
fout.close();
return 0;
}
13.
#include <fstream>
using namespace std;
ifstream fin("date.in");
ofstream fout("date.out");
int b[500][500];
int main()
{
int n,m,i,j,k,l,c;
fin>>n;
for(i=1;i<=n;i++)
for(j=1;j<=n;j++)
{ fin>>b[i][j];
b[i][j]=b[i][j]+b[i-1][j]+b[i][j-1]-b[i-1][j-
1];
}
fin>>m;
for(k=0;k<m;k++)
{
fin>>i>>j>>l>>c;
fout<<b[l][c]-b[i-1][c]-b[l][j-1]+b[i-1][j-1]<<" ";
}
fin.close();
fout.close();
return 0;
}
14.

Din enunţ intuim că o componentă a stării trebuie să fie submulţimea


participanţilor aflaţi în plută în punctul curent, intuiţie confirmată de
numărul mic al participanţilor (N ≤ 10). Există 210 astfel de variante, din
care vom exclude varianta în care pluta este goală. O stare a problemei,
reprezentănd soluţia unei subprobleme, va fi alcătuită din poziţia curentă
a plutei (punctul Pi unde se află) şi submulţimea oamenilor care se află în
plută. Observăm că de la punctul precedent pluta a traversat curentul, au
urcat nişte oameni apoi au coborât alţii; aceşti paşi contribuie la timpul

56
total şi reprezintă 2 părţi ale recurenţei soluţiei. Dacă notăm cu Tm[i][S]
timpul minim necesar pentru a ajunge în punctul i cu submulţimea S a
oamenilor din plută, putem descrie problema prin relaţiile:

unde Tu[S'][S] este timpul necesar pentru urcarea participanţilor care se


găsesc în mulţimea S dar nu fac parte din mulţimea S' (aceşti participanţi
alcătuiesc mulţimea Su), Tc[S'][S] este timpul necesar pentru coborârea
participanţilor din plută (participanţii din mulţimea Sc), iar Tt[i][S] este
timpul necesar traversării curentului de la punctul i-1 la punctul i dacă în
plută se află participanţii din S. Formulele pentru valorile Tu, Tc şi Tt
sunt:

Calcularea directă a soluţiei pe baza recurenţelor de mai sus are


complexitatea O(M*N*2N + M*2N*2N). Primul termen al complexităţii
este dat de calcularea valorilor Tt, iar al doilea de calcularea matricii Tm.
Valoarea rezultatului nu depinde de ordinea în care urcă şi coboară
participanţii din plută, deci o putem alege noi. Dacă aranjăm ordinea în
doi paşi astfel încât în primul pas doar să urce, apoi în al doilea pas doar
să coboare, vom obţine o îmbunătăţire semnificativă a timpului de
execuţie. Se observă că dacă am efectua doar adăugări de elemente (daca
în plută s-ar putea doar urca persoane, fără să poată coborî), atunci $S' \in
S$ şi deci reprezentarea în cod binar a mulţimii S' ar avea o valoare mai
mică decât reprezentarea binară a mulţimii S. Dacă ar avea voie doar să
coboare persoane, relaţia ar fi inversă. De aceea, vom ordona operaţiile de
urcare şi coborâre astfel încât să aibă loc întâi toate urcările (astfel încât
să putem parcurge crescător valorile binare ale mulţimilor) apoi toate
coborârile (astfel încât să putem efectua a doua parcurgere, în ordine
descrescătoare). Introducem matricea auxiliară Tum[i][S] care reprezintă
timpul minim necesar la care se poate afla pluta în punctul i cu mulţimea
S de oameni, dacă după ultima traversare nu a coborât încă nici o
persoană. Starea S a fost obţinută prin una sau mai multe urcări de
persoane în plută sau direct prin traversarea curentului. Recurenţele
pentru Tum[i][S] sunt:

57
Am eliminat calcularea matricilor Tu, Tc. Toate matricile rămase au
dimensiunea Mx2N iar calcularea fiecărui element necesită un timp O(N),
deci soluţia astfel obţinută are complexitatea O(M*N*2N).

iniţializează toate Tm[0];


pentru i = 1, M execută
calculează toate Tt[i];
pentru fiecare S în ordine crescătoare a reprezentării binare execută
Tum[i][S] = Tm[i-1][S] + Tt[i][S];
pentru fiecare j din S execută
Tum[i][S] = min(Tum[i][S], Tum[i][S - j] + s[j]);
sfârşit pentru;
sfârşit pentru;
pentru fiecare S în ordine descrescătoare a reprezentării binare execută
Tm[i][S] = Tum[i][S];
pentru fiecare j care nu există în S execută
Tm[i][S] = min(Tm[i][S], Tm[i][S + j] + s[j]);
sfârşit pentru;
sfârşit pentru;
sfârşit pentru;
returnează Tm[M][0];

Mai există două optimizări de spaţiu pe care le putem efectua în soluţia


prezentată. Putem elimina matricea Tum, calculând toate valorile direct
pe matricea Tm, deoarece aceasta va fi parcursă în ambii paşi în câte o
singură direcţie. A doua optimizare se bazează pe observaţia că nu avem
niciodată nevoie de alte linii în afară de ultimele 2 (i si i-1), deci putem
înlocui matricea cu 2 vectori de dimensiune 2N. Valorile Tt pot fi
calculate în cadrul primei bucle, reducând astfel spaţiul necesar soluţiei la
O(2N).

58
INFORMATICA 5

Metoda programării dinamice


Prof. Emanuela Cerchez
Liceul de Informatica „Grigore Moisil” Iasi

Prezentare generală
Programarea dinamică este o metodă de elaborare a algoritmilor care se aplică
în general problemelor pentru care se cere determinarea unui optim în urma
adoptării unor decizii.
Nu există un criteriu pe baza căruia să identificăm cu siguranţă o problemă
pentru rezolvarea căreia trebuie să utilizăm metoda programării dinamice, dar
putem formula două proprietăţi care sugerează o soluţie prin programare dinamică.

Substructură optimală
Problema dată poate fi descompusă în subprobleme şi soluţia optimă a
problemei depinde de soluţiile optime ale subproblemelor sale.
Acest criteriu nu indică neapărat o soluţie prin programare dinamică, ar putea fi
şi un indiciu că se poate aplica metoda Greedy sau metoda „Divide et Impera”.
 
 Subprobleme superpozabile
Subproblemele problemei date nu sunt independente, ci se suprapun.
Datorită faptului că subproblemele problemei date se suprapun, deducem că o
abordare prin metoda „Divide et Impera” ar fi dezastruoasă din punctul de vedere
al timpului de execuţie (datorită faptului că problemele se suprapun se ajunge la
rezolvarea repetată a aceleiaşi subprobleme). Prin urmare, vom rezolva
subproblemele o singură, dată, reţinând rezultatele într-o structură de date
suplimentară (de obicei un tablou).
Rezolvarea unei probleme prin programare dinamică presupune următorii paşi:
• Se identifică subproblemele problemei date.
• Se alege o structură de date suplimentară, capabilă să reţină soluţiile
subproblemelor.
• Se caracterizează substructura optimală a problemei printr-o relaţie de
recurenţă.
• Pentru a determina soluţia optimă, se rezolvă relaţia de recurenţă în mod
bottom-up (se rezolvă subproblemele în ordinea crescătoare a dimensiunii lor).

59
În cele ce urmează vom exemplifica pas cu pas modul de rezolvare a
problemelor prin metoda programării dinamice.

60
6 METODA PROGRAMĂRII DINAMICE

1. Înmulţirea optimală a matricelor


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 două elemente. În funcţie de modul de parantezare diferă numărul 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 să fie minim).

Exemplu
Pentru n=3 matrice cu dimensiunile (10,1000), (1000,10) şi
(10,100), produsul A1xA2xA3 se poate calcula în două moduri:
• (A1xA2)xA3 necesitând 1000000+10000=1010000 înmulţiri elementare
• A1x(A2xA3), necesitând 1000000+1000000=2000000 înmulţiri.
Reamintim că numărul de înmulţiri elementare necesare pentru a înmulţi o
matrice A cu n linii şi m coloane şi B o matrice cu m linii şi p coloane este nmp
(vezi problema 8 de la capitolul „Structuri elementare de date”).

Solutie
• Pentru a calcula A1xA2x...xAn, în final trebuie să înmulţim două matrice,
deci vom paranteza produsul astfel: (A1xA2x...xAk)x(Ak+1x...xAn ).
Această observaţie se aplică şi 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n. Observăm că
subproblemele nu sunt independente. De exemplu, calcularea produsului
AixAi+1x...xAj şi calcularea produsului Ai+1xAi+2x...xAj+1, au ca
subproblemă comună calcularea produsului Ai+1x...xAj.
• Pentru a reţine 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 să satisfacă următoarea relaţie de recurenţă:
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

61
INFORMATICA 7

Cum interpretăm această relaţie de recurenţă? 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 şi j-1), şi
alegem varianta 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 şi costul
înmulţirii celor două 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,
pentru care se obţine minimul. Nu vom considera un alt tablou, ci-l vom reţine,
pe poziţia simetrică faţă de diagonala principală (M[j][i]).
5. Rezolvarea recursivă a relaţiei de recurenţă de mai sus este ineficientă, datorită
faptului că subproblemele de suprapun, deci o abordare recursivă ar conduce la
rezolvarea aceleiaşi subprobleme de mai multe ori. Prin urmare vom rezolva
relaţia de recurenţă în mod bottom-up: (determinăm 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=cate matrice se inmultesc
for (i=1; i<=n-nr+1; i++)
{j=i+nr-1;
//se inmultesc nr matrice, de la Ai la Aj
for (k=i, min=Infinit; k<j; k++)
//determin minimul si pozitia 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 principală în matricea M:
void afisare(int i, int j)
{//afiseaza parantezarea optimala a produsului
Aix...xAj if (i==M[j][i]) cout<<"A"<<i;
else {cout<<"("; afisare(i,M[j][i]);
cout<<")";} cout<<"x";
if (j==M[j][i]+1) cout<<"A"<<j;
else {cout<<"("; afisare(M[j][i]+1,j); cout<<")";}
}

62
8 METODA PROGRAMĂRII DINAMICE

2. 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n. 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 că 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 incepe cu ai, este necesar să determinăm cele
mai lungi subşiruri crescătoare care încep cu aj, aiaj, j{i+1,.., n}.
• Pentru a reţine soluţiile subproblemelor vom considera doi vectori suplimentari
l şi poz, fiecare cu câte n componente, având semnificaţia:
l[i]=lungimea celui mai lung subşir crescător care începe cu a[i];
poz[i]=poziţia 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 există.
• Relaţia de recurenţă 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].
4. Rezolvăm 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;}

63
INFORMATICA 9

Pentru a determina soluţia optimă a problemei, determinăm maximul din


vectorul l, apoi afişăm soluţia, începând cu poziţia maximului şi 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 subsir crescator: "
<<max; cout<<"\nCel mai lung subsir:\n";
for (i=pozmax; i!=-1; i=poz[i])
cout<<a[i]<<' ';

3. Sumă în triunghi
Sa considerăm un triunghi format din n linii (1<n100), fiecare linie
conţinând numere întregi din domeniul [1,99], ca în exemplul următor:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
Problema constă în scrierea unui program care să 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. (IOI, Suedia 1994)

Soluţie
• Vom reţine triunghiul într-o matrice pătratică T, de ordin n, sub diagonala
principală. 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 maximă a numerelor de pe un
drum de la T[i][j] la ultima linie, trebuie să calculăm suma maximă a
numerelor de pe un drum de la T[i+1][j] la ultima linie şi suma maximă a
numerelor de pe un drum de la T[i+1][j+1] la ultima linie.
• Pentru a reţine soluţiile subproblemelor, vom utiliza o matrice suplimentară S,
pătratică de ordin n, cu semnificaţia
S[i][j]= suma maximă ce se poate obţine pe un drum de la T[i][j] la
un element de pe ultima linie, respectand condiţiile problemei.
Evident, soluţia problemei va fi S[1][1].
• Relaţia de recurenţă care caracterizează substructura optimală a problemei este:

64
10 METODA PROGRAMĂRII DINAMICE

S[n][i]=T[n][i], i{1,2,...,n}
S[i][j]=T[i][j]+max{S[i+1][j], S[i+1][j+1]}
4. Rezolvăm relaţia de recurenţă î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]);
}
Exerciţiu
Afişaţi şi drumul în triunghi pentru care se obţine soluţia optimă.

4. Subşir comun maximal


Fie X=(x1, x2, ..., xn) şi 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) şi 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
2. Notăm cu Xk=(x1, x2, ..., xk) (prefixul lui X de lungime k) şi 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
Dacă Xk=Yh atunci LCS(Xk,Yh)=1+LCS(Xk-1,Yh-1).
Dacă XkYh atunci LCS(Xk,Yh)=max(LCS(Xk-1,Yh), LCS(Xk,Yh-1)).
Din observaţia precedentă deducem că subproblemele problemei date nu sunt
independente şi că problema are substructură optimală.
3. Pentru a reţine soluţiile subproblemelor vom utiliza o matrice cu n+1 linii şi m+1
coloane, denumită 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 şi Yh.
4. Vom caracteriza substructura optimală a problemei prin următoarea relaţie de
recurenţă:
65
INFORMATICA 11

lcs[k][0]=lcs[0][h]=0, k{1,2,..,n}, h{1,2,..,m}


lcs[k][h]=1+lcs[k-1][h-1], dacă x[k]=y[h]
max{lcs[k][h-1], lcs[k-1][h]}, dacă x[k]y[h]
4. 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];
Deoarece nu am utilizat o structură de date suplimentară cu ajutorul căreia să
memorăm soluţia opţimă, vom reconstitui soluţia optimă pe baza rezultatelor
memorate în matricea lcs. Prin reconstituire vom obţine soluţia în ordine inversă,
din acest motiv vom memora soluţia într-un vector, pe care îl vom afişa de la
sfârşit către început:
cout<<"Lungimea subsirului comun maximal: "
<<lcs[n][m]; int d[100];
cout<<"\nCel mai lung subsir comun este:
\n"; for (int i=0, k=n, h=m; lcs[k][h]; )
if (x[k]==y[h])
{d[i++]=x[k];k--; h--
;} else
if (lcs[k][h]==lcs[k-1][h])
k--;
else
h--;
for (k=i-1;k>=0; k--)cout<<d[k]<<' ';

5. Stivă de jetoane
Un joc este constituit dintr-o stivă de n (n1000) jetoane, de două culori.
Jetoanele sunt numerotate de la 1 la n, jetonul cu numărul 1 fiind cel de la vârful
stivei. Cei doi jucători (să-i numim Ana şi Barbu) mută alternativ. La o mutare,
un jucător poate lua din stivă oricâte jetoane (cel puţin unul), cu condiţia ca toate
jetoanele luate să fie de aceeaşi culoare. Câştigă jucătorul care ia ultimul jeton. Să
presupunem că întotdeauna Ana face prima mutare.
Scrieţi un program care să determine dacă Ana are strategie sigură de câştig şi
dacă da, să afişeze pe ecran mutările Anei (câte jetoane ia din stivă atunci când îi
vine rândul). Programul va citi de la tastatură mutările lui Barbu.

66
12 METODA PROGRAMĂRII DINAMICE

Soluţie
Vom reţine culorile jetoanelor într-un vector c[]. Iniţializarea configuraţiei de
joc constă din citirea numărului de jetoane şi generarea aleatoare a culorilor
acestora, codificând prima culoare cu 0 şi cea de a doua culoare cu 1.
#define NMax 1002 int
n, c[NMax]; void
Init() {cout<<"n=";
cin>>n;
randomize();
for (int i=1; i<=n; i++) c[i]=random(2);
cout<<"Stiva de jetoane este ";
for (i=1; i<=n; i++)
cout<<c[i]; cout<<endl; }
5. Subproblemele problemei date constau în determinarea existenţei unei strategii
sigure de câştig pentru jucătorul care face prima mutare pentru o stivă
constituită din jetoanele i..n.
6. Pentru a reţine soluţiile subproblemelor, vom utiliza un vector S[], având
următoarea semnificaţie: S[i]=1, dacă jucătorul care extrage jetonul i are
strategie sigură de câştig şi 0 altfel. Evident, dacă S[1]=1, deducem că Ana
are strategie sigură de câştig.
7. Caracterizăm substructura optimale a soluţiei prin următoarea relaţie de
recurenţă:
S[n]=1;
Dacă c[i]c[i+1], atunci S[i]=1-S[i+1].
Dacă c[i]=c[i+1], atunci S[i]=1 (în cazul în care S[i+1]=1, jucătorul
va lua şi jetonul i pe lângă jetoanele care îî asigură câştigul pentru i+1..n;
dacă S[i+1]=0, atunci jucătorul va lua numai jetonul i).
8. Rezolvăm această relaţie de recurenţă în mod bottom-up:
int S[NMax];
void Dinamic()
{S[n]=1;
for (int i=n-1; i>0; i--)
if (c[i+1]!=c[i]) S[i]=1-S[i+1];
else S[i]=1; }
În cazul în care Ana are strategie sigură de câştig, pentru a vizualiza şi mutările
care conduc la câştigarea jocului, utilizăm informaţiile deja memorate în S[]:
if (!S[1])
{cout<<"Ana nu are strategie sigura de
castig!"; return;}
cout<<"Ana are strategie sigura de castig! Sa
jucam!\n"; for (int i=1; i<=n; )
{for (int k=i; k<=n && S[k]; k++);

67
INFORMATICA 13

//Ana ia jetoanele de la i la k
cout<<"Ana ia: "<<k-i<<"
jetoane\n"; if (k>n)
{cout<<"Ana a castigat!\n";
return;} //citim mutarea lui Barbu
cout<<"Stiva ";
for (int j=k; j<=n; j++) cout<<c[j];
cout<<"\nBarbu muta: "; cin>>nr;
//validez mutarea lui Barbu
for (j=k+1; j<k+nr; j++)
if (c[j]!=c[j-1])
{cout<<"Mutare gresita! Barbu
pierde!\n"; return;}
i=k+nr; }

6. Laser
Se consideră o placă dreptunghiulară cu dimensiunile mn. Această placă
trebuie tăiată în mn bucăţi mai mici, fiecare bucată fiind un pătrat cu dimensiunile
11. Întrucât placa este neomogenă, pentru fiecare bucată se indică densitatea
dxy, unde x, y sunt coordonatele colţului stânga-jos al pătratului respectiv.

Pentru operaţiile de tăiere se foloseşte un strung cu laser. Fiecare operaţie de


tăiere include:
– fixarea unei plăci pe masa de tăiere;
– stabilirea puterii laserului în funcţie de densitatea materialului de tăiat;
– o singură deplasare a laserului de-a lungul oricărei drepte paralele cu una din
axele de coordonate;
– scoaterea celor două plăci de pe masa de tăiere.
Costul unei operaţii de tăiere se determină după formula c=dmax, unde dmax este
densitatea maximă a bucăţilor 11 peste marginile cărora trece raza laserului.
Evident, costul total T poate fi determinat adunând costurile individuale c ale
tuturor operaţiilor de tăiere necesare pentru obţinerea bucăţilor 11.
Scrieţi un program care calculează costul minim T.
(Olimpiada Republicană de Informatică, Republica Moldova, 2001)
Date de intrare
Fişierul text LASER.IN conţine pe prima linie numerele m şi n separate prin
spaţiu. Următoarele m linii conţin câte n numere dxy separate prin spaţiu.

68
14 METODA PROGRAMĂRII DINAMICE

Date de ieşire
Fişierul text LASER.OUT conţine pe o singură linie numărul natural T.

Exemplu
LASER.IN LASER.OUT
3 5 52
1 1 1 1 5
1 7 1 1 1
1 1 1 6 1
Restricţii
  m,nN, 2m,n20
 
dxyN, 1dxy100

Soluţie
2. Subproblemele problemei date constau în tăierea unui dreptunghi din placa
dată, care are colţul stânga-sus în poziţia (i, j), are lungimea l şi înălţimea h.
3. Pentru a reţine soluţiile subproblemelor trebuie să utilizăm un tablou cu 4
dimensiuni Cnxmxnxm, cu semnificaţia C[i,j,h,l]=costul total de tăiere a unui
unui dreptunghi din placa dată, care are colţul stânga-sus în poziţia (i, j), are
lungimea l şi înălţimea h. Evident, soluţia problemei date va fi C[1,1,n,m].
Deoarece nu avem spaţiu suficient, vom aloca parţial tabloul c în mod dinamic,
şi îl vom iniţializa cu 0:
int i, j, k, l, h; for
(i=0; i<=n; i++)
for (j=0; j<=m; j++)
for (h=0; h<=n; h++)
{c[i][j][h]=new int[MaxD];
for (l=0; l<=m; l++) c[i][j][h][l]=0;}
3. Pentru a tăia un dreptunghi din placa dată, care are colţul stânga-sus în poziţia
(i, j), are lungimea l şi înălţimea h trebuie să executăm o primă tăietură
completă. Aceasta poate fi trasată fie orizontal (între liniile k-1 şi k, unde k
variază de la i+1 până la i+h-1), fie vertical, între coloanele k-1 şi k, unde k
variază de la j+1 la j+l-1). Vom alege succesiv poziţia de tăiere k în toate
modurile posibile şi vom reţine varianta care asigură optimul.

69
INFORMATICA 15

j j+1  j+l-1
i j j+1  k-1 k  j+l-1
i+1 i

... i+1

k-1 ...

k i+h-1

... Tăietură verticală


i+h-1
Tăietură orizontală

Prin urmare putem caracteriza substructura optimală a problemei prin


următoarea relaţie de recurenţă:
C[i][j][1][1]=0; i{1,2,..,n}, j{1,2,..,m}
C[i][j][h][l]=min {min{c[i][j][k-i][l]+c[k][j][h-
k+i][l]+dmaxL(k,j,l)}//orizontal
k=i+1,i+h-1
min{c[i][j][h][k-j]+c[i][k][h][l-k+j]+dmaxC(k,i,h)}} //vertical
k=j+1,j+l-1
În relaţiile de recurenţă precedente am utilizat valorile:
– dmaxL(k,j,l), reprezentând densitatea maximă a bucăţilor 11 peste
marginile cărora trece raza laserului, la o tăietură între liniile k-1 şi k, de
lungime l, începând de la coloana j;
– dmaxC(k,i,h), reprezentând densitatea maximă a bucăţilor 11 peste
marginile cărora trece raza laserului, la o tăietură între coloanele k-1 şi k,
de lungime h, începând cu linia i.
• Calculul soluţiei optime în mod bottom-up:
for (h=1; h<=n; h++)
for (l=1; l<=m; l++)
for (i=1; i<=n-h+1; i++)
for (j=1; j<=m-l+1; j++)
if (l>1 || h>1) {//determin
c[i][j][h][l]
c[i][j][h][l]=infinit;
//taieturi orizontale
for (k=i+1; k<i+h; k++)
{a=c[i][j][k-i][l]+c[k][j][h-k+i][l]+dmaxL(k,j,l);
if (c[i][j][h][l]>a) c[i][j][h][l]=a;}
//taieturi verticale for
(k=j+1; k<j+l; k++)
{a=c[i][j][h][k-j]+c[i][k][h][l-k+j]+dmaxC(k,i,h);
if (c[i][j][h][l]>a) c[i][j][h][l]=a;}
}

70
16 METODA PROGRAMĂRII DINAMICE

Observaţie
O optimizare posibilă a algoritmului constă în evitarea apelurilor funcţiilor
dmaxL() şi dmaxC(), prin precalcularea valorilor respective o singură dată şi
memorarea lor în câte un tablou tridimensional. Implementaţi această optimizare!

7. Problema patronului
Un patron de gogoşerie a cumpărat un calculator şi doreşte să înveţe să lucreze
pe el. Pentru aceasta va umple un raft de cărţi din colecţia „Informatica în lecţii de
9 minute şi 60 secunde”. Raftul are lungimea L cm. Seria dispune de n titluri,
numerotate 1, 2,..., n având respectiv grosimile de g1, g2,..., gn cm.
Scrieţi un program care să selecteze titlurile pe care le va cumpăra patronul,
astfel încât raftul să fie umplut complet (suma grosimilor cărţilor cumpărate să fie
egală cu lungimea raftului) şi numărul cărţilor achiziţionate să fie maxim. Progra-
mul va afişa numărul cărţilor selectate, precum şi grosimile acestora. Dacă nu
există soluţie, va fi afişat mesajul IMPOSIBIL. (ONI Suceava 1996, XI)
Restricţii
  
nN, 1n60
 
 LN,1L200
  
giN*, gi30000, i{1,2,...,n}
 
Timp maxim de execuţie: 1 secundă/test.

 Exemplu
Soluţie
1. Problema constă în selectarea unor elemente a căror sumă să fie egală cu L. În
cazul în care există mai multe soluţii, este preferată soluţia cu număr maxim de
elemente. O subproblemă a acestei probleme constă în selectarea unor elemente
a căror sumă să fie egală cu S, SL.
2. Pentru a reţine soluţiile subproblemelor vom utiliza un vector Nr, cu L+1
componente, având semnificaţia: Nr[S]=numărul maxim de titluri ce pot fi
cumpărate pentru a umple un raft de lungime S (0SL) sau -1 (dacă nu putem
umple un raft de lungime S). Pentru a reţine şi cărţile cumpărate, vom utiliza o
matrice Uz cu L+1 linii şi n coloane, având semnificaţia L[S][i] este 1, dacă
am utilizat titlul i pentru a umple optimal raftul de lungime S şi 0, în caz
contrar.
#define NMax 61
#define LMax 201
int n, L; //numarul de titluri si lungimea raftului
int g[NMax]; //grosimile
int Nr[LMax]; //numarul maxim de carti selectate
int Uz[LMax][NMax]; //indicii titlurilor utilizate

71
INFORMATICA 17

3. Să considerăm că ultimul titlu selectat pentru a umple un raft de lungime S


este i. Prin urmare, în rest sunt selectate (obligatoriu în mod optim) cărţi a
căror grosime totală trebuie să fie egală cu S-g[i]. Evident, pentru a
putea alege titlul i, condiţia este ca acest titlu să nu fie deja utilizat pentru
a umplerea
restului raftului (de lungime S-g[i]). Alegerea titlului i trebuie să fie făcută
de asemenea în mod optim (să maximizeze numărul total de cărţi selectate):
Nr[0]=0;
Nr[S]=max{-1, max{1+Nr[S-g[i]]|S-g[i]0 şi Nr[S-g[i]]-1
şi i=1,n Uz[S-g[i]][i]=0}

4. Rezolvarea relaţiei de recurenţă:


int S, k, i;
for (S=1; S<=L; S++)
for (Nr[S]=-1, i=1; i<=n; i++)
if (g[i]<=S && Nr[S-g[i]]!=-1 && !Uz[S-g[i]][i] )
if (Nr[S]<1+Nr[S-g[i]])
{Nr[S]=1+Nr[S-g[i]];
for (k=1;k<=n;k++) Uz[S][k]=Uz[S-
g[i]][k]; Uz[S][i]=1;}
Pentru a afişa soluţia, utilizăm informaţiile memorate în tabloul Uz:
if (Nr[L]==-1) cout<<"IMPOSIBIL";
else
{cout<<"Numarul maxim de carti este:
"<<Nr[L]<<endl; for (int k=1; k<=n; k++)
if (Uz[L][k]) cout <<g[k]<<" ";}

8. Telecomanda
Cu ocazia olimpiadei naţionale de informatică, afişaj
televiziunea locală organizează un nou joc în 0 1 2 3 4
direct. Organizatorii utilizează un calculator, care 5 6 7 8 9
generează şi afişează pe un monitor două numere + - * / # =
de maxim 100 de cifre fiecare (X şi Y).

Fiecare concurent dispune de o telecomandă prevăzută cu un afişaj de o cifră şi


cu anumite taste, ca în figura alăturată. Telecomanda are şi o memorie, în care sunt
reţinute în ordine cifrele obţinute de concurenţi.
Cifrele primului număr (X) sunt afişate succesiv pe afişajul telecomenzii
fiecărui concurent, în ordine de la stânga la dreapta. Concurenţii trebuie să
transforme primul număr, obţinând în memoria telecomenzii proprii pe cel de al
doilea, utilizând tastele pe care le au la dispoziţie pe telecomandă. După efectuarea
unei operaţii asupra cifrei curente (cea de pe afişaj), pe afişaj apare automat urmă-
toarea cifră din X (dacă mai există). Efectele apăsării tastelor sunt următoarele:

72
18 METODA PROGRAMĂRII DINAMICE

Taste acţionate Efect


+ urmat de o cifră Se generează suma dintre cifra de pe afişaj şi cifra tastată (operaţie
posibilă doar dacă suma este tot o cifră). Cifra sumă este reţinută
în memorie.
– urmat de o cifră Se generează diferenţa dintre cifra de pe afişaj şi cifra tastată
(operaţie posibilă doar dacă se obţine tot o cifră). Cifra obţinută
este reţinută în memorie.
* urmat de o cifră Se reţine în memorie valoarea tastei care se acţionează după tasta
*. Deoarece asupra cifrei curente din X nu se efectuează nici o
operaţie, aceasta nu dispare de pe afişaj.
/ Se şterge cifra curentă din X
# Se şterg din X cifra curentă şi toate cifrele care urmează, până la
sfârşit.
= Se copiază în memorie cifra curentă.
Acţiunea se încheie atunci când toate cifrele lui X au fost prelucrate. Am obţinut
o soluţie când în memoria telecomenzii se află cifrele numărului Y. O soluţie este
optimă dacă numărul de taste acţionate este minim. Câştigătorii jocului sunt acei
concurenţi care descoperă o soluţie optimă.
Date fiind X şi Y, scrieţi un program care să determine o soluţie optimă de
transformare a numărului X în numărul Y. (ONI, Bacău 2001, XI-XII)

Date de intrare
Fişier de intrare TELE.IN conţine două linii:
X
Y
Date de ieşire
Fişier de ieşire TELE.OUT conţine două linii:
min
t1t2...tmin
unde:
– min este un număr natural nenul, reprezentând numărul minim de taste
acţionate pentru transformarea lui X în Y.
– t1t2...tmin este o succesiune de min caractere, care reprezintă tastele
acţionate; între caractere nu se vor pune separatori.
Exemplu
TELE.IN TELE.OUT
372 4
78 /=+6
Timp maxim de execuţie/test: 1 secundă

73
INFORMATICA 19

Soluţie
1.Să notăm cu m numărul de cifre din X şi cu n numărul de
cifre din Y. De asemenea vom nota Xi sufixul care începe
cu cifra i din X (secvenţa de cifre XiXi+1...Xm) şi cu
Yj sufixul care începe cu cifra j din Y (secvenţa de
cifre YjYj+1...Yn). Subproblemele problemei date
constau în determinarea transformării de cost minim a lui
Xi în Yj, i{1,2,...,m} şi j{1,2,...,n}.
2.Pentru a reţine soluţiile subproblemelor vom utiliza două
matrice cm şi o, fiecare cu m linii şi n coloane, având
următoarea semnificaţie:
– cm[i][j] = numărul minim de acţionări de taste necesar pentru a
transforma Xi în Yj.
– o[i][j] = prima operaţie ce va fi executată pentru a transforma optimal Xi
în Yj.
typedef char Operatie[3];
char x[DimMax], y[DimMax];
int cm[DimMax][DimMax];
Operatie o[DimMax][DimMax];
int n, m;
Evident, cm[1][1] reprezintă numărul minim de acţionări de taste necesar
pentru a transforma numărul X în numărul Y.
3. Relaţia de recurenţă carecaracterizează substructura optimală a problemei este:
cm[i][j]=min
{1+cm[i+1][j], //operatia /
2+cm[i][j+1], //operatia *Y[j]
2+cm[i+1][j+1], //operatia - X[i]-Y[j], daca X[i]>Y[j]
2+cm[i+1][j+1], //operatia + Y[j]-X[i], daca X[i]<Y[j]
1+cm[i+1][j+1], //operatia =
1+cm[m+1,j]} //operatia #
4. Secvenţa de iniţializare:
void Init()
{ m=strlen(x); n=strlen(y);
for (int i=1; i<=m; i++)
{cm[i][n+1]=1; strcpy(o[i][n+1],"#");
} for (i=1; i<=n; i++)
{cm[m+1][i]= 2*(n-i+1);
for (int j=i; j<=n; j++)
{o[m+1][j][0]='*';
o[m+1][j][1]=y[j];o[m+1][j][2]=NULL;}
}

74
}
Rezolvăm relaţia de recurenţă în mod bottom-up:

75
20 METODA PROGRAMĂRII DINAMICE

Operatie omin;
int min;
for (int j=n; j>0; j--)
for (int i=m; i>0; i--) {min=1+cm[i+1][j];
strcpy(omin,"/");
if (min>2+cm[i][j+1])
{min=2+cm[i][j+1]
;
omin[0]='*'; omin[1]=y[j];
omin[2]=NULL;} if (x[i]>y[j])
{if (min>2+cm[i+1][j+1])
{min=2+cm[i+1][j+1];
omin[0]='-'; omin[1]=x[i]-
y[j]+'0'; omin[2]=NULL;}
}
else
if (x[i]<y[j])
{if (min>2+cm[i+1][j+1])
{min=2+cm[i+1][j+1]
;
omin[0]='+'; omin[1]=y[j]-
x[i]+'0'; omin[2]=NULL;}
}
else
{if (min>1+cm[i+1][j+1]) {min=1+cm[i+1][j+1];
strcpy(omin,"=");}
}
if (min>1+cm[m+1][j])
{min=1+cm[m+1][j];
strcpy(omin,"#");} cm[i][j]=min;
strcpy(o[i][j],omin);
}

O soluţie optimă se obţine utilizând matricea o, a operaţiilor efectuate la fiecare


pas.
ofstream f("tele.out");
f<<cm[1][1]<<endl;
for (int i=1,j=1; i<=m+1 && j<=n+1 && !(i==m+1 && j==n+1);)
{f<<o[i][j];
if (o[i][j][0]=='+'||o[i][j][0]=='-'||o[i][j][0]=='=')
{i++; j++; }
else
if (o[i][j][0] =='*') j++;
else
if (o[i][j][0] =='/') i++;
else i=m+1;
}

76
f.close();

77
INFORMATICA 21

9. Codificare optimală
Fie un text de lungime maximă 100, ce conţine doar litere. Textul poate fi
codificat, înlocuind apariţiile consecutive ale subşirurilor sale cu subşirul urmat de
numărul său de apariţii.

Exemplu
Textul T="aaacaaacbbdefdef" poate fi codificat: "a3c1a3c1b2def2"
dar şi "aaac2b2def2". Evident, cel de al doilea mod de codificare este mai
scurt, deci mai convenabil.
Scrieţi un program care să codifice optimal un text dat (codul rezultat să fie de
lungime minimă).

Soluţie
1. Spaţiul subproblemelor problemei iniţiale este format din determinarea unei
codificări optimale pentru caracterele din text de la i până la j (T[i..j]),
0ij<n, unde n este lungimea textului. Evident, subproblemele se suprapun.
De exemplu, determinarea codificării optime pentru T[i..j], necesită
determinarea unor codificări optime pentru T[i..k] şi pentru T[k+1..j],
k{0,1,..., j-1}.
2. Pentru a memora soluţiile subproblemelor, vom utiliza o matrice l, pătratică de
ordin n, având semnificaţia l[i][j] = lungimea codificarii optime pentru
T[i..j]; 0ij<n. Observaţi că este utilizată numai jumătatea de deasupra
diagonalei principale.
3. Problema are substructură optimală, caracterizată de următoarea relaţie de
recurenţă:
Orice caracter x se codifică pe două poziţii (x1), deci:
l[i][i]=2, i{0,1,..., n-1}
l[i][j]=min {j-i+2, minI,
minII} unde:
 
 j-i+2 provine din codificarea întregului şir, urmat de 1
 
minI=min{l[i][k]+l[k+1][j]}
k=i,j-1

Codificăm optimal T[i..j] concatenând codificările optimale ale şirurilor


T[i..k] şi T[k+1..j] (poziţia k{i, i+1,...,j-1} se alege în mod
optimal)

minII=min{strlen(s)+NrCifre(k)}, unde s subşir al lui
T[i..j] astfelîncât T[i..j]=ss..s (de k ori). Deci T[i..j]
se codifică sk.

78
22 METODA PROGRAMĂRII DINAMICE

Deoarece jumătatea de sub diagonala principală din matricea l este neutilizată,


pentru a putea reconstitui soluţia optimă, o vom utiliza astfel:
  l[j][i]=0, dacă l[i][j]=j-i+2

  l[j][i]=k, unde k este poziţia de splitare a textului pentru l[i][j]=minI


  l[j][i]=-k, unde k este lungimea subşirului s care se repetă, pentru
 T[i][j]=minII.
4. Rezolvarea relaţiei de recurenţă:
for (int i=0; i<n; i++) l[i][i]=2;
for (int d=2; d<=n; d++) //codific subsiruri de lungime d
for (i=0; i<=n-d; i++) //care incep la pozitia i
{int j=i+d-1; //si se termina la pozitia j
//cazul I- codificarea este intregul sir urmat de 1
l[i][j]=d+1; l[j][i]=0;
//cazul II
for (int k=i; k<j; k++)
if (l[i][j]>l[i][k]+l[k+1][j])
{l[i][j]=l[i][k]+l[k+1][j];
l[j][i]=k;}// pozitia optima de splitare
//cazul III
for (k=1; k<=d/2; k++)
if (d%k==0) //determin un subsir de lungime k
if (SeRepeta(i,j,k))
{l[i][j]=k+NrCifre(d/k);
l[j][i]=-k; //retin lungimea
subsirului break;}
}
Observaţi că am utilizat funcţia NrCifre() care determină numărul de cifre
dintr-un număr natural, precum şi funcţia SeRepeta():
int SeRepeta (int i, int j, int k)
{/*verifica daca sirul T[i..j] este format prin
concatenarea succesiva a lui T[i..i+k-1] */
char s[DimMax], s1[DimMax];
strncpy(s,T+i,k); s[k]=NULL; s1[k]=NULL;
for (int h=i+k; h<=j; h+=k)
{strncpy(s1,T+h, k);
if (strcmp(s,s1)) return 0;}
return 1;}
Afişarea soluţiei optime o vom realiza prin apelarea funcţiei recursive
Afisare(0,n-1):
ofstream f("cod.out");
void Afisare(int i, int j)
{
if (i==j) cout<<T[i]<<1; //un singur caracter
else

79
INFORMATICA 23

{int k=l[j][i];
char s[DimMax];
if (!k) // cazul I
{strncpy(s,T+i,j-i+1); s[j-i+1]=NULL;
cout<<s<<1; }
else
if (k>0) //cazul II
{Afisare (i, k);
Afisare (k+1,j);}
else //cazul III
{strncpy(s,T+i,-k); s[-k]=NULL;
cout<<s<<(j-i+1)/(-k);}
}
}

10. Florărie
Presupunem că aveţi o florărie şi că doriţi să aranjaţi vitrina într-un mod cât mai
plăcut. Aveţi F buchete de flori de feluri diferite şi cel puţin tot atâtea vase,
aranjate pe un raft. Vazele sunt lipite de raft, şi sunt numerotate în ordine, de la
stânga la dreapta, de la 1 la V, unde V este numărul de vaze. Mai exact vaza 1 este
cea mai din stânga, iar vaza V cea mai din dreapta. Buchetele sunt şi ele numerotate
distinct, cu numere între 1 şi F. Aceste numere de identificare a buchetelor au o
semnificaţie: ele determină ordinea de apariţie cerută pentru buchete în vaze. Mai
exact, buchetul i trebuie să fie într-o vază din stânga vazei care conţine buchetul
j, dacă i<j.
Să presupunem, de exemplu, că aveţi un buchet de azalee (cu numărul de
identificare 1) un buchet de begonii (cu numărul de identificare 2) şi un buchet de
trandafiri (cu numărul de identificare 3). Acum, toate buchetele trebuie puse în
vaze, astfel încât ordinea numerelor lor de identificare să se păstreze. Buchetul de
azalee trebuie să fie într-o vază din stânga begoniilor, care trebuie să fie într-o vază
din stânga trandafirilor. Dacă există mai multe vaze decât buchete, cele în plus vor
rămâne goale. O vază poate conţine un singur buchet de flori.
Fiecare vază are o caracteristică proprie (asemănător florilor). Prin urmare,
aşezănd un buchet de flori într-o anumită vază, obţineţi un anumit efect estetic,
exprimat printr-o valoare întreagă. Valorile estetice sunt reprezentate sub forma
unui tablou, ca în exemplul de mai jos. Dacă o vază rămâne goală, valoarea estetică
este 0.
V A Z E
1 2 3 4 5
1 (azalee) 7 23 -5 -24 16
Buche

2 (begonii) 5 21 -4 10 23
e

80
24 METODA PROGRAMĂRII DINAMICE

3 (trandafiri) -21 5 -4 -20 20


Conform acestui tabel, azaleele, de exemplu, vor arăta grozav în vaza 2 şi
groaznic în vaza 4.
Pentru a obţine cel mai plăcut efect, trebuie să maximizaţi suma valorilor
estetice ale vazelor, păstrând ordinea buchetelor. Dacă există mai multe soluţii, se
cere numai una. (IOI Turcia 1999)

Restricţii
 
1≤F≤100 unde F este numărul de buchete de flori.
 
F≤V≤100 unde V este numărul de vaze.
 
-50Aij50 unde Aij este valoarea estetică care se obţine punând buchetul i
în vaza j.
 
Timp maxim de execuţie 2 secunde / test.

Date de intrare
Fişierul de intrare se numeşte flower.in. Prima linie conţine două numere: F
V. Fiecare din următoarele F linii conţine câte V numere întregi, astfel încât
Aij este al j-lea număr de pe linia (i+1) din fişierul de intrare.

Date de ieşire
Fişierul de ieşire flower.out conţine două linii. Prima linie conţine suma
valorilor estetice a aranjamentului floral. A doua linie reprezintă aranjamentul
floral, ca o listă de F numere, astfel încât cel de al k-lea număr de pe linie
identifică vaza în care este pus buchetul k.

Exemplu
flower.in flower.out
3 5 53
7 23 –5 –24 16 2 4 5
5 21 -4 10 23
-21 5 -4 -20 20
Soluţie
1. Subproblemele problemei date constau în amplasarea optimă (cu maximizarea
sumei valorilor estetice) a buchetelor 1..i în vazele 1..j, respectând ordinea
buchetelor (i{1,2,...,F}, j{i,...,V-F+i}).
2. Pentru a reţine soluţiile subproblemelor vom utiliza o matrice c cu F linii şi V
coloane (c[i][j]=valoarea estetică optimă care se poate obţine amplasând
buchetele 1..i în vazele 1..j, respectând ordinea buchetelor. Evident,
valoarea estetică optimă se obţine în c[F][V]. Pentru reconstituirea soluţiei
optime, vom utiliza o matrice poz cu F linii şi V coloane (poz[i][j]= vaza

81
INFORMATICA 25

în care plasăm buchetul i, într-o amplasare estetică optimă a buchetelor 1..i


în vazele 1..j).
3. Pentru a amplasa optim buchetele 1..i în vazele 1..j, respectând ordinea
buchetelor (i{1,2,...,F}, j{i,...,V-F+i}) avem două posibilităţi
(dintre care o alegem pe cea mai convenabilă): fie amplasăm buchetul i în vaza
j şi amplasăm optimal buchetele 1..i-1 în vazele 1..j-1, fie amplasăm
buchetele 1..i în vazele 1..j-1. Prin urmare, caracterizăm substructura
optimală a problemei prin următoarea relaţie de recurenţă:
c[i][i-1]=-;, i{1,2,...,F}
c[0][j]=0; j{0,...,V}
c[i][j]=max{A[i][j]+c[i-1][j-1], c[i][j-1]}, i{1,2,...,F},
j{i,...,V-F+i}
4. Pentru implementare observăm putem iniţializa c[i][j] cu valoarea Aij,
astfel încât să nu mai fie necesar să utilizăm o matrice suplimentară A.
Rezolvăm relaţia de recurenţă în mod bottom-up astfel:
int i, j;
for (i=1; i <= F; i++) c[i][i-
1]=MINF; for (i=1; i <= F; i++)
for (j=i; j <= (V - F + i); j++)
if (c[i-1][j-1]+c[i][j]>c[i][j-1])
{c[i][j] += c[i-1][j-1];
poz[i][j]=j;
} else
{c[i][j]=c[i][j-1];
poz[i][j]=poz[i][j-1];}
Afişarea soluţiei o vom face utilizând valorile memorate în matricea poz:
ofstream f("flower.out");
f<<c[F][V]<<endl;
int sol[MaxNr];
for (int i=F, j=V; i>0; j--)
if (poz[i][j]==j)
{sol[i]=j; i--;}
for (i=1; i<=F; i++) f<<sol[i]<<'
'; f.close();

11. Vin bun


La un depozit specializat din Recaş sosesc, pe rând, n clienţi care solicită
fiecare o cantitate dată de Cabernet. În butoiul din magazinul de deservire
(considerat de capacitate nelimitată) nu se găseşte iniţial nici o picătură de vin. Dan
Şeptică, administratorul depozitului, are un algoritm propriu de deservire a
clienţilor: în funcţie de ceea ce are în butoi, dar şi de inspiraţia de moment, el poate

82
26 METODA PROGRAMĂRII DINAMICE

să răspundă: „Vă ofer întreaga cantitate cu cea mai mare plăcere!” sau „Nu vă pot
oferi acum ceea ce doriţi, reveniţi cu solicitarea altădată!”.
Pe clientul servit îl eliberează de grija banilor corespunzători costului licorii
cumpărate, iar pe cel refuzat îl salută politicos şi are grijă ca , imediat ce a plecat
clientul, să coboare şi să aducă în butoiul din magazin exact cantitatea solicitată de
clientul respectiv (cel ce nu a fost servit) .
Clienţii sunt restaurante de lux şi nu cumpăra mai mult de 1 hectolitru.
Cunoscând cele n cantităţi cerute de clienţi, să se determine un mod de a
răspunde solicitărilor astfel încât, în final, cantitatea de vin vânduta să fie maximă.

Date de intrare
Din fişierul de intrare CERERI.IN se citesc:
n – numărul de clienţi (n600);
c1 c2 ... cn – cantităţile (în litri) cerute de clienţi, în ordinea sosirii lor.

Date de ieşire
În fişierul VANZARI.OUT se va scrie cantitatea vândută (tot în litri).

Exemplu
CERERI.IN VANZARI.OUT
4 6
5 3 4 2
Interpretare
Acţiunile lui Septică sunt: refuză primul client şi pune 5 litri în butoi, îl refuză
pe al doilea şi mai pune încă 3 litri în butoi, iar pe următorii doi clienţi îi serveşte ,
realizând o vânzare de 6 litri. (ONI Timişoara 1997, X)

Soluţie
Subproblemele problemei date constau în determinarea cantităţii maxime de vin
ce poate fi vândută luând în considerare cererile clienţilor i..n, în ipoteza că
iniţial avem în butoi o cantitate j. Soluţiile acestor subprobleme le vom reţine într-
o matrice v.
Relaţia de recurenţă o obţinem astfel:
v[i][0]=0, i{1,2,...,n}
v[i][j]= max{v[i+1][j+c[i]] (cazul în care clientul i este
refuzat) c[i]+v[i+1][j-c[i]], dacă j≥c[i]
(cazul în care clientul i este servit)}
Soluţia problemei iniţiale o vom obţine în v[1][0].

83
INFORMATICA 27
Probleme propuse.
1. Să se găsească cel mai lung subşir comun pentru două şiruri.
2. Să se găsească cel mai lung subşir ordonat, comun pentru două şiruri.
3. Să se găsească cea mai lungă secvenţă comună pentru două şiruri.
4. Să se găsească cea mai lungă secvenţă ordonată, comună pentru două şiruri.
5. Se citesc n numere naturale. Se cere să se tipărească cea mai mare sumă care se
poate forma utilizân-d cele n numere naturale (fiecare nr participă o singură dată în
calculul sumei) şi care se divide cu n, precum şi numerele care alcătuiesc această
sumă.
6 Problema rucsacului

8.5 Determinarea celor mai scurte drumuri intr-un graf


Fie G = <V, M> un graf orientat, unde V este multimea varfurilor si M este multimea
muchiilor. Fiecarei muchii i se asociaza o lungime nenegativa. Dorim sa calculam
lungimea celui mai scurt drum intre fiecare pereche de varfuri.

Vom presupune ca varfurile sunt numerotate de la 1 la n si ca matricea L da lungimea


fiecarei muchii: L[i, i] = 0, L[i, j]  0 pentru i  j, L[i, j] =  daca muchia (i, j) nu exista.

Principiul optimalitatii este valabil: daca cel mai scurt drum de la i la j trece prin varful k,
atunci portiunea de drum de la i la k, cat si cea de la k la j, trebuie sa fie, de asemenea,
optime.

Construim o matrice D care sa contina lungimea celui mai scurt drum intre fiecare pereche
de varfuri. Algoritmul de programare dinamica initializeaza pe D cu L. Apoi,
84
efectueaza n iteratii. Dupa iteratia k, Dva contine lungimile celor mai scurte drumuri care
folosesc ca varfuri intermediare doar varfurile din {1, 2, ..., k}. Dupa n iteratii, obtinem
rezultatul final. La iteratia k, algoritmul trebuie sa verifice, pentru fiecare pereche de
varfuri (i, j), daca exista sau nu un drum, trecand prin varful k, care este mai bun decat
actualul drum optim ce trece doar prin varfurile din {1, 2, ..., k1}.
Fie Dk matricea D dupa iteratia k. Verificarea necesara este atunci:

Dk[i, j] = min(Dk-1[i, j], Dk-1[i, k] Dk-1[k, j])

unde am facut uz de principiul optimalitatii pentru a calcula lungimea celui mai scurt
drum via k. Implicit, am considerat ca un drum optim care trece prin k nu poate trece de
doua ori prin k.

Acest algoritm simplu este datorat lui Floyd (1962):

function Floyd(L[1 .. n, 1 .. n])


array D[1 .. n, 1 .. n]
DL
for k  1 to n do
for i  1 to n do
for j  1 to n do
D[i, j]  min(D[i, j], D[i, k]D[k, j])
return D

De exemplu, daca avem

85
obtinem succesiv

Puteti deduce ca algoritmul lui Floyd necesita un timp in (n3). Un alt mod de a rezolva
aceasta problema este sa aplicam algoritmul Dijkstra (Capitolul 6) de n ori, alegand mereu
un alt varf sursa. Se obtine un timp in n (n2), adica tot in (n3). Algoritmul lui Floyd,
datorita simplitatii lui, are insa constanta multiplicativa mai mica, fiind probabil mai rapid
in practica. Daca folosim algoritmul Dijkstra-modificat in mod similar, obtinem un timp
total in O(max(mn, n2) log n), unde m = #M. Daca graful este rar, atunci este preferabil sa
aplicam algoritmul Dijkstra-modificat de n ori; daca graful este dens (m  n2), este mai
bine sa folosim algoritmul lui Floyd.

86
De obicei, dorim sa aflam nu numai lungimea celui mai scurt drum, dar si traseul sau. In
acesta situatie, vom construi o a doua matrice P, initializata cu zero. Bucla cea mai
interioara 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

Cand algoritmul se opreste, P[i, j] va contine varful din ultima iteratie care a cauzat o
modificare in D[i, j]. Pentru a afla prin ce varfuri trece cel mai scurt drum de la i la j,
consultam elementul P[i, j]. Daca P[i, j] = 0, atunci cel mai scurt drum este chiar muchia
(i, j). Daca P[i, j] = k, atunci cel mai scurt drum de la i la j trece prin k si urmeaza sa
consultam recursiv elementele P[i, k] si P[k, j] pentru a gasi si celelalte varfuri
intermediare.

Pentru exemplul precedent se obtine

Deoarece P[1, 3] = 4, cel mai scurt drum de la 1 la 3 trece prin 4. Deoarece P[1, 4] = 2,
cel mai scurt drum de la 1 la 4 trece prin 2. Rezulta ca cel mai scurt drum de la 1 la 3 este:
1, 2, 4, 3.

87
8.6 Arbori binari optimi de cautare
Un arbore binar in care fiecare varf contine o valoare (numita cheie) este un arbore de
cautare, daca cheia fiecarui varf neterminal este mai mare sau egala cu cheile
descendentilor sai stangi si mai mica sau egala cu cheile descendentilor sai drepti. Daca
cheile arborelui sunt distincte, aceste inegalitati sunt, in mod evident, stricte.

Figura 8.4 Un arbore binar de cautare.

Figura 8.4 este un exemplu de arbore de cautare[***] , continand cheile A, B, C, ..., H.


Varfurile pot contine si alte informatii (in afara de chei), la care sa avem acces prin
intermediul cheilor.

Aceasta structura de date este utila, deoarece permite o cautare eficienta a valorilor in
arbore (Exercitiul 8.10). De asemenea, este posibil sa actualizam un arbore de cautare (sa
stergem un varf, sa modificam valoarea unui varf, sau sa adaugam un varf) intr-un mod
eficient, fara sa distrugem proprietatea de arbore de cautare.

Cu o multime data de chei, se pot construi mai multi arbori de cautare (Figura 8.5).

88
Figura 8.5 Un alt arbore binar de cautare.

Pentru a cauta o cheie X in arborele de cautare, X va fi comparata la inceput cu cheia


radacinii arborelui. Daca X este mai mica decat cheia radacinii, atunci se continua cautarea
in subarborele stang; daca Xeste egala cu cheia radacinii, atunci cautarea se incheie cu
succes; daca X este mai mare decat cheia radacinii, atunci se continua cautarea in
subarborele drept. Se continua apoi recursiv acest proces.

De exemplu, in arborele din Figura 8.4 putem gasi cheia E prin doua comparatii, in timp
ce aceeasi cheie poate fi gasita in arborele din Figura 8.5 printr-o singura comparatie.
Daca cheile A, B, C, ..., H au aceeasi probabilitate, atunci pentru a gasi o cheie oarecare
sunt necesare in medie:

(23132434)/8 = 22/8 comparatii, pentru arborele din Figura 8.4


(43231323)/8 = 21/8 comparatii, pentru arborele din Figura 8.5

Cand cheile sunt echiprobabile, arborele de cautare care minimizeaza numarul mediu de
comparatii necesare este arborele de cautare de inaltime minima (demonstrati acest lucru
si gasiti o metoda pentru a construi arborele respectiv!).

89
Vom rezolva in continuare o problema mai generala. Sa presupunem ca avem
cheile c1 < c2 < ... < cn si ca, in tabloul p, p[i] este probabilitatea cu care este cautata
cheia ci, 1  i  n. Pentru simplificare, vom considera ca sunt cautate doar cheile prezente
in arbore, deci ca p[1]p[2]...p[n] = 1. Ne propunem sa gasim arborele optim de cautare
pentru cheile c1, c2, ..., cn, adica arborele care minimizeaza numarul mediu de comparatii
necesare pentru a gasi o cheie.

Problema este similara cu cea a gasirii arborelui cu lungimea externa ponderata minima
(Sectiunea 6.3), cu deosebirea ca, de aceasta data, trebuie sa mentinem ordinea cheilor.
Aceasta restrictie face ca problema gasirii arborelui optim de cautare sa fie foarte
asemanatoare cu problema inmultirii inlantuite a matricilor. In esenta, se poate aplica
acelasi algoritm.

Daca o cheie ci se afla intr-un varf de adincime di, atunci sunt necesare di 1 comparatii
pentru a o gasi. Pentru un arbore dat, numarul mediu de comparatii necesare este

Dorim sa gasim arborele pentru care acest numar este minim.

Vom rezolva aceasta problema prin metoda programarii dinamice. Prima decizie consta in
a determina cheia ck a radacinii. Sa observam ca este satisfacut principiul optimalitatii:
daca avem un arbore optim pentru c1, c2, ..., cn si cu cheia ck in radacina, atunci subarborii
sai stang si drept sunt arbori optimi pentru cheile c1, c2, ..., ck-1, respectiv ck+1, ck+2, ..., cn.
Mai general, intr-un arbore optim continand cele nchei, un subarbore oarecare este la
randul sau optim pentru o secventa de chei succesive ci, ci+1, ..., cj, i  j.

90
In tabloul C, sa notam cu C[i, j] numarul mediu de comparatii efectuate intr-un subarbore
care este optim pentru cheile ci, ci+1 ,..., cj, atunci cand se cauta o cheie X in arborele optim
principal. Valoarea

m[i, j] = p[i] p[i1] ... p[ j]

este probabilitatea ca X sa se afle in secventa ci, ci+1, ..., cj. Fie ck cheia radacinii
subarborelui considerat. Atunci, probabilitatea compararii lui X cu ck este m[i, j], si avem:

C[i, j] = m[i, j] C[i, k1] C[k1, j]

Pentru a obtine schema de programare dinamica, ramine sa observam ca ck (cheia


radacinii subarborelui) este aleasa astfel incat

C[i, j] = m[i, j]  (C[i, k1]C[k1, j]) (*)

In particular, C[i, i] = p[i] si C[i, i1] = 0.

Daca dorim sa gasim arborele optim pentru cheile c1 < c2 < ... < c5, cu probabilitatile

p[1] = 0,30 p[2] = 0,05 p[3] = 0,08


p[4] = 0,45 p[5] = 0,12

calculam pentru inceput matricea m:

91
Sa notam ca C[i, i] = p[i], 1  i  5. Din relatia (*), calculam celelalte valori pentru C[i, j]:

C[1, 2] = m[1, 2] min(C[1, 0]C[2, 2], C[1, 1]C[3, 2])


= 0,35 min(0,05, 0,30) = 0,40

Similar,

C[2, 3] = 0,18 C[3, 4] = 0,61 C[4, 5] = 0,69

Apoi,

C[1, 3] = m[1, 3] min(C[1, 0]C[2, 3], C[1, 1]C[3, 3], C[1, 2]C[4, 3])
= 0,43 min(0,18, 0,38, 0,40) = 0,61

C[2, 4] = 0,76 C[3, 5] = 0,85

C[1, 4] = 1,49 C[2, 5] = 1,00

C[1, 5] = m[1, 5] min(C[1, 0]C[2, 5], C[1, 1]C[3, 5], C[1, 2]C[4, 5],
C[1, 3]C[5, 5], C[1, 4]C[6, 5]) = 1,73

Arborele optim necesita deci in medie 1,73 comparatii pentru a gasi o cheie.
92
In acest algoritm, calculam valorile C[i, j] in primul rand pentru ji = 1, apoi
pentru ji = 2 etc. Cand ji = q, avem de calculat nq valori ale lui C[i, j], fiecare
implicand o alegere intre q1 posibilitati. Timpul necesar [****] este deci in

Stim acum cum sa calculam numarul minim de comparatii necesare pentru a gasi o cheie
in arborele optim. Mai ramane sa construim efectiv arborele optim. In paralel cu tabloul C,
vom construi tabloul r, astfel incat r[i, j] sa contina valoarea lui k pentru care este obtinuta
in relatia (*) valoarea minima a lui C[i, j], unde i < j. Generam un arbore binar, conform
urmatoarei metode recursive:

radacina este etichetata cu (1, n)


daca un varf este etichetat cu (i, j), i < j, atunci fiul sau stang va fi etichetat cu
(i, r[i, j]1) si fiul sau drept cu (r[i, j]1, j)
varfurile terminale sunt etichetate cu (i, i)

Plecand de la acest arbore, arborele de cautare optim se obtine schimband etichetele


(i, j), i < j, in cr[i, j], iar etichetele (i, i) in ci.

93
Figura 8.6 Un arbore optim de cautare.

Pentru exemplul precedent, obtinem astfel arborele optim din Figura 8.6.

Problema se poate generaliza, acceptand sa cautam si chei care nu se afla in arbore.


Arborele optim de cautare se obtine in mod similar.

8.7 Arborii binari de cautare ca tip de data


Intr-o prima aproximare, arborele binar este un tip de data similar tipului lista. Varfurile
sunt compuse din informatie (cheie) si legaturi, iar arborele propiu-zis este complet
precizat prin adresa varfului radacina. In privinta organizarii memoriei, putem opta fie
pentru tablouri paralele, ca in Exercitiul 8.10, fie pentru alocarea dinamica a elementelor.
Alegand alocarea dinamica, vom utiliza in intregime modelul oferit de
clasa lista<E> elaborata in Sectiunea 4.3. Astfel, clasa parametrica arbore<E>, cu o
structura interna de forma:

94
template <class E>
class arbore {
// ... declaratii friend
public:
arbore( ) { root = 0; n = 0; }

// ... functii membre

private:
varf<E> *root; // adresa varfului radacina
int n; // numarul varfurilor din arbore
};

are la baza o clasa privata varf<E> prin intermediul careia vom implementa majoritatea
operatiilor efectuate asupra arborilor. Vom cauta sa izolam, ori de cate ori va fi posibil,
operatiile direct aplicabile varfurilor, astfel incat interfata dintre cele doua clase sa fie
foarte clar precizata printr-o serie de “operatii elementare”.

Nu vom implementa in aceasta sectiune arbori binari in toata generalitatea lor, ci doar
arborii de cautare. Obiectivul urmarit in prezentarea listelor a fost structura de date in sine,
impreuna cu procedurile generale de manipulare. In cazul arborelui de cautare, nu mai este
necesara o astfel de generalitate, deoarece vom implementa direct operatiile specifice. In
mare, aceste operatii pot fi impartite in trei categorii:

Cautari. Localizarea varfului cu o anumita cheie, a succesorului sau predecesorului


lui, precum si a varfurilor cu cheile de valoare maxima, respectiv minima.
Modificari. Arborele se modifica prin inserarea sau stergerea unor varfuri.
Organizari. Arborele nu este construit prin inserarea elementelor, ci global,

95
stabilind intr-o singura trecere legaturile dintre varfuri. Frecvent, organizarea se face
conform unor criterii pentru optimizarea cautarilor. Un caz particular al acestei operatii
este reorganizarea arborelui dupa o perioada suficient de mare de utilizare. Este vorba de
reconstruirea arborelui intr-o structura optima, pe baza statisticilor de utilizare.

Datorita operatiilor de cautare si modificare, elementele de tip E trebuie sa fie


comparabile prin operatorii uzuali ==, !=, >. In finalul Sectiunii 7.4.1, am aratat ca o
asemenea pretentie nu este totdeauna justificata. Desigur ca, in cazul unor structuri bazate
pe relatia de ordine, asa cum sunt heap-ul si arborele de cautare, este absolut normal ca
elementele sa poata fi comparate.

Principalul punct de interes pentru noi este optimizarea, conform algoritmului de


programare dinamica. Nu vom ignora nici cautarile, nici operatiile de modificare (tratate
in Sectiunea 8.7.2).

8.7.1 Arborele optim

Vom rezolva problema obtinerii arborelui optim in cel mai simplu caz posibil (din punct
de vedere al utilizarii, dar nu si in privinta programarii): arborele deja exista si trebuie
reorganizat intr-un arbore de cautare optim. Avand in vedere specificul diferit al
operatiilor de organizare fata de celelalte operatii efectuate asupra grafurilor, am
considerat util sa incapsulam optimizarea intr-o clasa pe care o vom numi “structura
pentru optimizarea arborilor” sau, pe scurt, s8a.

Clasa s8a este o clasa parametrica privata, asociata clasei arbore<E>. Functionalitatea
ei consta in:

96
i) initializarea unui tablou cu adresele varfurilor in ordinea crescatoare a probabilitatilor
cheilor
ii) stabilirea de noi legaturi intre varfuri astfel incat arborele sa fie optim.

Principalul motiv pentru care a fost aleasa aceasta implementare este ca sunt necesare doar
operatii modificare a legaturilor. Deplasarea unui varf (de exemplu, pentru sortare)
inseamna nu numai deplasarea cheii, ci si a informatiei asociate. Cum fiecare din aceste
elemente pot fi oricat de mari, clasa s8a realizeaza o economie semnificativa de timp si
(mai ales) de memorie.

Pentru optimizarea propriu-zisa, am implementat atat algoritmul de programare dinamica,


cat si pe cel greedy prezentat in Exercitiul 8.12. Desi algoritmul greedy nu garanteaza
obtinerea arborelui optim, el are totusi avantajul ca este mai eficient decat algoritmul de
programare dinamica din punct de vedere al timpului de executie si al memoriei utilizate.
Invocarea optimizarii se realizeaza din clasa arbore<E>, prin secvente de genul

arbore<float> af;

// arborele af se creeaza prin inserarea cheilor


// arborele af se utilizeaza

// pe baza probabilitatilor predefinite si actualizate


// prin utilizarea arborelui se invoca optimizarea

af.re_prodin( ); // sau af.re_greedy( );

unde functiile membre re_greedy() si re_prodin() sunt


definte astfel:

97
template <class E>
arbore<E>& arbore<E>::re_greedy( ) {
// reorganizare prin metoda greedy
s8a<E> opt( root, n );
root = opt.greedy( );
return *this;
}

template <class E>


arbore<E>& arbore<E>::re_prodin( ) {
// reorganziare prin programare dinamica
s8a<E> opt( root, n );
root = opt.prodin( );
return *this;
}

Dupa adaugarea tuturor functiilor si datelor membre necesare implementarii


functiilor greedy() si prodin(), clasa s8a are urmatoarea structura:

template <class E>


class s8a { // clasa pentru construirea arborelui optim
friend class arbore<E>;
private:
s8a( varf<E> *root, int nn ): pvarf( n = nn ) {
int i = 0; // indice in pvarf
setvarf( i, root ); // setarea elementelor din pvarf
}

98
// initializarea tabloului pvarf cu un arbore deja format
void setvarf( int&, varf<E>* );
varf<E>* greedy( ) { // "optim" prin algoritmul greedy
return _greedy( 0, n );
}

varf<E>* prodin( ) { // optim prin programare dinamica


_progDinInit( ); return _progDin( 0, n - 1 );
}

// functiile prin care se formeaza efectiv arborele


varf<E>* _greedy ( int, int );
varf<E>* _progDin ( int, int );
void _progDinInit( ); // initializeaza tabloul r

// date membre
tablou<varf<E>*> pvarf; // tabloul adreselor varfurilor
int n; // numarul varfurilor din arbore

// tabloul indicilor necesar alg. de programare dinamica


tablou< tablou<int> > r;
};

In stabilirea valorilor tablourilor pvarf si r se pot distinge foarte clar cele doua etape
ale executiei constructorului clasei s8a, etape mentionate in Sectiunea 4.2.1. Este vorba
de etapa de initializare (implementata prin lista de initializare a membrilor) si de etapa de

99
atribuire (implementata prin corpul constructorului). Lista de initializare asociata
constructorului clasei s8a contine parametrul necesar dimensionarii
tabloului pvarf pentru cele n elemente ale arborelui. Cum este insa initializat
tabloul r care nu apare in lista de initializare? In astfel de cazuri, se invoca automat
constructorul implicit (apelabil fara nici un argument) al clasei respective. Pentru
clasa tablou<T>, constructorul implicit doar initializeaza cu 0 datele membre.

Etapa de atribuire a constructorului clasei s8a, implementata prin invocarea


functiei setvarf(), consta in parcurgerea arborelui si memorarea adreselor varfurilor
vizitate in tabloul pvarf. Functia setvarf() parcurge pentru fiecare varf subarborele
stang, apoi memoreaza adresa varfului curent si, in final, parcurge subarborele drept.
Dupa cum vom vedea in Exercitiul 9.1, acest mod de parcurgere are proprietatea ca
elementele arborelui sunt parcurse in ordine crescatoare. De fapt, este vorba de o metoda
de sortare similara quicksort-ului, varful radacina avand acelasi rol ca si elementul pivot
din quicksort.

template <class E>


void s8a<E>::setvarf( int& poz, varf<E>* x ) {

if ( x ) {
setvarf( poz, x->st );
pvarf[ poz++ ] = x;
setvarf( poz, x->dr );

// anulam toate legaturile elementului x


x->st = x->dr = x->tata = 0;
}
}
100
In aceasta functie, x->st, x->dr si x->tata sunt legaturile varfului curent x catre fiul
stang, catre cel drept si, respectiv, catre varful tata. In plus fata de aceste legaturi,
obiectele de tip varf<E> mai contin cheia (informatia) propriu-zisa si un camp auxiliar
pentru probabilitatea varfului (elementului). In consecinta, clasa varf<E> are
urmatoarea structura:

template <class E>


class varf {
friend class arbore<E>;
friend class s8a<E>;

private:
varf( const E& v, float f = 0 ): key( v )
{ st = dr = tata = 0; p = f; }

varf<E> *st; // adresa fiului stang


varf<E> *dr; // adresa fiului drept
varf<E> *tata; // adresa varfului tata

E key; // cheia
float p; // frecventa utilizarii cheii curente
};

Implementarea celor doua metode de optimizare a arborelui urmeaza pas cu pas


algoritmul greedy si, respectiv, algoritmul de programare dinamica. Ambele (re)stabilesc
legaturile dintre varfuri printr-un proces recursiv, pornind fie direct de la probabilitatile
elementelor, fie de la o matrice (matricea r) construita pe baza acestor probabilitati.
Functiile care stabilesc legaturile, adica _progDin() si _greedy(), sunt urmatoarele:
101
template <class E>
varf<E>* s8a<E>::_greedy( int m, int M ) {
// m si M sunt limitele subsecventei curente
if ( m == M ) return 0;

// se determina pozitia k a celei mai frecvente chei


int k; float pmax = pvarf[ k = m ]->p;
for ( int i = m; ++i < M; )
if ( pvarf[ i ]->p > pmax ) pmax = pvarf[ k = i ]->p;

// se selecteaza adresa varfului de pe pozitia k


varf<E> *actual = pvarf[ k ];

// se construiesc subarborii din stanga si din deapta


// se initializeaza legatura spre varful tata
if ( (actual->st = _greedy( m, k )) != 0 )
actual->st->tata = actual;
if ( (actual->dr = _greedy( k + 1, M )) != 0 )
actual->dr->tata = actual;

// subarborele curent este gata; se returneaza adresa lui


return actual;
}

template <class E>


varf<E>* s8a<E>::_progDin( int i, int j ) {
// i si j, i <=j, sunt coordonatele radacinii
// subarborelui curent in tabloul r
if ( i > j ) return 0;
102
// se selecteaza adresa varfului radacina
varf<E> *actual = pvarf[ r[ j ][ i ] ];

if ( i != j ) { // daca nu este un varf frunza ...


// se construiesc subarborii din stanga si din deapta
// se initializeaza legatura spre varful tata
if ( (actual->st = _progDin( i, r[j][i] - 1 )) != 0 )
actual->st->tata = actual;
if ( (actual->dr = _progDin( r[j][i] + 1, j )) != 0 )
actual->dr->tata = actual;
}

// subarborele curent este gata; se returneaza adresa lui


return actual;
}

Folosind notatiile introduse in descrierea algoritmului de optimizare prin programare


dinamica, functia _progDinInit() construieste matricea r, unde r[i][j], i < j,
este indicele in tabloul pvarf al adresei varfului etichetat cu (i, j). In acest scop, se
foloseste o alta matrice C, unde C[i][j], i < j, este numarul de comparatii efectuate
in subarborele optim al cheilor cu indicii i, ..., j. Initial, C este completata cu
probabilitatile cumulate ale cheilor de indici i, ..., j.

Se observa ca matricile r si C sunt superior triunghiulare. Totusi, pentru implementare,


am preferat sa lucram cu matrici inferior triunghiulare, adica cu transpusele
matricilor r si C, deoarece adresarea elementelor ar fi fost altfel mai complicata.

103
template <class E>
void s8a<E>::_progDinInit( ) {
int i, j, d;
tablou< tablou<float> > C; // tabloul C este local

// redimensionarea si initializarea tablourilor C si r


// ATENTIE! tablourile C si r sunt TRANSPUSE.
r.newsize( n );
C.newsize( n );
for ( i = 0; i < n; i++ ) {
r[ i ].newsize( i + 1 ); r[ i ][ i ] = i;
C[ i ].newsize( i + 1 ); C[ i ][ i ] = pvarf[ i ]->p;
}

// pentru inceput C este identic cu m


for ( d = 1; d < n; d++ )
for ( i = 0; (j = i + d) < n; i++ )
C[ j ][ i ] = C[ j - 1 ][ i ] + C[ j ][ j ];

// elementele din C se calculeaza pe diagonale


for ( d = 1; d < n; d++ )
for ( i = 0; (j = i + d) < n; i++ ) {
// in calculul minimului dintre C[i][k-1]+C[k+1][j]
// consideram mai intai cazurile k=i si k=j in care
// avem C[i][i-1] = 0 si C[j+1][j] = 0
int k; float Cmin;
if ( C[ j ][ i + 1 ] < C[ j - 1 ][ i ] )
Cmin = C[ j ][ (k = i) + 1 ];
else
104
Cmin = C[ (k = j) - 1 ][ i ];

// au mai ramas de testat elementele i+1, ..., j-1


for ( int l = i + 1; l < j; l++ )
if ( C[ l - 1 ][ i ] + C[ j ][ l + 1 ] < Cmin )
Cmin = C[ (k = l) - 1 ][ i ] + C[ j ][ l + 1 ];

// minimul si pozitia lui sunt stabilite ...


C[ j ][ i ] += Cmin;
r[ j ][ i ] = k;
}
}

8.7.2 Cautarea in arbore

Principala operatie efectuata prin intermediul arborilor binari de cautare este regasirea
informatiei asociate unei anumite chei. Functia de cautare search() are ca argument
cheia pe baza careia se va face cautarea si returneaza false sau true, dupa cum cheia fost
regasita, sau nu a fost regasita in arbore. Cand cautarea s-a terminat cu succes, valoarea
din arbore a cheii regasite este returnata prin intermediul argumentului de tip referinta,
pentru a permite consultarea informatiilor asociate.

template <class E>


int arbore<E>::search( E& k ) {
varf<E> *x = _search( root, k );
if ( !x ) return 0; // element absent
x->p++; // actualizarea frecventei
k = x->key; return 1;
}
105
Actualizarea probabilitatilor cheilor din arbore, dupa fiecare operatie de cautare, este ceva
mai delicata, deoarece impune stabilirea importantei evaluarilor existente in raport cu
rezultatele cautarilor. De fapt, este vorba de un proces de invatare care porneste de la
anumite cunostinte deja acumulate. Problema este de a stabili gradul de importanta al
cunostintelor existente in raport cu cele nou dobandite. Inainte de a prezenta o solutie
elementara a acestei probleme, sa observam ca algoritmii de optimizare lucreaza cu
probabilitati, dar numai ca ponderi. In consecinta, rezultatul optimizarii nu se schimba,
daca in loc de probabilitati se folosesc frecvente absolute.

Fie trei chei ale caror probabilitati de cautare au fost estimate initial la 0,18, 0,65, 0,17. Sa
presupunem ca se doreste optimizarea arborelui de cautare asociat acestor chei, atat pe
baza acestor estimari, cat si folosind rezultatele a 1000 de cautari de instruire terminate cu
succes[*****] . Daca fixam ponderea estimarilor initiale in raport cu rezultatele instruirii
la 5  2, atunci vom initializa membrul p (estimarea probabilitatii cheii curente) din
clasa varf<E> cu valorile

0,18  1000  (5  2)  450


0,65  1000  (5  2)  1625
0,17  1000  (5  2)  425

Apoi, la fiecare cautare terminata cu success, membrul p corespunzator cheii gasite se


incrementeaza cu 1. De exemplu, daca prima cheie a fost gasita in 247 cazuri, a doua in
412 cazuri si a treia in 341 cazuri, atunci valorile lui p folosite la optimizarea arborelui
vor fi 697, 2037 si 766. Suma acestor valori este 3500, valoare care corespunde celor 1000
de incercari plus ponderea de 1000  (5  2)  2500 asociata estimarii initiale. Noile
probabilitati, invatate prin instruire, sunt:

106
697  3500  0,20
2037  3500  0,58
766  3500  0,22

Pentru verificarea rezultatelor de mai sus, sa refacem calculele, lucrand numai cu


probabilitati. Estimarile initiale ale probabilitatilor sunt 0,18, 0,65 si 0,17. In urma
instruirii, cele trei chei au fost cautate cu probabilitatile:

247  1000  0,247


412  1000  0,412
697  1000  0,697

Avand in vedere raportul de 5 / 2 stabilit intre estimarea initiala si rezultatele instruirii,


probabilitatile finale[******] sunt:

(0,18  5  0,247  2) / 7  0,20


(0,65  5  0,412  2) / 7  0,58
(0,17  5  0,697  2)  7  0,22

Cautarea este, de fapt, o parcurgere a varfurilor, realizata prin


functia _search(varf<E>*, const E&). Aceasta functie nu face parte din
clasa arbore<E>, deoarece opereaza exclusiv asupra varfurilor. Iata varianta ei
recursiva, impreuna cu alte doua functii asemanatoare: _min(), pentru determinarea
varfului minim din arbore si _succ(), pentru determinarea succesorului[*******] .

template <class E>


varf<E>* _search( varf<E>* x, const E& k ) {
107
while ( x != 0 && k != x->key )
x = k > x->key? x->dr: x->st;
return x;
}

template <class E>


varf<E>* _min( varf<E>* x ) {
while ( x->st != 0 )
x = x->st;
return x;
}

template <class E>


varf<E>* _succ( varf<E>* x ) {
if ( x->dr != 0 ) return _min( x->dr );

varf<E> *y = x->tata;
while ( y != 0 && x == y->dr )
{ x = y; y = y->tata; }
return y;
}

Existenta acestor functii impune completarea clasei varf<E> cu


declaratiile friend corespunzatoare.

Sa remarcam asemanarea dintre functiile C++ de mai sus si functiile analoage din
Exercitiul 8.10.

108
Pentru a demonstra corectitudinea functiilor _serarch() si _min(), nu avem decat sa
ne reamintim ca, prin definitie, intr-un arbore binar de cautare fiecare varf K verifica
relatiile X  K si K  Y pentru orice varf X din subarborele stang si orice varf Y din
subarborele drept.

Figura 8.7 Pozitiile relative ale varfului K in raport cu sucesorul sau S.

Demonstrarea corectitudinii functiei _succ() este de asemenea foarte simpla.


Fie K varful al carui succesor S trebuie determinat. Varfurile K si S pot fi situate astfel:

Varful S este in subarborele drept al varfului K. Deoarece aici sunt numai


varfuri Y cu proprietatea K  Y (vezi Figura 8.7a) rezulta ca S este valoarea minima din
acest subarbore. In plus, avand in vedere procedura pentru determinarea minimului,
varful S nu are fiul stang.
Varful K este in subarborele stang al varfului S. Deoarece fiecare varf X de aici
109
verifica inegalitatea X  S (vezi Figura 8.7b), deducem ca maximul din acest subarbore
este chiar K. Dar maximul se determina parcurgand fiii din dreapta pana la un varf fara
fiul drept. Deci, varful K nu are fiul drept, iar S este primul ascendent din stanga al
varfului K.

In consecinta, cele doua situatii se exclud reciproc, deci functia _succ() este corecta.

8.7.3 Modificarea arborelui

Modificarea structurii arborelui de cautare, prin inserarea sau stergerea unor varfuri
trebuie realizata astfel incat proprietatea de arbore de cautare sa nu se altereze. Cele doua
operatii sunt diferite in privinta complexitatii. Inserarea este simpla, fiind similara cautarii.
Stergerea este mai dificila si mult diferita de operatiile cu care deja ne-am obisnuit.

Pentru inserarea unei noi chei, vom folosi functia

template <class E>


int arbore<E>::ins( const E& k, float p ) {
varf<E> *y = 0, *x = root;

while ( x != 0 ) {
y = x;
if ( k == x->key ) { // cheia deja exista in arbore
x->p += p; // se actualizeaza frecventa
return 0; // se returneaza cod de eroare
}
x = k > x->key? x->dr: x->st;
}
110
// cheia nu exista in arbore
varf<E> *z = new varf<E>( k, p );
z->tata = y;

if ( y == 0 ) root = z;
else if ( z->key > y->key ) y->dr = z;
else y->st = z;

n++; // in arbore este cu un varf mai mult


return 1;
}

Valoarea returnata este true, daca cheia k a putut fi inserata in arbore, sau false, in cazul
in care deja exista in arbore un varf cu cheia k. Inserarea propriu-zisa consta in cautarea
cheii k prin intermediul adreselor x si y, y fiind adresa tatalui lui x. Atunci cand am
terminat procesul de cautare, valoarea lui x devine 0 si noul varf se va insera la stanga sau
la dreapta lui y, in functie de relatia dintre cheia k si cheia lui y.

Procedura de stergere incepe prin a determina adresa z a varfului de sters, pe baza cheii k.
Daca procesul de cautare se finalizeaza cu succes, cheia k se va actualiza (in scopul unor
prelucrari ulterioare) cu informatia din varful z, iar apoi se demareaza procesul de
stergere efectiva a varfului z. Daca z este un varf terminal, nu avem decat sa anulam
legatura corespunzatoare din varful tata. Chiar si atunci cand zare un singur fiu, stergerea
este directa. Adresa lui z din varful tata se inlocuieste cu adresa fiului lui z. A treia si cea
mai complicata situatie apare atunci cand z este situat undeva in interiorul arborelui,
avand ambele legaturi complete. In acest caz, nu vom mai sterge varful z, ci varful y,
succesorul lui z, dar nu inainte de a copia continutul lui y in z. Stergerea varfului y se
111
face conform unuia din cele doua cazuri de mai sus, deoarece, in mod sigur, y nu are fiul
stang. Intr-adevar, intr-un arbore de cautare, succesorul unui varf cu doi fii nu are fiul
stang, iar predecesorul[********] unui varf cu doi fii nu are fiul drept (demonstrati acest
lucru!). Pentru ilustrarea celor trei situatii, am sters din arborele din Figura 8.8a
varfurile E (varf cu doi fii), A (varf cu un fiu) si L (varf terminal).

Figura 8.8 Stergerea varfurilor E, A si L dintr-un arbore binar de


cautare.

Procedura de stergere se implementeaza astfel:

template <class E>


int arbore<E>::del( E& k ) {
varf<E> *z = _search( root, k ); // se cauta cheia k
if ( !z ) return 0; // nu a fost gasita

112
n--; // in arbore va fi cu un varf mai putin
k = z->key; // k va retine intreaga informatie din z

// - y este z daca z are cel mult un fiu si


// succesorul lui z daca z are doi fii
// - x este fiul lui y sau 0 daca y nu are fii
varf<E> *y, *x;

y = z->st == 0 || z->dr == 0? z: _succ( z );


x = y->st != 0? y->st: y->dr;

// se elimina varful y din arbore astfel:


// 1. se stabileste legatura in x spre varful tata
if ( x != 0 )
x->tata = y->tata;

// 2. in varful tata se stabileste legatura spre x


if ( y->tata == 0 ) root = x;
else if ( y == y->tata->st ) y->tata->st = x;
else y->tata->dr = x;

// 3. daca z are 2 fii, succesorul lui ii ia locul


if ( y != z ) { z->key = y->key; z->p = y->p; }

// 4. stergerea propriu-zisa
y->st = y->dr = 0;
delete y;

113
return 1;
}

Complexitatea functiei de stergere este tipica pentru structurile de cautare. Aceste structuri
tind sa devina atat de compacte in organizarea lor interna, incat stergerea fiecarei chei
necesita reparatii destul de complicate. De aceea, deseori se prefera o “stergere lenesa”
(lazy deletion), prin care varful este doar marcat ca “sters”, stergerea efectiva realizandu-
se cu ocazia unor reorganizari periodice.

Desi clasa arbore<E> este incomplet specificata, lipsind constructorul de copiere,


operatorul de atribuire, destructorul etc, operatiile implementate in aceasta sectiune pot fi
testate prin urmatorul program.

#include <iostream.h>
#include "arbore.h"

main( ) {
int n;
cout << "Numarul de varfuri ... "; cin >> n;

arbore<char> g; char c; float f;

cout << "Cheile si Frecventele lor:\n";


for ( int i = 0; i < n; i++ ) {
cout << "... ";
cin >> c; cin >> f;
g.ins( c, f );
}
114
cout << "Arborele initial:\n"; g.inord( );

cout << "\n\nDelete din initial (cheie) <EOF>:\n ...";


while( cin >> c ) {
if ( g.del( c ) ) {
cout << "\nSe sterge varful cu cheia: " << c;
cout << "\nInordine:\n"; g.inord( );
}
else
cout << "\nelement absent";
cout << "\n... ";
}
cin.clear( );

g.re_greedy( );
cout << "\n\nArborele Greedy:\n"; g.inord( );

cout << "\n\nInsert in Greedy "


<< "(cheie+frecventa) <EOF>:\n... ";
while( (cin >> c) && (cin >> f) ) {
g.ins( c, f );
cout << "\nInordine:\n"; g.inord( );
cout << "\n... ";
}
cin.clear( );

cout << "\n\nCautari in Greedy (cheie) <EOF>:\n ...";


while( cin >> c ) {
115
if ( g.search( c ) ) {
cout << "\nNodul cu cheia: " << c;
cout << "\nInordine:\n"; g.inord( );
}
else
cout << "\nelement absent";
cout << "\n... ";
}
cin.clear( );

cout << "\n\nDelete din Greedy (cheie) <EOF>:\n ...";


while( cin >> c ) {
if ( g.del( c ) ) {
cout << "\nSe sterge varful cu cheia: " << c;
cout << "\nInordine:\n"; g.inord( );
}
else
cout << "\nelement absent";
cout << "\n... ";
}
cin.clear( );

g.re_prodin( );
cout << "Arborele Greedy re-ProgDin:\n"; g.inord( );

return 1;
}

116
Functia arbore<E>::inord(), definita in Sectiunea 9.2, realizeaza afisarea arborelui,
astfel incat sa poata fi usor de reconstituit pe hartie. De exemplu, arborele din Figura 8.8b
este afisat astfel:

0x166c ( key C, f 0, st 0x0000, dr 0x0000, tata 0x163c )


0x163c ( key H, f 0, st 0x166c, dr 0x165c, tata 0x0000 )
0x169c ( key M, f 0, st 0x0000, dr 0x0000, tata 0x168c )
0x168c ( key N, f 0, st 0x169c, dr 0x16ac, tata 0x165c )
0x16ac ( key P, f 0, st 0x0000, dr 0x0000, tata 0x168c )
0x165c ( key R, f 0, st 0x168c, dr 0x0000, tata 0x163c )

8.8 Programarea dinamica comparata cu tehnica greedy


Atat programarea dinamica, cat si tehnica greedy, pot fi folosite atunci cand solutia unei
probleme este privita ca rezultatul unei secvente de decizii. Deoarece principiul
optimalitatii poate fi exploatat de ambele metode, s-ar putea sa fim tentati sa elaboram o
solutie prin programare dinamica, acolo unde este suficienta o solutie greedy, sau sa
aplicam in mod eronat o metoda greedy, atunci cand este necesara de fapt aplicarea
programarii dinamice. Vom considera ca exemplu o problema clasica de optimizare.

Un hot patrunde intr-un magazin si gaseste n obiecte, un obiect i avand valoarea vi si


greutatea gi. Cum sa-si optimizeze hotul profitul, daca poate transporta cu un rucsac cel
mult o greutate G? Deosebim doua cazuri. In primul dintre ele, pentru orice obiect i, se
poate lua orice fractiune 0  xi  1 din el, iar in al doilea caz, xi  {0,1}, adica orice obiect
poate fi incarcat numai in intregime in rucsac. Corespunzator acestor doua cazuri,

117
obtinem problema continua a rucsacului, respectiv, problema 0/1 a rucsacului. Evident,
hotul va selecta obiectele astfel incat sa maximizeze functia obiectiv

unde x = (x1, x2, ..., xn), verifica conditia

Solutia problemei rucsacului poate fi privita ca rezultatul unei secvente de decizii. De


exemplu, hotul va decide pentru inceput asupra valorii lui x1, apoi asupra valorii lui x2 etc.
Printr-o secventa optima de decizii, el va incerca sa maximizeze functia obiectiv. Se
observa ca este valabil principiul optimalitatii. Ordinea deciziilor poate fi desigur oricare
alta.

Problema continua a rucsacului se poate rezolva prin metoda greedy, selectand la fiecare
pas, pe cat posibil in intregime, obiectul pentru care vi/gi este maxim. Fara a restrange
generalitatea, vom presupune ca

v1/g1  v2/g2  ...  vn/gn

Puteti demonstra ca prin acest algoritm obtinem solutia optima si ca aceasta este de
forma x* = (1, ..., 1, , 0, ..., 0), k fiind un indice, 1  k  n, astfel incat 0  xk  1.
Algoritmul greedy gaseste secventa optima de decizii, luand la fiecare pas cate o decizie
care este optima local. Algoritmul este corect, deoarece nici o decizie din secventa nu este

118
eronata. Daca nu consideram timpul necesar sortarii initiale a obiectelor, timpul este in
ordinul lui n.

Sa trecem la problema 0/1 a rucsacului. Se observa imediat ca tehnica greedy nu conduce


in general la rezultatul dorit. De exemplu, pentru g = (1, 2, 3), v = (6, 10, 12), G = 5,
algoritmul greedy furnizeaza solutia (1, 1, 0), in timp ce solutia optima este (0, 1, 1).
Tehnica greedy nu poate fi aplicata, deoarece este generata o decizie (x1 = 1) optima local,
nu insa si global. Cu alte cuvinte, la primul pas, nu avem suficienta informatie locala
pentru a decide asupra valorii lui x1. Strategia greedy exploateaza insuficient principiul
optimalitatii, considerand ca intr-o secventa optima de decizii fiecare decizie (si nu fiecare
subsecventa de decizii, cum procedeaza programarea dinamica) trebuie sa fie optima.
Problema se poate rezolva printr-un algoritm de programare dinamica, in aceasta situatie
exploatandu-se complet principiul optimalitatii. Spre deosebire de problema continua, nu
se cunoaste nici un algoritm polinomial pentru problema 0/1 a rucsacului.

Diferenta esentiala dintre tehnica greedy si programarea dinamica consta in faptul ca


metoda greedy genereaza o singura secventa de decizii, exploatand incomplet principiul
optimalitatii. In programarea dinamica, se genereaza mai multe subsecvente de decizii;
tinand cont de principiul optimalitatii, se considera insa doar subsecventele optime,
combinandu-se acestea in solutia optima finala. Cu toate ca numarul total de secvente de
decizii este exponential (daca pentru fiecare din cele n decizii sunt d posibilitati, atunci
sunt posibile d n secvente de decizii), algoritmii de programare dinamica sunt de multe ori
polinomiali, aceasta reducere a complexitatii datorandu-se utilizarii principiului
optimalitatii. O alta caracteristica importanta a programarii dinamice este ca se
memoreaza subsecventele optime, evitandu-se astfel recalcularea lor.

119
8.9 Exercitii
8.1 Demonstrati ca numarul total de apeluri recursive necesare pentru a-l calcula

pe C(n, k) este 2  2.

Solutie: Notam cu r(n, k) numarul de apeluri recursive necesare pentru a-l calcula
pe C(n, k). Procedam prin inductie, in functie de n. Daca n este 0, proprietatea este
adevarata. Presupunem proprietatea adevarata pentru n1 si demonstram pentru n.

Presupunem, pentru inceput, ca 0 < k < n. Atunci, avem recurenta

r(n, k) = r(n1, k1) r(n1, k) 2

Din relatia precedenta, obtinem

r(n, k) = 2 22 22=2 2

Daca k este 0 sau n, atunci r(n, k) = 0 si, deoarece in acest caz avem = 1, rezulta ca
proprietatea este adevarata. Acest rezultat poate fi verificat practic, ruland programul din
Exercitiul 2.5.

8.2 Aratati ca principiul optimalitatii

120
i) este valabil in problema gasirii celui mai scurt drum dintre doua varfuri ale unui graf
ii) nu este valabil in problema determinarii celui mai lung drum simplu dintre doua
varfuri ale unui graf

8.3 Demonstrati ca  4n/(2n1).

8.4 Folosind algoritmul serie, calculati probabilitatea ca jucatorul A sa castige,


presupunand n = 4 si p = 0,45.

8.5 Problema inmultirii inlantuite optime a matricilor se poate rezolva si prin


urmatorul algoritm recursiv:

function rminscal(i, j)
{returneaza numarul minim de inmultiri scalare
pentru a calcula produsul matricial 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. Gasiti o limita inferioara a timpului. Explicati ineficienta
acestui algoritm.

121
Solutie: Notam cu r( ji1) numarul de apeluri recursive necesare pentru a-l calcula
pe rminscal(i, j). Pentru n > 2 avem

iar r(2) = 2. Prin metoda iteratiei, deduceti ca r(n)  2n-1, pentru n > 2. Timpul pentru un
apel rminscal(1, n) este atunci in (2n).

8.6 Elaborati un algoritm eficient care sa afiseze parantezarea optima a unui produs
matricial M(1), ..., M(n). Folositi pentru aceasta matricea r, calculata de
algoritmul minscal. Analizati algoritmul obtinut.

Solutie: Se apeleaza cu paran(1, n) urmatorul algoritm:

function paran(i, j)
if i = j then write “M(”, i, “)”
else write “(”
parant(i, r[i, j])
write “*”
parant(r[i, j]1, j)
write “)”

Aratati prin inductie ca o parantezare completa unei expresii de n elemente are exact n1
perechi de paranteze. Deduceti de aici care este eficienta algoritmului.

122
8.7 Presupunand matricea P din algoritmul lui Floyd cunoscuta, elaborati un algoritm
care sa afiseze prin ce varfuri trece cel mai scurt drum dintre doua varfuri oarecare.

8.8 Intr-un graf orientat, sa presupunem ca ne intereseaza doar existenta, nu si


lungimea drumurilor, intre fiecare pereche de varfuri. Initial, L[i, j] = true daca muchia
(i, j) exista si L[i, j] = false in caz contrar. Modificati algoritmul lui Floyd astfel incat, in
final, sa avem D[i, j] = true daca exista cel putin un drum de la i la j si D[i, j] = false in
caz contrar.

Solutie: Se inlocuieste bucla cea mai interioara cu:

D[i, j]  D[i, j] or (D[i, k] and D[k, j])

obtinandu-se algoritmul lui Warshall (1962). Matricea booleana L se numeste inchiderea


tranzitiva a grafului.

8.9 Aratati cu ajutorul unui contraexemplu ca urmatoarea propozitie nu este, in


general, adevarata: “Un arbore binar este un arbore de cautare daca cheia fiecarui varf
neterminal este mai mare sau egala cu cheia fiului sau stang si mai mica sau egala cu cheia
fiului sau drept”.

8.10 Fie un arbore binar de cautare reprezentat prin adrese, astfel incat varful i (adica
varful a carui adresa este i) este memorat in patru locatii diferite continand :

123
KEY[i] = cheia varfului
ST[i] = adresa fiului stang
DR[i] = adresa fiului drept
TATA[i] = adresa tatalui

(Daca se foloseste o implementare prin tablouri paralele, atunci adresele sunt indici de
tablou). Presupunem ca variabila root contine adresa radacinii arborelui si ca o adresa este
zero, daca si numai daca varful catre care se face trimiterea lipseste. Elaborati algoritmi
pentru urmatoarele operatii in arborele de cautare:

i) Determinarea varfului care contine o cheie v data. Daca un astfel de varf nu exista, se
va returna adresa zero.
ii) Determinarea varfului care contine cheia minima.
iii) Determinarea succesorului unui varf i dat (succesorul varfului i este varful care are
cea mai mica cheie mai mare decat KEY[i]).

Care este eficienta acestor algoritmi?

Solutie:

i) Apelam tree-search(root, v), tree-search fiind functia:

function tree-search(i, v)
if i = 0 or v = KEY[i] then return i
if v < KEY[i] then return tree-search(ST[i], v)
else return tree-search(DR[i], v)

Iata si o versiune iterativa a acestui algoritm:

124
function iter-tree-search(i, v)
while i  0 and v  KEY[i] do
if i < KEY[i] then i  ST[i]
else i  DR[i]
return i

ii) Se apeleaza tree-min(root), tree-min fiind functia:

function tree-min(i)
while ST[i]  0 do i  ST[i]
return i

iii) Urmatorul algoritm returneaza succesorul varfului i:

function tree-succesor(i)
if DR[i]  0 then return tree-min(DR[i])
j  TATA[i]
while j  0 and i = DR[ j] do i  j
j  TATA[ j]
return j

8.11 Gasiti o formula explicita pentru T(n), unde T(n) este numarul de arbori de cautare
diferiti care se pot construi cu n chei distincte.

Indicatie: Faceti legatura cu problema inmultirii inlantuite a matricilor.

125
8.12 Exista un algoritm greedy evident pentru a construi arborele optim de cautare
avand cheile c1 < c2 < ... < cn: se plaseaza cheia cea mai probabila, ck, la radacina si se
construiesc subarborii sai stang si drept pentru cheile c1, c2, ..., ck-1,
respectiv, ck+1, ck+2, ..., cn, in mod recursiv, pe acelasi principiu.

i) Cat timp necesita algoritmul pentru cazul cel mai nefavorabil?


ii) Aratati pe baza unui contraexemplu ca prin acest algoritm greedy nu se obtine
intotdeauna arborele optim de cautare.

8.13 Un subcaz oarecare al problemei 0/1 a rucsacului se poate formula astfel:

Sa se gaseasca

unde maximul se ia pentru toti vectorii (xl, ..., xj) pentru care

xi  {0, 1}, l  i  j

In particular, V(1, n, G) este valoarea maxima care se poate incarca in rucsac in cazul
problemei initiale. O solutie a acestei probleme se poate obtine daca consideram ca
deciziile se iau retrospectiv, adica in ordinea xn, xn-1, ..., x1. Principiul optimalitatii este
valabil si avem

126
V(1, n, G) = max(V(1, n1, G), V(1, n1, Ggn) vn )

si, in general,

V(1, j, X) = max(V(1, j1, X), V(1, j1, Xgj ) vj)

unde V(1, 0, X) = 0 pentru X  0, iar V(1, j, X) =  pentru X < 0. De aici se poate calcula,
prin tehnica programarii dinamice, valoarea V(1, n, G) care ne intereseaza.

Gasiti o recurenta similara pentru situatia cand deciziile se iau prospectiv, adica in
ordinea x1, x2, ..., xn.

8.14 Am vazut (in Sectiunea 6.1) ca tehnica greedy poate fi aplicata in problema
determinarii restului cu un numar minim de monezi doar pentru anumite cazuri
particulare. Problema se poate rezolva, in cazul general, prin metoda programarii
dinamice.

Sa presupunem ca avem un numar finit de n tipuri de monezi, fiecare in numar nelimitat,


iar tabloul M[1 .. n] contine valoarea acestor monezi. Fie S suma pe care dorim sa o
obtinem, folosind un numar minim de monezi.

i) In tabloul C[1 .. n, 1 .. S], fie C[i, j] numarul minim de monezi necesare pentru a
obtine suma j, folosind doar monezi de tipul M[1], M[2], ..., M[i], unde C[i, j] =  daca
suma j nu poate fi obtinuta astfel. Gasiti o recurenta pentru C[i, j].
ii) Elaborati un algoritm care foloseste tehnica programarii dinamice pentru a calcula
valorile C[n, j], 1  j  S. Algoritmul trebuie sa utilizeze un singur vector de S elemente.
Care este timpul necesar, in functie de n si S?

127
iii) Gasiti un algoritm greedy care determina cum se obtine suma S cu un numar minim de
monezi, presupunand cunoscute valorile C[n, j].

8.15 Fie u si v doua secvente de caractere. Dorim sa transformam pe u in v, cu un numar


minim de operatii de urmatoarele tipuri:

sterge un caracter
adauga un caracter
schimba un caracter

De exemplu, putem sa transformam abbac in abcbc in trei etape:

abbac  abac (sterge b)


 ababc (adauga b)
 abcbc (schimba a cu c)

Aratati ca aceasta transformare nu este optima. Elaborati un algoritm de programare


dinamica care gaseste numarul minim de operatii necesare (si le specifica) pentru a-l
transforma pe u in v.

simbolul drept

a b c
simbolul a b b a
stang b c b a
c a c c

128
8.16 Sa consideram alfabetul  = {a, b, c}. Pentru elementele lui  definim urmatoarea
tabla de inmultire:

Observati ca inmultirea definita astfel nu este nici comutativa si nici asociativa. Gasiti un
algoritm eficient care examineaza sirul x = x1 x2 ... xn de caractere ale lui  si decide
daca x poate fi parantezat astfel incat expresia rezultata sa fie a. De exemplu,
daca x = bbbba, algoritmul trebuie sa returneze “da” deoarece (b(bb))(ba) = a.

8.17 Aratati ca numarul de moduri in care un poligon convex cu n laturi poate fi


partitionat in n2 triunghiuri, folosind linii diagonale care nu se intretaie, este T(n1),
unde T(n1) este al (n1)-lea numar catalan.

129
130
131

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