Documente Academic
Documente Profesional
Documente Cultură
1
Structura
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ă
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
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).
7
Dezvoltarea relațiilor de recurență
Există două abordări principale:
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]
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)
16
Cel mai lung subșir strict crescător
17
Cel mai lung subșir strict crescător
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
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)
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).
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ţă.
(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.
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.
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ă)
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
j←1
pentru i ← 1,n
scrie x[i][j]
j ←u[i][j]
sfpentru
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ă:
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)
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
ik<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
1 2 3 4 l=2
1
2
l=3
3 l=4
4
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
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
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");
47
fin>>a[i][j];
}
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");
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");
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;
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;
}
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.
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:
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).
58
INFORMATICA 5
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
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 nmp
(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, 1ijn. 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, 1ijn.
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]}
ik<j
61
INFORMATICA 7
62
8 METODA PROGRAMĂRII DINAMICE
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=(ai1ai2 ...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=(ai2ai3 ...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, aiaj, 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
3. Sumă în triunghi
Sa considerăm un triunghi format din n linii (1<n100), 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ă.
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ă XkYh 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
5. Stivă de jetoane
Un joc este constituit dintr-o stivă de n (n1000) 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 mn. Această placă
trebuie tăiată în mn bucăţi mai mici, fiecare bucată fiind un pătrat cu dimensiunile
11. Î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.
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,nN, 2m,n20
dxyN, 1dxy100
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
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
nN, 1n60
LN,1L200
giN*, gi30000, 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, SL.
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 (0SL) 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
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).
72
18 METODA PROGRAMĂRII DINAMICE
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);
}
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]),
0ij<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]; 0ij<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
78
22 METODA PROGRAMĂRII DINAMICE
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
Restricţii
1≤F≤100 unde F este numărul de buchete de flori.
F≤V≤100 unde V este numărul de vaze.
-50Aij50 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
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 (n600);
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
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, ..., k1}.
Fie Dk matricea D dupa iteratia k. Verificarea necesara este atunci:
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.
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
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.
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.
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.
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:
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
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
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:
Daca dorim sa gasim arborele optim pentru cheile c1 < c2 < ... < c5, cu probabilitatile
91
Sa notam ca C[i, i] = p[i], 1 i 5. Din relatia (*), calculam celelalte valori pentru C[i, j]:
Similar,
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[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 ji = 1, apoi
pentru ji = 2 etc. Cand ji = q, avem de calculat nq valori ale lui C[i, j], fiecare
implicand o alegere intre q1 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:
93
Figura 8.6 Un arbore optim de cautare.
Pentru exemplul precedent, obtinem astfel arborele optim din Figura 8.6.
94
template <class E>
class arbore {
// ... declaratii friend
public:
arbore( ) { root = 0; n = 0; }
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:
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.
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.
arbore<float> af;
97
template <class E>
arbore<E>& arbore<E>::re_greedy( ) {
// reorganizare prin metoda greedy
s8a<E> opt( root, n );
root = opt.greedy( );
return *this;
}
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 );
}
// date membre
tablou<varf<E>*> pvarf; // tabloul adreselor varfurilor
int n; // numarul varfurilor din arbore
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.
if ( x ) {
setvarf( poz, x->st );
pvarf[ poz++ ] = x;
setvarf( poz, x->dr );
private:
varf( const E& v, float f = 0 ): key( v )
{ st = dr = tata = 0; p = f; }
E key; // cheia
float p; // frecventa utilizarii cheii curente
};
103
template <class E>
void s8a<E>::_progDinInit( ) {
int i, j, d;
tablou< tablou<float> > C; // tabloul C este local
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.
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
106
697 3500 0,20
2037 3500 0,58
766 3500 0,22
varf<E> *y = x->tata;
while ( y != 0 && x == y->dr )
{ x = y; y = y->tata; }
return y;
}
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.
In consecinta, cele doua situatii se exclud reciproc, deci functia _succ() este corecta.
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.
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;
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).
112
n--; // in arbore va fi cu un varf mai putin
k = z->key; // k va retine intreaga informatie din z
// 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.
#include <iostream.h>
#include "arbore.h"
main( ) {
int n;
cout << "Numarul de varfuri ... "; cin >> n;
g.re_greedy( );
cout << "\n\nArborele Greedy:\n"; g.inord( );
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:
117
obtinem problema continua a rucsacului, respectiv, problema 0/1 a rucsacului. Evident,
hotul va selecta obiectele astfel incat sa maximizeze functia obiectiv
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
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.
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 n1 si demonstram pentru n.
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.
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
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 j1 do
q min(q, rminscal(i, k)rminscal(k1, j)d[i1]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( ji1) 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.
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 n1
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.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]).
Solutie:
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)
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
function tree-min(i)
while ST[i] 0 do i ST[i]
return 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.
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.
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, n1, G), V(1, n1, Ggn) vn )
si, in general,
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.
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].
sterge un caracter
adauga un caracter
schimba un caracter
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.
129
130
131