Clasa a 7-a Lecția 24: Programare dinamică (1): Difference between revisions

From Algopedia
Jump to navigationJump to search
(Created page with "== Înregistrare video lecție == {{#ev:youtube|https://www.youtube.com/watch?v=a0wFdh8DInQ|||||start=7600&end=11100&loop=1}} == Conceptul de programare dinamică == Introducere Wikipedia definește programarea dinamică drept o metodă pentru rezolvarea unor probleme complexe prin descompunerea lor în subprobleme mai simple. Se poate aplica problemelor care prezintă proprietățile de suprapunere a subproblemelor și de substructură optimală. Atunci când s...")
 
No edit summary
Line 5: Line 5:


== Conceptul de programare dinamică ==
== Conceptul de programare dinamică ==
Introducere


Wikipedia definește programarea dinamică drept o metodă pentru rezolvarea unor probleme complexe prin descompunerea lor în subprobleme mai simple. Se poate aplica problemelor care prezintă proprietățile de suprapunere a subproblemelor și de substructură optimală. Atunci când se poate aplica, metoda ia mult mai puțin timp decât alte soluții naive.
=== Introducere ===


Substructura optimală înseamnă că soluția unei problemei de optimizare date poate fi obținută prin combinarea unor soluții optimale ale subproblemelor. Ca atare, primul pas către găsirea unei rezolvări prin această metodă este verificarea dacă problema are proprietatea substructurii optimale.  
Wikipedia definește [http://en.wikipedia.org/wiki/Dynamic_programming programarea dinamică] drept o metodă pentru rezolvarea unor probleme complexe prin descompunerea lor în subprobleme mai simple. Se poate aplica problemelor care prezintă proprietățile de ''suprapunere a subproblemelor'' și de ''substructură optimală''. Atunci când se poate aplica, metoda ia mult mai puțin timp decât alte soluții naive.
 
[[Image:Shortest_path_optimal_substructure.svg|frame|right|300px|Drumul cel mai scurt între două orașe poate fi rezolvat cu programare dinamică]]
 
''Substructura optimală'' înseamnă că soluția unei problemei de optimizare date poate fi obținută prin combinarea unor soluții optimale ale subproblemelor. Ca atare, primul pas către găsirea unei rezolvări prin această metodă este verificarea dacă problema are proprietatea substructurii optimale.  


Drumul cel mai scurt între două orașe poate fi rezolvat cu programare dinamică
Drumul cel mai scurt între două orașe poate fi rezolvat cu programare dinamică


Astfel de substructuri optimale sunt în mod uzual descrise prin recurență. De exemplu, date niște orașe și distanțele între ele, drumul cel mai scurt de la orașul u la orașul v are proprietatea de substructură optimală: să luăm orice oraș intermediar w pe drumul cel mai scurt, d. Dacă d este cu adevărat drumul cel mai scurt, atunci drumul d1 de la u la w și d2 de la w la v sunt cu adevărat cele mai scurte drumuri între orașele corespunzătoare. Dacă drumul de la u la w nu ar fi cel mai scurt posibil ar însemna că am avea un alt drum mai scurt, ceea ce ar duce la un drum total d, de la u la v, mai scurt decât cel original. Așadar, putem formula soluția problemei găsirii celui mai scurt drum într-o manieră recursivă, ceea ce fac algoritmii Bellman-Ford și Floyd-Warshall.
Astfel de substructuri optimale sunt în mod uzual descrise prin recurență. De exemplu, date niște orașe și distanțele între ele, drumul cel mai scurt de la orașul <tt>u</tt> la orașul <tt>v</tt> are proprietatea de substructură optimală: să luăm orice oraș intermediar <tt>w</tt> pe drumul cel mai scurt, <tt>d</tt>. Dacă <tt>d</tt> este cu adevărat drumul cel mai scurt, atunci drumul <tt>d1</tt> de la <tt>u</tt> la <tt>w</tt> și <tt>d2</tt> de la <tt>w</tt> la <tt>v</tt> sunt cu adevărat cele mai scurte drumuri între orașele corespunzătoare. Dacă drumul de la <tt>u</tt> la <tt>w</tt> nu ar fi cel mai scurt posibil ar însemna că am avea un alt drum mai scurt, ceea ce ar duce la un drum total <tt>d</tt>, de la <tt>u</tt> la <tt>v</tt>, mai scurt decât cel original. Așadar, putem formula soluția problemei găsirii celui mai scurt drum într-o manieră recursivă, ceea ce fac algoritmii [https://en.wikipedia.org/wiki/Bellman-Ford_algorithm Bellman-Ford] și [https://en.wikipedia.org/wiki/Floyd-Warshall_algorithm Floyd-Warshall].


Suprapunerea subproblemelor înseamnă că ele nu sunt independente, adică subproblemele au în comun alte subprobleme. Nu vom insista pe această proprietate decât atât cât să menționăm că dacă subproblemele nu se suprapun dar prezintă substructură optimală atunci putem aplica tehnica de programare divide et impera (divide and conquer). De aceea quicksort și mergesort nu sunt clasificate ca probleme de programare dinamică.
Suprapunerea subproblemelor înseamnă că ele nu sunt independente, adică subproblemele au în comun alte subprobleme. Nu vom insista pe această proprietate decât atât cât să menționăm că dacă subproblemele nu se suprapun dar prezintă substructură optimală atunci putem aplica tehnica de [https://www.algopedia.ro/wiki/index.php/Clasa_a_7-a_Lec%C8%9Bia_18:_Divide_et_impera,_mergesort,_quicksort programare divide et impera (divide and conquer)]. De aceea [http://en.wikipedia.org/wiki/Quicksort quicksort] și [http://en.wikipedia.org/wiki/Mergesort mergesort] nu sunt clasificate ca probleme de programare dinamică.


Metodă


Când încercăm să rezolvăm o problemă de programare dinamică întrebarea crucială la care trebuie să răspundem este putem calcula soluția finală pe baza unor subprobleme care sunt la rândul lor optimale? Pentru a răspunde la întrebare trebuie mai întâi să formulăm subproblemele. Dar pentru a formula subproblemele trebuie să avem în minte principiul substructurii optimale. Altfel s-ar putea să împărțim problema în subprobleme care nu au substructură optimală.
=== Metodă ===
 
Când încercăm să rezolvăm o problemă de programare dinamică întrebarea crucială la care trebuie să răspundem este ''putem calcula soluția finală pe baza unor subprobleme care sunt la rândul lor '''optimale''' ''? Pentru a răspunde la întrebare trebuie mai întâi să formulăm subproblemele. Dar pentru a formula subproblemele trebuie să avem în minte principiul substructurii optimale. Altfel s-ar putea să împărțim problema în subprobleme care nu au substructură optimală.


Să recapitulăm: pentru a putea răspunde la întrebarea dacă o problemă admite o soluție prin programare dinamică trebuie să aflăm dacă are substructură optimală. Dar pentru a afla dacă are substructură optimală trebuie să găsim subproblemele. Pe care încercăm să le găsim folosind programarea dinamică.
Să recapitulăm: pentru a putea răspunde la întrebarea dacă o problemă admite o soluție prin programare dinamică trebuie să aflăm dacă are substructură optimală. Dar pentru a afla dacă are substructură optimală trebuie să găsim subproblemele. Pe care încercăm să le găsim folosind programarea dinamică.
Line 28: Line 32:


În continuare vom lua câteva exemple.
În continuare vom lua câteva exemple.


== Subsecvența crescătoare de lungime maximă ==
== Subsecvența crescătoare de lungime maximă ==
Această problemă cere să găsim o subsecvență a unei secvențe de numere în care elementele sunt în ordine crescătoare, iar subsecvența este de lungime maximă. Subsecvența nu este neapărat contiguă, nici unică.
Această problemă cere să găsim o subsecvență a unei secvențe de numere în care elementele sunt în ordine crescătoare, iar subsecvența este de lungime maximă. Subsecvența nu este neapărat contiguă, nici unică.


Line 36: Line 42:
În secvența
În secvența


0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15


o secvență crescătoare de lungime maximă este
o secvență crescătoare de lungime maximă este


0, 2, 6, 9, 13, 15
0, 2, 6, 9, 13, 15


Această secvență are lungime șase. Nu există nici o secvență crescătoare de lungime șapte. Cea mai lungă subsecvență crescătoare nu este unică. De exemplu
Această secvență are lungime șase. Nu există nici o secvență crescătoare de lungime șapte. Cea mai lungă subsecvență crescătoare nu este unică. De exemplu


0, 4, 6, 9, 11, 15
0, 4, 6, 9, 11, 15


este o altă subsecvență crescătoare de aceeași lungime, în secvența de intrare.
este o altă subsecvență crescătoare de aceeași lungime, în secvența de intrare.


Soluție
 
=== Soluție ===


Cum împărțim problema în subprobleme? Să încercăm astfel:
Cum împărțim problema în subprobleme? Să încercăm astfel:


L[i] = lungimea celei mai lungi subsecvențe care începe la poziția i
L[i] = lungimea celei mai lungi subsecvențe care începe la poziția i


Are această împărțire proprietatea de optimalitate? Să presupunem că avem o subsecvență optimală crescătoare care trece prin indicii
Are această împărțire proprietatea de optimalitate? Să presupunem că avem o subsecvență optimală crescătoare care trece prin indicii


i1, i2, i3, …, ik
i<sub>1</sub>, i<sub>2</sub>, i<sub>3</sub>, …, i<sub>k</sub>


Astfel
Astfel


L[i1] = k
L[i<sub>1</sub>] = k


Este secvența i2, ..., ik optimală? Cu alte cuvinte lungimea secvenței optime care începe la i2 este k-1? Răspunsul este da, secvența care începe la i2, anume i2, ..., ik este o secvență maximă, deoarece altfel am putea-o înlocui cu o secvență mai lungă, ceea ce ar duce la o subsecvență L[i1] mai lungă decât cea inițială, care era optimală. Obținem o contradicție.
Este secvența <tt>i<sub>2</sub>, ..., i<sub>k</sub></tt> optimală? Cu alte cuvinte lungimea secvenței optime care începe la <tt>i<sub>2</sub></tt> este <tt>k-1</tt>? Răspunsul este da, secvența care începe la i<sub>2</sub>, anume i<sub>2</sub>, ..., i<sub>k</sub> este o secvență maximă, deoarece altfel am putea-o înlocui cu o secvență mai lungă, ceea ce ar duce la o subsecvență <tt>L[i<sub>1</sub>]</tt> mai lungă decât cea inițială, care era optimală. Obținem o contradicție.


Următorul pas este să găsim relația de recurență:
Următorul pas este să găsim relația de recurență:


L[i] = max(L[j]+1), j > i și V[j] ≥ V[i]
L[i] = max(L[j]+1), j > i și V[j] ≥ V[i]


Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula vectorul L de la coadă la cap, căutând pentru fiecare element L[i] cel mai mare element L[j] de după el (j > i) astfel încât V[j] ≥ V[i].
Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula vectorul <source lang="C" enclose="none">L<source lang="C" enclose="none"> de la coadă la cap, căutând pentru fiecare element <source lang="C" enclose="none">L[i]<source lang="C" enclose="none"> cel mai mare element <source lang="C" enclose="none">L[j]<source lang="C" enclose="none"> de după el <tt>(j > i)</tt> astfel încât <source lang="C" enclose="none">V[j] ≥ V[i]<source lang="C" enclose="none">.


Soluția problemei este maximul din vectorul L.
Soluția problemei este maximul din vectorul <source lang="C" enclose="none">L</source>.


Exemplu
 
=== Exemplu ===


Să calculăm cea mai lungă secvență crescătoare în secvența:
Să calculăm cea mai lungă secvență crescătoare în secvența:


0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15


Vom calcula lungimile secvențelor maximale de la coadă la cap: L[15], apoi L[14] și așa mai departe. Când ajungem la calculul lui L[6] avem următoarea situație:
Vom calcula lungimile secvențelor maximale de la coadă la cap: <source lang="C" enclose="none">L[15]</source>, apoi <source lang="C" enclose="none">L[14]</source> și așa mai departe. Când ajungem la calculul lui <tt>L[6]</tt> avem următoarea situație:


Pentru a calcula lungimea secvenței maximale care începe la poziția 6 vom căuta toate pozițiile mai mari (7, 8, 9, ...). Le vom lua în considerare doar pe acelea în care V[i] >= V[6]. Dintre ele vom alege poziția unde L[i] este maximă. Astfel vom găsi că V[9] = 9 și L[9] = 3. Vom avea, deci, o secvență de lungime 4. Ea este:
[[File:max-increasing-seq.png|frame|none|Exemplu de calcul pentru elementul ''L[6]'' - cea mai lungă secvență crescătoare care începe la poziția 6.]]


6, 9, 11, 15
Pentru a calcula lungimea secvenței maximale care începe la poziția 6 vom căuta toate pozițiile mai mari <tt>(7, 8, 9, ...)</tt>. Le vom lua în considerare doar pe acelea în care <source lang="C" enclose="none">V[i] >= V[6]</source>. Dintre ele vom alege poziția unde <source lang="C" enclose="none">L[i]</source> este maximă. Astfel vom găsi că <source lang="C" enclose="none">V[9] = 9</source> și <source lang="C" enclose="none">L[9] = 3</source>. Vom avea, deci, o secvență de lungime 4. Ea este:


Exemplu de calcul pentru elementul L[6] - cea mai lungă secvență crescătoare care începe la poziția 6.
6, 9, 11, 15


Analiză
Exemplu de calcul pentru elementul <source lang="C" enclose="none">L[6]</source> - cea mai lungă secvență crescătoare care începe la poziția 6.


Acest algoritm are complexitate O(n2), unde n este lungimea secvenței. Memoria folosită este O(n).


Există și un algoritm mai rapid, de complexitate O(n log n), bazat pe căutare binară a valorilor de început.
=== Analiză ===


Reconstrucția soluției
Acest algoritm are complexitate ''O(n<sup>2</sup>)'', unde n este lungimea secvenței. Memoria folosită este ''O(n)''.
 
Există și un algoritm mai rapid, de complexitate ''O(n log n)'', bazat pe căutare binară a valorilor de început.
 
 
=== Reconstrucția soluției ===
 
Putem reconstrui o subsecvență maximală folosind încă un vector, <source lang="C" enclose="none">N[]</source>. El memorează pentru fiecare indice <tt>i</tt> un alt indice <tt>j</tt> care urmează în secvență după <tt>j</tt>. Atunci când calculăm elementul curent, vom nota nu doar maximul găsit (plus unu) ci și poziția lui în vector, el fiind elementul următor în secvență.


Putem reconstrui o subsecvență maximală folosind încă un vector, N[]. El memorează pentru fiecare indice i un alt indice j care urmează în secvență după j. Atunci când calculăm elementul curent, vom nota nu doar maximul găsit (plus unu) ci și poziția lui în vector, el fiind elementul următor în secvență.


== Subșirul de sumă maximală ==
== Subșirul de sumă maximală ==
Această problemă cere să găsim un subșir de sumă maximă într-un șir de numere. Un subșir este contiguu.
Această problemă cere să găsim un subșir de sumă maximă într-un șir de numere. Un subșir este contiguu.


Exemplu
 
=== Exemplu ===


Să considerăm șirul:
Să considerăm șirul:


12, 14, 0, -4, 61, -39
12, 14, 0, -4, 61, -39


subșirul de sumă maximală este:
subșirul de sumă maximală este:


12, 14, 0, -4, 61
12, 14, 0, -4, 61


și are sumă 83.
și are sumă 83.


Soluție
 
=== Soluție ===


Cum împărțim problema în subprobleme? Să încercăm astfel:
Cum împărțim problema în subprobleme? Să încercăm astfel:


S[i] este suma maximală a unui șir care se termină la poziția i
S[i] este suma maximală a unui șir care se termină la poziția i


Are această împărțire proprietatea de optimalitate?
Are această împărțire proprietatea de optimalitate?


Să presupunem că avem un subșir de sumă maximă și că el se află între indicii i și j. Cu alte cuvine suma maximală este
Să presupunem că avem un subșir de sumă maximă și că el se află între indicii <tt>i</tt> și <tt>j</tt>. Cu alte cuvine suma maximală este


S[j] = V[i] + V[i+1] + ... + V[j-1] + V[j]
S[j] = V[i] + V[i+1] + ... + V[j-1] + V[j]


Are șirul care se termină în j-1, sumă maximală? Cu alte cuvinte este suma
Are șirul care se termină în <tt>j-1</tt>, sumă maximală? Cu alte cuvinte este suma


S[j-1] = V[i] + V[i+1] + ... + V[j-1]
S[j-1] = V[i] + V[i+1] + ... + V[j-1]


maximală?
maximală?
Line 131: Line 146:
Răspunsul este da, deoarece altfel am putea-o înlocui cu o sumă mai mare, ceea ce ar duce la o sumă mai mare decît cea inițială, care era optimală. Obținem o contradicție.
Răspunsul este da, deoarece altfel am putea-o înlocui cu o sumă mai mare, ceea ce ar duce la o sumă mai mare decît cea inițială, care era optimală. Obținem o contradicție.


Următorul pas este să găsim relația de recurență. Observăm că pentru o poziție i avem două variante: fie continuăm secvența anterioară, care se termină în i-1, fie începem una nouă, care pornește în i. Vom continua secvența anterioară dacă adunând numărul curent obținem o sumă mai mare strict decât numărul curent. Altfel vom porni o sumă nouă. Relația de recurență este:
Următorul pas este să găsim relația de recurență. Observăm că pentru o poziție <tt>i</tt> avem două variante: fie continuăm secvența anterioară, care se termină în <tt>i-1</tt>, fie începem una nouă, care pornește în <tt>i</tt>. Vom continua secvența anterioară dacă adunând numărul curent obținem o sumă mai mare strict decât numărul curent. Altfel vom porni o sumă nouă. Relația de recurență este:


S[i] = max(V[i], S[i-1] + V[i])
S[i] = max(V[i], S[i-1] + V[i])


Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula vectorul S calculînd pentru fiecare element S[i] maximul dintre S[i-1] + V[i] și V[i]. Observăm că, în realitate, vom compara S[i-1] cu zero.
Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula vectorul <source lang="C" enclose="none">S</source> calculând pentru fiecare element <source lang="C" enclose="none">S[i]</source> maximul dintre <source lang="C" enclose="none">S[i-1] + V[i]</source> și <source lang="C" enclose="none">V[i]</source>. Observăm că, în realitate, vom compara <source lang="C" enclose="none">S[i-1]</source> cu zero.


Soluția problemei este maximul din vectorul S.
Soluția problemei este maximul din vectorul <source lang="C" enclose="none">S</source>.


Exemplu
 
=== Exemplu ===


Fie secvența anterioară:
Fie secvența anterioară:


12, 14, 0, -4, 61, -39
12, 14, 0, -4, 61, -39


Vom calcula de la stânga la dreapta sumele maximale ce se termină la pozițiile respective:
Vom calcula de la stânga la dreapta sumele maximale ce se termină la pozițiile respective:


V12140-461-39S1226 =  
{| class="wikitable"
max(14, 12 + 14)26 =  
! V
max(0, 0 + 26)22 =  
| 12
max(-4, -4 + 26)83 =  
| 14
max(61, 61 + 22)44 =  
| 0
max(-39, -39 + 83)
| -4
| 61
| -39
|-
! S
| 12
| 26 = max(14, 12 + 14)
| 26 = max(0, 0 + 26)
| 22 = max(-4, -4 + 26)
| 83 = max(61, 61 + 22)
| 44 = max(-39, -39 + 83)
|}
 
 
=== Analiză ===


Analiză
Acest algoritm are complexitate ''O(n)'', unde n este lungimea șirului. Memoria folosită este ''O(1)'', deoarece nu avem nevoie să stocăm elementele, ci doar ultima sumă calculată.


Acest algoritm are complexitate O(n), unde n este lungimea șirului. Memoria folosită este O(1), deoarece nu avem nevoie să stocăm elementele, ci doar ultima sumă calculată.


Reconstrucția soluției
=== Reconstrucția soluției ===


Putem ține minte șirul maximal ținând minte începutul și sfârșitul subșirului maximal atunci când îl găsim.
Putem ține minte șirul maximal ținând minte începutul și sfârșitul subșirului maximal atunci când îl găsim.


Soluții alternative


Această problemă poate fi rezolvată fără programare dinamică. Ne propunem același lucru, ca la pasul i să calculăm S[i] cu aceeași semnificație. Acest S[i] se poate calcula cu cunoștințe de nivel mic: sume parțiale. S[i] va fi suma unei secvențe de la k la i, cu k ≤ i. Deci, dacă vom calcula P[], sumele parțiale pe vector putem spune că S[i] = P[i] - P[k]. Să ne uităm puțin la această expresie: cine variază în ea? P[i] este constant, este suma elementelor până la i. Putem varia doar P[k]. Este evident că dorim să alegem un P[k] cât mai mic, nu?
=== Soluții alternative ===
 
Această problemă poate fi rezolvată fără programare dinamică. Ne propunem același lucru, ca la pasul <tt>i</tt> să calculăm <source lang="C" enclose="none">S[i]</source> cu aceeași semnificație. Acest <source lang="C" enclose="none">S[i]</source> se poate calcula cu cunoștințe de nivel mic: sume parțiale. <source lang="C" enclose="none">S[i]</source> va fi suma unei secvențe de la <tt>k</tt> la <tt>i</tt>, cu <tt>k ≤ i</tt>. Deci, dacă vom calcula <source lang="C" enclose="none">P[]</source>, sumele parțiale pe vector putem spune că <source lang="C" enclose="none">S[i] = P[i] - P[k]</source>. Să ne uităm puțin la această expresie: cine variază în ea? <source lang="C" enclose="none">P[i]</source> este constant, este suma elementelor până la <tt>i</tt>. Putem varia doar <source lang="C" enclose="none">P[k]</source>. Este evident că dorim să alegem un <source lang="C" enclose="none">P[k]</source> cât mai mic, nu?


Algoritmul este unul banal: calculează sume parțiale, avansând i. Suma minimă care se termină în i este suma parțială curentă minus suma parțială minimă din trecut. La fiecare avans actualizează suma parțială minimă cu cea curentă, dacă este cazul.
Algoritmul este unul banal: calculează sume parțiale, avansând <tt>i</tt>. Suma minimă care se termină în <tt>i</tt> este suma parțială curentă minus suma parțială minimă din trecut. La fiecare avans actualizează suma parțială minimă cu cea curentă, dacă este cazul.
 
Acest algoritm are aceleași complexități ca cel cu programare dinamică. El este însă un algoritm mai flexibil de adaptat unor modificări, cum ar fi suma maximă de lungime cel puțin <tt>K</tt> sau suma maximă de lungime cel mult <tt>K</tt>.


Acest algoritm are aceleași complexități ca cel cu programare dinamică. El este însă un algoritm mai flexibil de adaptat unor modificări, cum ar fi suma maximă de lungime cel puțin K sau suma maximă de lungime cel mult K.


== Subșirul comun maximal al două șiruri ==
== Subșirul comun maximal al două șiruri ==
Această problemă cere să găsim lungimea unui subșir de lungime maximă (contiguu) inclus în două șiruri.
Această problemă cere să găsim lungimea unui subșir de lungime maximă (contiguu) inclus în două șiruri.


Exemplu
 
=== Exemplu ===


Date șirurile
Date șirurile


ABABC
ABABC


și
și


CBABA
CBABA


un subșir maximal are lungime 3 și este
un subșir maximal are lungime 3 și este


BAB
BAB
 


Soluție
=== Soluție ===


Cum împărțim problema în subprobleme? Să observăm că deoarece subșirul maximal face parte din ambele șiruri el se va termina la o poziție i în șirul X și la o poziție j în șirul Y. Și atunci să încercăm astfel:
Cum împărțim problema în subprobleme? Să observăm că deoarece subșirul maximal face parte din ambele șiruri el se va termina la o poziție <tt>i</tt> în șirul <tt>X</tt> și la o poziție <tt>j</tt> în șirul <tt>Y</tt>. Și atunci să încercăm astfel:


M[i][j] = lungimea celui mai lung subșir care se termină la poziția i în X și la poziția j în Y.
M[i][j] = lungimea celui mai lung subșir care se termină la poziția i în X și la poziția j în Y.


Are această împărțire proprietatea de optimalitate?
Are această împărțire proprietatea de optimalitate?


Să presupunem că avem un subșir optimal care se termină la pozițiile i, respectiv j. Atunci este subșirul M[i-1][j-1] la rândul lui optimal? Cu alte cuvinte subșirul mai scurt cu unu, care se termină la pozițiile anterioare în X, respectiv Y, este optimal? Da, el este optimal, caci altfel l-am putea înlocui cu un șir mai lung, care ar duce la o soluție mai bună, ceea ce ar însemna că soluția originală nu a fost optimă.
Să presupunem că avem un subșir optimal care se termină la pozițiile <tt>i</tt>, respectiv <tt>j</tt>. Atunci este subșirul <tt>M[i-1][j-1]</tt> la rândul lui optimal? Cu alte cuvinte subșirul mai scurt cu unu, care se termină la pozițiile anterioare în <tt>X</tt>, respectiv <tt>Y</tt>, este optimal? Da, el este optimal, caci altfel l-am putea înlocui cu un șir mai lung, care ar duce la o soluție mai bună, ceea ce ar însemna că soluția originală nu a fost optimă.


Următorul pas este să găsim relația de recurență:
Următorul pas este să găsim relația de recurență:


\( M[i][j] =\begin{cases}M[i-1][j-1]+1, & \text{dacă X[i]=Y[j]} \\ 0, & \text{în caz contrar} \end{cases} \)
:<math>M[i][j] = \begin{cases} M[i-1][j-1]+1, & \mbox{dacă X[i] = Y[j] \\0, & \mbox{în caz contrar }. \end{cases}</math>


Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula matricea M pe linii, folosind formula de recurență.
Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula matricea <source lang="C" enclose="none">M</source> pe linii, folosind formula de recurență.


Soluția problemei este maximul din matrice.
Soluția problemei este maximul din matrice.


Exemplu
 
=== Exemplu ===


Pentru cele două șiruri de mai sus,
Pentru cele două șiruri de mai sus,


ABABC
ABABC


și
și


CBABA
CBABA


Vom calcula o matrice M[5][5] care arată astfel:
Vom calcula o matrice <source lang="C" enclose="none">M[5][5]</source> care arată astfel:


CBABAA00101B01020A00203B01030C10000
{| class="wikitable"
!
! C
! B
! A
! B
! A
|-
! A
| 0
| 0
| 1
| 0
| 1
|-
! B
| 0
| 1
| 0
| 2
| 0
|-
! A
| 0
| 0
| 2
| 0
| 3
|-
! B
| 0
| 1
| 0
| 3
| 0
|-
! C
| 1
| 0
| 0
| 0
| 0
|}


Observăm că avem două subșiruri comune de lungime 3: ABA și BAB.
Observăm că avem două subșiruri comune de lungime 3: ABA și BAB.


Analiză


Acest algoritm are complexitate O(m·n), unde m și n sunt lungimile șirurilor. Memoria folosită este, la prima vedere, O(m·n). În realitate, parcurgând matricea pe linii, nu avem nevoie să memorăm decât o linie din matrice pentru a calcula toate valorile. De aceea memoria necesară este O(min(m, n)).
=== Analiză ===
 
Acest algoritm are complexitate ''O(m·n)'', unde <tt>m</tt> și <tt>n</tt> sunt lungimile șirurilor. Memoria folosită este, la prima vedere, ''O(m·n)''. În realitate, parcurgând matricea pe linii, nu avem nevoie să memorăm decât o linie din matrice pentru a calcula toate valorile. De aceea memoria necesară este ''O(min(m, n))''.


Există și algoritmi mai eficienți bazați pe arbori sufix, despre care nu vom discuta aici.
Există și algoritmi mai eficienți bazați pe arbori sufix, despre care nu vom discuta aici.


Reconstrucția soluției
 
=== Reconstrucția soluției ===


Cum facem să aflăm nu doar lungimea ci și care este șirul comun maximal și unde apare el în cele două șiruri?
Cum facem să aflăm nu doar lungimea ci și care este șirul comun maximal și unde apare el în cele două șiruri?


Vom face calculul matricei noastre printr-o parcurgere pe linii. La fiecare pas ne vom întreba dacă elementul calculat (lungimea) este mai mare decât maximul curent. Dacă da, vom memora acel maxim împreună cu indicii i și j la care s-a obținut el.
Vom face calculul matricei noastre printr-o parcurgere pe linii. La fiecare pas ne vom întreba dacă elementul calculat (lungimea) este mai mare decât maximul curent. Dacă da, vom memora acel maxim împreună cu indicii <tt>i</tt> și <tt>j</tt> la care s-a obținut el.


La final vom avea poziția de start în primul șir ca fiind i - max + 1 și în al doilea șir ca fiind j - max + 1.
La final vom avea poziția de start în primul șir ca fiind <tt>i - max + 1</tt> și în al doilea șir ca fiind <tt>j - max + 1</tt>.


[mathjax]


== Subsecvența comună maximală a două șiruri ==
== Subsecvența comună maximală a două șiruri ==
Această problemă cere să găsim lungimea unei subsecvențe de lungime maximă inclusă în două secvențe. Prin secvență înțelegem caractere în ordinea originală, nu neapărat consecutive ca poziție.
Această problemă cere să găsim lungimea unei subsecvențe de lungime maximă inclusă în două secvențe. Prin secvență înțelegem caractere în ordinea originală, nu neapărat consecutive ca poziție.


Exemplu
 
=== Exemplu ===


Date secvențele
Date secvențele


X: ACDEGIKMPR
X: ACDEGIKMPR
Y: CMEFPGHRK
Y: CMEFPGHRK


o secvență maximală are lungime 4 și este
o secvență maximală are lungime 4 și este


CEGK
CEGK


o altă secvență maximală este
o altă secvență maximală este


CMPR
CMPR


Soluție
 
=== Soluție ===


Cum împărțim problema în subprobleme? Să încercăm să o împărțim în mod similar cu subșirul comun maximal:
Cum împărțim problema în subprobleme? Să încercăm să o împărțim în mod similar cu subșirul comun maximal:


M[i][j] = lungimea celei mai lungi subsecvențe incluse în șirurile X[1..i] și Y[1..j]
M[i][j] = lungimea celei mai lungi subsecvențe incluse în șirurile X[1..i] și Y[1..j]


Are această împărțire proprietatea de optimalitate?
Are această împărțire proprietatea de optimalitate?


Să presupunem că avem o secvență optimală a prefixelor care se termină la pozițiile i, respectiv j. Ea va trece prin anumite perechi (ik, jk), strict crescătoare, cu proprietatea că X[ik] = Y[jk]:
Să presupunem că avem o secvență optimală a prefixelor care se termină la pozițiile <tt>i</tt>, respectiv <tt>j</tt>. Ea va trece prin anumite perechi <tt>(i<sub>k</sub>, j<sub>k</sub>)</tt>, strict crescătoare, cu proprietatea că <tt>X[i<sub>k</sub>] = Y[j<sub>k</sub>]</tt>:


Si,j = (i1, j1) (i2, j2) ... (ik, jk)
Si,j = (i1, j1) (i2, j2) ... (ik, jk)
Line 269: Line 350:
cu proprietatea că:
cu proprietatea că:


Xi1Xi2... Xik-1Xik = Yj1Yj2... Yjk-1Yjk
X<sub>i<sub>1</sub></sub>X<sub>i<sub>2</sub></sub>... X<sub>i<sub>k-1</sub></sub>X<sub>i<sub>k</sub></sub> = Y<sub>j<sub>1</sub></sub>Y<sub>j<sub>2</sub></sub>... Y<sub>j<sub>k-1</sub></sub>Y<sub>j<sub>k</sub></sub>


Atunci este lungimea M[ik-1][jk-1] la rândul ei optimală? Cu alte cuvinte subsecvența mai scurtă cu unu, care se termină la poziția ik-1 în X, respectiv poziția jk-1 în Y, este optimală? Da, ea este optimală, căci altfel am putea-o înlocui cu o secvență mai lungă, care ar duce la o soluție mai bună, ceea ce ar însemna că soluția originală nu a fost optimă.
Atunci este lungimea <tt>M[i<sub>k-1</sub>][j<sub>k-1</sub>]</tt> la rândul ei optimală? Cu alte cuvinte subsecvența mai scurtă cu unu, care se termină la poziția <tt>i<sub>k-1</sub></tt> în <tt>X</tt>, respectiv poziția <tt>j<sub>k-1</sub></tt> în <tt>Y</tt>, este optimală? Da, ea este optimală, căci altfel am putea-o înlocui cu o secvență mai lungă, care ar duce la o soluție mai bună, ceea ce ar însemna că soluția originală nu a fost optimă.


Următorul pas este să găsim relația de recurență:
Următorul pas este să găsim relația de recurență:


\( M[i][j] =\begin{cases}0, & \text{dacă i = 0 sau j = 0} \\ M[i-1][j-1]+1, & \text{dacă X[i]=Y[j]} \\ max(M[i-1][j], M[i][j-1]), & \text{dacă X[i] Y[j]} \end{cases} \)
<math>M[i][j] = \begin{cases} 0, & \mbox{dacă i = 0 \mbox{ sau } j = 0\\M[i-1][j-1]+1, & \mbox{dacă } X[i] = Y[j]\\max(M[i-1][j],M[i][j-1]) & \mbox{dacă X[i] \neq Y[j]. \end{cases}</math>


Cum demonstrăm această relație de recurență? Avem două cazuri de demonstrat:
Cum demonstrăm această relație de recurență? Avem două cazuri de demonstrat:


Egalitate


Prefixele X[1..i] și Y[1..j] se termină în același caracter. În acest caz o subsecvență maximală a celor două prefixe se poate obține selectând acel ultim caracter. Putem, deci exclude acel caracter și calcula subsecvența maximală în prefixele X[1..i-1] și Y[1..j-1]. La lungimea ei vom aduna unu.
=== Egalitate ===


Diferență
Prefixele <tt>X[1..i]</tt> și <tt>Y[1..j]</tt> se termină în același caracter. În acest caz o subsecvență maximală a celor două prefixe se poate obține selectând acel ultim caracter. Putem, deci exclude acel caracter și calcula subsecvența maximală în prefixele <tt>X[1..i-1]</tt> și <tt>Y[1..j-1]</tt>. La lungimea ei vom aduna unu.


Prefixele X[1..i] și Y[1..j] se termină în caractere diferite. Să luăm cazul anterior, avem secvențele:


X: ACDEGIKMPR
=== Diferență ===
Y: CMEFPGHRK
 
Prefixele <tt>X[1..i]</tt> și <tt>Y[1..j]</tt> se termină în caractere diferite. Să luăm cazul anterior, avem secvențele:
 
X: ACDEGIKMPR
Y: CMEFPGHRK


Avem iarăși două cazuri:
Avem iarăși două cazuri:


Cazul 1: subsecvența optimă se termină în R. În acest caz ea nu se poate termina și în K. Deci putem elimina ultimul caracter din Y și calcula M[i][j-1].
* '''Cazul 1''': subsecvența optimă se termină în <tt>R</tt>. În acest caz ea nu se poate termina și în <tt>K</tt>. Deci putem elimina ultimul caracter din <tt>Y</tt> și calcula <tt>M[i][j-1]</tt>.
* '''Cazul 2''': subsecvența optimă nu se termină în <tt>R</tt>. În acest caz putem elimina acest <tt>R</tt> și calcula <tt>M[i-1][j]</tt>.


Cazul 2: subsecvența optimă nu se termină în R. În acest caz putem elimina acest R și calcula M[i-1][j].
Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula matricea <source lang="C" enclose="none">M</source> pe linii, folosind formula de recurență.


Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula matricea M pe linii, folosind formula de recurență.
Soluția problemei este <source lang="C" enclose="none">M[n][m]</source>.


Soluția problemei este M[n][m].


Exemplu
=== Exemplu ===


Pentru cele două șiruri de mai sus,
Pentru cele două șiruri de mai sus,


ACDEGIKMPR
ACDEGIKMPR


și
și


CMEFPGHRK
CMEFPGHRK


Vom calcula o matrice M[10][9] care arată astfel:
Vom calcula o matrice <source lang="C" enclose="none">M[10][9]</source> care arată astfel:


CMEFPGHRKA000000000C111111111D111111111E112222222G112223333I112223333K112223334M122223334P122233334R122233344
{| class="wikitable"
!
! C !! M !! E !! F !! P !! G !! H !! R !! K
|-
!A
|0||0||0||0||0||0||0||0||0
|-
!C
|1||1||1||1||1||1||1||1||1
|-
!D
|1||1||1||1||1||1||1||1||1
|-
!E
|1||1||2||2||2||2||2||2||2
|-
!G
|1||1||2||2||2||3||3||3||3
|-
!I
|1||1||2||2||2||3||3||3||3
|-
!K
|1||1||2||2||2||3||3||3||4
|-
!M
|1||2||2||2||2||3||3||3||4
|-
!P
|1||2||2||2||3||3||3||3||4
|-
!R
|1||2||2||2||3||3||3||4||4
|}


Cea mai lungă secvență este de lungime 4, valoarea maximă ce apare în tabel.
Cea mai lungă secvență este de lungime 4, valoarea maximă ce apare în tabel.


Analiză


Acest algoritm are complexitate O(m · n), unde m și n sunt lungimile șirurilor. Memoria folosită este O(min(m,n)) dacă memorăm doar o linie din matrice. Dacă dorim să aflăm și o secvență maximală, nu doar lungimea ei, vom avea nevoie de memorie O(m · n), precum vom vedea mai jos.
=== Analiză ===
 
Acest algoritm are complexitate ''O(m · n)'', unde <tt>m</tt> și <tt>n</tt> sunt lungimile șirurilor. Memoria folosită este ''O(min(m,n))'' dacă memorăm doar o linie din matrice. Dacă dorim să aflăm și o secvență maximală, nu doar lungimea ei, vom avea nevoie de memorie ''O(m · n)'', precum vom vedea mai jos.
 
Există un algoritm care reduce necesarul de memorie la ''O(min(m, n))''.


Există un algoritm care reduce necesarul de memorie la O(min(m, n)).


Reconstrucția soluției
=== Reconstrucția soluției ===


Dorim să aflăm nu doar lungimea, ci și o secvență maximală inclusă în ambele șiruri. Cum procedăm?
Dorim să aflăm nu doar lungimea, ci și o secvență maximală inclusă în ambele șiruri. Cum procedăm?
Line 328: Line 446:
Observăm că literele ce fac parte din secvențele maximale se obțin acolo unde, în matrice, avem ramura doi de calcul, când literele de la pozițiile respective sunt egale.
Observăm că literele ce fac parte din secvențele maximale se obțin acolo unde, în matrice, avem ramura doi de calcul, când literele de la pozițiile respective sunt egale.


Pentru a putea reconstrui soluția vom memora în fiecare căsuță, pe lângă lungimea maximă și direcția din care provine ea, adică din care am calculat acel maxim. Direcția poate fi spre stânga, spre în sus, sau în diagonală stânga-sus. Apoi vom porni din elementul M[m][n] și vom urma aceste direcții până ce ieșim din matrice. De fiecare dată când urmăm o diagonală reținem litera. Astfel, descoperirea secvenței se face în ordine inversă.
Pentru a putea reconstrui soluția vom memora în fiecare căsuță, pe lângă lungimea maximă și direcția din care provine ea, adică din care am calculat acel maxim. Direcția poate fi spre stânga, spre în sus, sau în diagonală stânga-sus. Apoi vom porni din elementul <source lang="C" enclose="none">M[m][n]</source> și vom urma aceste direcții până ce ieșim din matrice. De fiecare dată când urmăm o diagonală reținem litera. Astfel, descoperirea secvenței se face în ordine inversă.




== Tema 25 - cercul de informatică pentru performanță clasa a șaptea ==
== Tema 24 ==
Tema 25 presupune să se rezolve următoarele probleme (program C trimis la nerdarena):


dreptunghiuri pentru subsecvența crescătoare maximală. Atenţie: puteți pica teste cu TLE, este în regulă.
Să se rezolve următoarele probleme (program C trimis la [https://www.nerdarena.ro/ NerdArena]):


subșirul de sumă maximă
* [https://www.nerdarena.ro/problema/dreptunghiuri Dreptunghiuri] pentru subsecvența crescătoare maximală. '''Atenţie:''' puteți pica teste cu TLE, este în regulă.
* [https://www.nerdarena.ro/problema/ssm Subșirul de sumă maximă]
* [https://www.nerdarena.ro/problema/sclm Subșirul comun maximal]
* [https://www.nerdarena.ro/problema/cmlsc Subsecvența comună maximală]


subșirul comun maximal


subsecvența comună maximală
[http://solpedia.francu.com/wiki/index.php?title=Clasa_a_7-a_Lec%C8%9Bia_24:_Programare_dinamic%C4%83_(1) Accesează rezolvarea temei 24]

Revision as of 09:18, 10 June 2025

Înregistrare video lecție


Conceptul de programare dinamică

Introducere

Wikipedia definește programarea dinamică drept o metodă pentru rezolvarea unor probleme complexe prin descompunerea lor în subprobleme mai simple. Se poate aplica problemelor care prezintă proprietățile de suprapunere a subproblemelor și de substructură optimală. Atunci când se poate aplica, metoda ia mult mai puțin timp decât alte soluții naive.

Drumul cel mai scurt între două orașe poate fi rezolvat cu programare dinamică

Substructura optimală înseamnă că soluția unei problemei de optimizare date poate fi obținută prin combinarea unor soluții optimale ale subproblemelor. Ca atare, primul pas către găsirea unei rezolvări prin această metodă este verificarea dacă problema are proprietatea substructurii optimale.

Drumul cel mai scurt între două orașe poate fi rezolvat cu programare dinamică

Astfel de substructuri optimale sunt în mod uzual descrise prin recurență. De exemplu, date niște orașe și distanțele între ele, drumul cel mai scurt de la orașul u la orașul v are proprietatea de substructură optimală: să luăm orice oraș intermediar w pe drumul cel mai scurt, d. Dacă d este cu adevărat drumul cel mai scurt, atunci drumul d1 de la u la w și d2 de la w la v sunt cu adevărat cele mai scurte drumuri între orașele corespunzătoare. Dacă drumul de la u la w nu ar fi cel mai scurt posibil ar însemna că am avea un alt drum mai scurt, ceea ce ar duce la un drum total d, de la u la v, mai scurt decât cel original. Așadar, putem formula soluția problemei găsirii celui mai scurt drum într-o manieră recursivă, ceea ce fac algoritmii Bellman-Ford și Floyd-Warshall.

Suprapunerea subproblemelor înseamnă că ele nu sunt independente, adică subproblemele au în comun alte subprobleme. Nu vom insista pe această proprietate decât atât cât să menționăm că dacă subproblemele nu se suprapun dar prezintă substructură optimală atunci putem aplica tehnica de programare divide et impera (divide and conquer). De aceea quicksort și mergesort nu sunt clasificate ca probleme de programare dinamică.


Metodă

Când încercăm să rezolvăm o problemă de programare dinamică întrebarea crucială la care trebuie să răspundem este putem calcula soluția finală pe baza unor subprobleme care sunt la rândul lor optimale ? Pentru a răspunde la întrebare trebuie mai întâi să formulăm subproblemele. Dar pentru a formula subproblemele trebuie să avem în minte principiul substructurii optimale. Altfel s-ar putea să împărțim problema în subprobleme care nu au substructură optimală.

Să recapitulăm: pentru a putea răspunde la întrebarea dacă o problemă admite o soluție prin programare dinamică trebuie să aflăm dacă are substructură optimală. Dar pentru a afla dacă are substructură optimală trebuie să găsim subproblemele. Pe care încercăm să le găsim folosind programarea dinamică.

Nu cumva ne învârtim în cerc? Așa pare și așa și este, într-o oarecare măsură. Experiența este cea care ne învață cum să căutăm împărțirea în subprobleme astfel încât să obținem proprietatea de optimalitate. Tot experiența este cea care ne ajută să ne dăm seama rapid dacă împărțirea găsită are această proprietate sau nu.

Odată ce demonstrăm că o împărțire în subprobleme se bazează doar pe subprobleme optimale, restul decurge, de obicei, destul de simplu: găsim o formulă de recurență, apoi o implementăm de jos în sus (sau bottom-up), pornind de la cele mai mici probleme, către cele mai mari, până la soluția cerută.

În continuare vom lua câteva exemple.


Subsecvența crescătoare de lungime maximă

Această problemă cere să găsim o subsecvență a unei secvențe de numere în care elementele sunt în ordine crescătoare, iar subsecvența este de lungime maximă. Subsecvența nu este neapărat contiguă, nici unică.

Exemplu

În secvența

0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15

o secvență crescătoare de lungime maximă este

0, 2, 6, 9, 13, 15

Această secvență are lungime șase. Nu există nici o secvență crescătoare de lungime șapte. Cea mai lungă subsecvență crescătoare nu este unică. De exemplu

0, 4, 6, 9, 11, 15

este o altă subsecvență crescătoare de aceeași lungime, în secvența de intrare.


Soluție

Cum împărțim problema în subprobleme? Să încercăm astfel:

L[i] = lungimea celei mai lungi subsecvențe care începe la poziția i

Are această împărțire proprietatea de optimalitate? Să presupunem că avem o subsecvență optimală crescătoare care trece prin indicii

i1, i2, i3, …, ik

Astfel

L[i1] = k

Este secvența i2, ..., ik optimală? Cu alte cuvinte lungimea secvenței optime care începe la i2 este k-1? Răspunsul este da, secvența care începe la i2, anume i2, ..., ik este o secvență maximă, deoarece altfel am putea-o înlocui cu o secvență mai lungă, ceea ce ar duce la o subsecvență L[i1] mai lungă decât cea inițială, care era optimală. Obținem o contradicție.

Următorul pas este să găsim relația de recurență:

L[i] = max(L[j]+1), j > i și V[j] ≥ V[i]

Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula vectorul L<source lang="C" enclose="none"> de la coadă la cap, căutând pentru fiecare element <source lang="C" enclose="none">L[i]<source lang="C" enclose="none"> cel mai mare element <source lang="C" enclose="none">L[j]<source lang="C" enclose="none"> de după el <tt>(j > i)</tt> astfel încât <source lang="C" enclose="none">V[j] V[i]<source lang="C" enclose="none">. Soluția problemei este maximul din vectorul <source lang="C" enclose="none">L.


Exemplu

Să calculăm cea mai lungă secvență crescătoare în secvența:

0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15

Vom calcula lungimile secvențelor maximale de la coadă la cap: L[15], apoi L[14] și așa mai departe. Când ajungem la calculul lui L[6] avem următoarea situație:

Exemplu de calcul pentru elementul L[6] - cea mai lungă secvență crescătoare care începe la poziția 6.

Pentru a calcula lungimea secvenței maximale care începe la poziția 6 vom căuta toate pozițiile mai mari (7, 8, 9, ...). Le vom lua în considerare doar pe acelea în care V[i] >= V[6]. Dintre ele vom alege poziția unde L[i] este maximă. Astfel vom găsi că V[9] = 9 și L[9] = 3. Vom avea, deci, o secvență de lungime 4. Ea este:

6, 9, 11, 15

Exemplu de calcul pentru elementul L[6] - cea mai lungă secvență crescătoare care începe la poziția 6.


Analiză

Acest algoritm are complexitate O(n2), unde n este lungimea secvenței. Memoria folosită este O(n).

Există și un algoritm mai rapid, de complexitate O(n log n), bazat pe căutare binară a valorilor de început.


Reconstrucția soluției

Putem reconstrui o subsecvență maximală folosind încă un vector, N[]. El memorează pentru fiecare indice i un alt indice j care urmează în secvență după j. Atunci când calculăm elementul curent, vom nota nu doar maximul găsit (plus unu) ci și poziția lui în vector, el fiind elementul următor în secvență.


Subșirul de sumă maximală

Această problemă cere să găsim un subșir de sumă maximă într-un șir de numere. Un subșir este contiguu.


Exemplu

Să considerăm șirul:

12, 14, 0, -4, 61, -39

subșirul de sumă maximală este:

12, 14, 0, -4, 61

și are sumă 83.


Soluție

Cum împărțim problema în subprobleme? Să încercăm astfel:

S[i] este suma maximală a unui șir care se termină la poziția i

Are această împărțire proprietatea de optimalitate?

Să presupunem că avem un subșir de sumă maximă și că el se află între indicii i și j. Cu alte cuvine suma maximală este

S[j] = V[i] + V[i+1] + ... + V[j-1] + V[j]

Are șirul care se termină în j-1, sumă maximală? Cu alte cuvinte este suma

S[j-1] = V[i] + V[i+1] + ... + V[j-1]

maximală?

Răspunsul este da, deoarece altfel am putea-o înlocui cu o sumă mai mare, ceea ce ar duce la o sumă mai mare decît cea inițială, care era optimală. Obținem o contradicție.

Următorul pas este să găsim relația de recurență. Observăm că pentru o poziție i avem două variante: fie continuăm secvența anterioară, care se termină în i-1, fie începem una nouă, care pornește în i. Vom continua secvența anterioară dacă adunând numărul curent obținem o sumă mai mare strict decât numărul curent. Altfel vom porni o sumă nouă. Relația de recurență este:

S[i] = max(V[i], S[i-1] + V[i])

Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula vectorul S calculând pentru fiecare element S[i] maximul dintre S[i-1] + V[i] și V[i]. Observăm că, în realitate, vom compara S[i-1] cu zero.

Soluția problemei este maximul din vectorul S.


Exemplu

Fie secvența anterioară:

12, 14, 0, -4, 61, -39

Vom calcula de la stânga la dreapta sumele maximale ce se termină la pozițiile respective:

V 12 14 0 -4 61 -39
S 12 26 = max(14, 12 + 14) 26 = max(0, 0 + 26) 22 = max(-4, -4 + 26) 83 = max(61, 61 + 22) 44 = max(-39, -39 + 83)


Analiză

Acest algoritm are complexitate O(n), unde n este lungimea șirului. Memoria folosită este O(1), deoarece nu avem nevoie să stocăm elementele, ci doar ultima sumă calculată.


Reconstrucția soluției

Putem ține minte șirul maximal ținând minte începutul și sfârșitul subșirului maximal atunci când îl găsim.


Soluții alternative

Această problemă poate fi rezolvată fără programare dinamică. Ne propunem același lucru, ca la pasul i să calculăm S[i] cu aceeași semnificație. Acest S[i] se poate calcula cu cunoștințe de nivel mic: sume parțiale. S[i] va fi suma unei secvențe de la k la i, cu k ≤ i. Deci, dacă vom calcula P[], sumele parțiale pe vector putem spune că S[i] = P[i] - P[k]. Să ne uităm puțin la această expresie: cine variază în ea? P[i] este constant, este suma elementelor până la i. Putem varia doar P[k]. Este evident că dorim să alegem un P[k] cât mai mic, nu?

Algoritmul este unul banal: calculează sume parțiale, avansând i. Suma minimă care se termină în i este suma parțială curentă minus suma parțială minimă din trecut. La fiecare avans actualizează suma parțială minimă cu cea curentă, dacă este cazul.

Acest algoritm are aceleași complexități ca cel cu programare dinamică. El este însă un algoritm mai flexibil de adaptat unor modificări, cum ar fi suma maximă de lungime cel puțin K sau suma maximă de lungime cel mult K.


Subșirul comun maximal al două șiruri

Această problemă cere să găsim lungimea unui subșir de lungime maximă (contiguu) inclus în două șiruri.


Exemplu

Date șirurile

ABABC

și

CBABA

un subșir maximal are lungime 3 și este

BAB


Soluție

Cum împărțim problema în subprobleme? Să observăm că deoarece subșirul maximal face parte din ambele șiruri el se va termina la o poziție i în șirul X și la o poziție j în șirul Y. Și atunci să încercăm astfel:

M[i][j] = lungimea celui mai lung subșir care se termină la poziția i în X și la poziția j în Y.

Are această împărțire proprietatea de optimalitate?

Să presupunem că avem un subșir optimal care se termină la pozițiile i, respectiv j. Atunci este subșirul M[i-1][j-1] la rândul lui optimal? Cu alte cuvinte subșirul mai scurt cu unu, care se termină la pozițiile anterioare în X, respectiv Y, este optimal? Da, el este optimal, caci altfel l-am putea înlocui cu un șir mai lung, care ar duce la o soluție mai bună, ceea ce ar însemna că soluția originală nu a fost optimă.

Următorul pas este să găsim relația de recurență:

Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula matricea M pe linii, folosind formula de recurență.

Soluția problemei este maximul din matrice.


Exemplu

Pentru cele două șiruri de mai sus,

ABABC

și

CBABA

Vom calcula o matrice M[5][5] care arată astfel:

C B A B A
A 0 0 1 0 1
B 0 1 0 2 0
A 0 0 2 0 3
B 0 1 0 3 0
C 1 0 0 0 0

Observăm că avem două subșiruri comune de lungime 3: ABA și BAB.


Analiză

Acest algoritm are complexitate O(m·n), unde m și n sunt lungimile șirurilor. Memoria folosită este, la prima vedere, O(m·n). În realitate, parcurgând matricea pe linii, nu avem nevoie să memorăm decât o linie din matrice pentru a calcula toate valorile. De aceea memoria necesară este O(min(m, n)).

Există și algoritmi mai eficienți bazați pe arbori sufix, despre care nu vom discuta aici.


Reconstrucția soluției

Cum facem să aflăm nu doar lungimea ci și care este șirul comun maximal și unde apare el în cele două șiruri?

Vom face calculul matricei noastre printr-o parcurgere pe linii. La fiecare pas ne vom întreba dacă elementul calculat (lungimea) este mai mare decât maximul curent. Dacă da, vom memora acel maxim împreună cu indicii i și j la care s-a obținut el.

La final vom avea poziția de start în primul șir ca fiind i - max + 1 și în al doilea șir ca fiind j - max + 1.


Subsecvența comună maximală a două șiruri

Această problemă cere să găsim lungimea unei subsecvențe de lungime maximă inclusă în două secvențe. Prin secvență înțelegem caractere în ordinea originală, nu neapărat consecutive ca poziție.


Exemplu

Date secvențele

X: ACDEGIKMPR
Y: CMEFPGHRK

o secvență maximală are lungime 4 și este

CEGK

o altă secvență maximală este

CMPR


Soluție

Cum împărțim problema în subprobleme? Să încercăm să o împărțim în mod similar cu subșirul comun maximal:

M[i][j] = lungimea celei mai lungi subsecvențe incluse în șirurile X[1..i] și Y[1..j]

Are această împărțire proprietatea de optimalitate?

Să presupunem că avem o secvență optimală a prefixelor care se termină la pozițiile i, respectiv j. Ea va trece prin anumite perechi (ik, jk), strict crescătoare, cu proprietatea că X[ik] = Y[jk]:

Si,j = (i1, j1) (i2, j2) ... (ik, jk)

cu proprietatea că:

Xi1Xi2... Xik-1Xik = Yj1Yj2... Yjk-1Yjk

Atunci este lungimea M[ik-1][jk-1] la rândul ei optimală? Cu alte cuvinte subsecvența mai scurtă cu unu, care se termină la poziția ik-1 în X, respectiv poziția jk-1 în Y, este optimală? Da, ea este optimală, căci altfel am putea-o înlocui cu o secvență mai lungă, care ar duce la o soluție mai bună, ceea ce ar însemna că soluția originală nu a fost optimă.

Următorul pas este să găsim relația de recurență:

Cum demonstrăm această relație de recurență? Avem două cazuri de demonstrat:


Egalitate

Prefixele X[1..i] și Y[1..j] se termină în același caracter. În acest caz o subsecvență maximală a celor două prefixe se poate obține selectând acel ultim caracter. Putem, deci exclude acel caracter și calcula subsecvența maximală în prefixele X[1..i-1] și Y[1..j-1]. La lungimea ei vom aduna unu.


Diferență

Prefixele X[1..i] și Y[1..j] se termină în caractere diferite. Să luăm cazul anterior, avem secvențele:

X: ACDEGIKMPR
Y: CMEFPGHRK

Avem iarăși două cazuri:

  • Cazul 1: subsecvența optimă se termină în R. În acest caz ea nu se poate termina și în K. Deci putem elimina ultimul caracter din Y și calcula M[i][j-1].
  • Cazul 2: subsecvența optimă nu se termină în R. În acest caz putem elimina acest R și calcula M[i-1][j].

Următorul pas este să construim subproblemele bottom-up, de la problemele mai mici la cele mai mari. Pentru aceasta vom calcula matricea M pe linii, folosind formula de recurență.

Soluția problemei este M[n][m].


Exemplu

Pentru cele două șiruri de mai sus,

ACDEGIKMPR

și

CMEFPGHRK

Vom calcula o matrice M[10][9] care arată astfel:

C M E F P G H R K
A 0 0 0 0 0 0 0 0 0
C 1 1 1 1 1 1 1 1 1
D 1 1 1 1 1 1 1 1 1
E 1 1 2 2 2 2 2 2 2
G 1 1 2 2 2 3 3 3 3
I 1 1 2 2 2 3 3 3 3
K 1 1 2 2 2 3 3 3 4
M 1 2 2 2 2 3 3 3 4
P 1 2 2 2 3 3 3 3 4
R 1 2 2 2 3 3 3 4 4

Cea mai lungă secvență este de lungime 4, valoarea maximă ce apare în tabel.


Analiză

Acest algoritm are complexitate O(m · n), unde m și n sunt lungimile șirurilor. Memoria folosită este O(min(m,n)) dacă memorăm doar o linie din matrice. Dacă dorim să aflăm și o secvență maximală, nu doar lungimea ei, vom avea nevoie de memorie O(m · n), precum vom vedea mai jos.

Există un algoritm care reduce necesarul de memorie la O(min(m, n)).


Reconstrucția soluției

Dorim să aflăm nu doar lungimea, ci și o secvență maximală inclusă în ambele șiruri. Cum procedăm?

Observăm că literele ce fac parte din secvențele maximale se obțin acolo unde, în matrice, avem ramura doi de calcul, când literele de la pozițiile respective sunt egale.

Pentru a putea reconstrui soluția vom memora în fiecare căsuță, pe lângă lungimea maximă și direcția din care provine ea, adică din care am calculat acel maxim. Direcția poate fi spre stânga, spre în sus, sau în diagonală stânga-sus. Apoi vom porni din elementul M[m][n] și vom urma aceste direcții până ce ieșim din matrice. De fiecare dată când urmăm o diagonală reținem litera. Astfel, descoperirea secvenței se face în ordine inversă.


Tema 24

Să se rezolve următoarele probleme (program C trimis la NerdArena):


Accesează rezolvarea temei 24