Aceste recurențe au o oarecare asemănare cu problema SSM (enunț + soluție).
subsecvență (
substring în engleză -
i | 1 | 2 | 3 | 4 | 5| 6 |
|v[i]| -10 | 2 | 3 | -1| 2 | -3 |
Răspuns: SSM este între 2 și 5 (poziții). Are suma +6. ($SSM = 2, 3, -1, 2$)
Explicație: avem numere pozitive, deci există o soluție simplă în care putem să alegem doar un număr pozitiv/mai multe numere pozitive de pe poziții alăturate (adică încercăm să evităm numere negative). Cele mai lungi subsecvențe cu numere pozitive sunt 2,3 și 2. Observăm că dacă extindem $2, 3$ la $2, 3, -1, 2$, deși am inclus un număr negativ, suma secvenței crește.
</spoiler>
<spoiler Exemplu 2>
n = 4
|i | 1 | 2 | 3 | 4 |
|v[i]| 10 | 20 | 30 | 40|
Răspuns: SSM este între 1 și 4 (poziții). Are suma 100. ($SSM = 10, 20, 30, 40$)
Explicație: deoarece toate numerele sunt **pozitive**, SSM cuprinde toate numerele.
</spoiler>
<spoiler Exemplu 3>
n = 4
|i | 1 | 2 | 3 | 4 |
|v[i]| -10 | -20 | -30 | -40|
Răspuns: SSM este între 1 si 1 (poziții). Are suma -10. ($SSM = -10$)
Explicație: deoarece toate numerele sunt **negative**, SSM cuprinde doar cel mai mare număr.
</spoiler>
=== Rezolvare ===
== Tipar ==
Tiparul acestei probleme ne sugerează că o soluție este obținută incremental, în sensul că **putem** privi problema astfel: găsim cea mai bună soluție folosind **primele $i-1$** elemente din șir, apoi încercăm să o **extindem** folosind elementul **i** (adică ne extindem la dreapta ~CU~ $v[i]$).
== Numire recurență ==
Întrucât la fiecare pas trebuie sa reținem ** cea mai bună soluție** folosind un **prefix** din vectorul v, soluția va fi salvată într-un tablou auxiliar definit astfel:
$ dp[i] $ = suma subsecvenței de sumă maximă (**suma SSM**) folosind ** doar primele i ** elemente din vectorul v și care **se termină pe poziția i**
== Mențiuni ==
* Pentru a menține o convenție, toate tablourile de acest tip din laborator vor fi notate cu ** dp ** (dynamic programming).
* Ca să rezolvăm problema dată, trebuie să rezolvăm o mulțime de subprobleme
* $dp[i]$ reprezintă **soluția** pentru problema $v[1], ..., v[i]$ și care se termină cu $v[i]$
* Soluția pentru problema inițială este maximul din vectorul $dp[i]$ - a.k.a. **max(dp[i])**.
== Găsire recurență ==
Întrucât dorim ca această problemă să fie rezolvabilă printr-un algoritm/bucată de cod, trebuie să descriem o metodă concretă prin care vom calcula $dp[i]$.
* **Cazul de bază**
* În general în probleme putem avea mai multe cazuri de bază, care în principiu se leagă de valori extreme are dimensiunilor subproblemelor.
* În cazul SSM, avem un singur caz de bază, când avem un singur element în prefix: $dp[1] = v[1] $.
* Explicație: dacă avem un singur element, atunci acesta formează singura subsecvență posibilă, deci $ SSM = v[1] $
* ** Cazul general **
* presupune inductiv că avem rezolvate toate subproblemele mai mici
* în cazul SSM, presupunem că avem calculat $ dp[i-1] $ și dorim sa calculăm $ dp[i] $ (cunoaștem cea mai bună soluție folosind primele i-1 elememente și vedem dacă elementul de pe poziția i o poate îmbunătăți)
* la fiecare pas avem de ales dacă $v[i]$ extinde cea mai bună soluție care se termină pe $v[i-1]$ sau se începe o nouă secvență cu $v[i]$
* decidem în funcție de $ dp[i - 1]$ și $v[i] $
* ** dacă ** $ dp[i - 1] >= 0 $ (cea mai bună soluție care se termină pe i - 1 are cost nenegativ)
* extindem secvență care se termină cu v[i-1] folosind elementul v[i]: $dp[i] = dp[i-1] + v[i]$
* Explicație: $dp[i-1] + v[i] >= v[i]$ (încă are rost să extind)
* ** dacă ** $ dp[i - 1] < 0 $ (cea mai bună soluție care se termină pe i - 1 are cost negativ)
* vom începe o nouă secvență cu $v[i]$, adică $dp[i] = v[i]$
* Explicație: $v[i] > dp[i-1] + v[i]$, deci prin extindere nu obțin soluție maximă!
== Implementare recurență ==
În majoritatea problemelor de DP, găsirea recurenței ocupă cea mai mare parte a timpului de rezolvare (lucru adevărat și în cazul problemelor de la PA). De aceea, faptul că ați reușit să scrieți pe foaie lucruri foarte complicate poate fi un indiciu ca ați pornit pe o cale greșită.
<spoiler Exemplu implementare>
Problema se poate testa pe infoarena: [[https://www.infoarena.ro/problema/ssm | Subsecvență de sumă maximă]].
Mai jos se află un exemplu simplu de implementare a recurenței găsite în C++.
<code cpp>
// găsește SSM pentru vectorul v cu n elemente
// pentru a menține convenția din explicații:
// - elementele sunt indexate de la 0, dar le folosesc doar pe cele care incep de la 1
// => v[1], ..., v[n]
int SSM(int n, vector<int> &v) {
vector<int> dp(n + 1); // vector cu n + 1 elemente (indexarea începe de la 0)
// am nevoie de dp[1], ..., dp[n]
// caz de bază
dp[1] = v[1];
// caz general
for (int i = 2; i <= n; ++i) {
if (dp[i - 1] >= 0) {
// extinde la dreapta cu v[i]
dp[i] = dp[i - 1] + v[i];
} else {
// încep o nouă secvență
dp[i] = v[i];
}
}
// soluția e maximul din vectorul dp
int sol = dp[1];
for (int i = 2; i <= n; ++i) {
if (dp[i] > sol) {
sol = dp[i];
}
}
return sol; // aceasta este suma asociată cu SSM
}
</code>
Dacă dorim să afișăm și indicii între care apare SSM, putem să stocăm și poziția de start pentru fiecare soluție intermediară. .
Hint: definiți ** start[i] ** = poziția pe care a început subsecvența care dă soluția cu cost dp[i].
</spoiler>
=== Mențiuni ===
Întrucât această soluție presupune calculul iterativ (coloană cu coloană) a matricei dp, complexitatea este liniară. De asemenea, se mai parcurge o dată dp pentru a găsi maximul.
* **complexitate temporală **: $T = O(n)$
* **complexitate spațială ** : $S = O(n)$
* desigur că pentru problema SSM, nu era nevoie sa reținem, tablourile dp/start în memorie.
* puteam sa construim element cu element și maximul din dp în aceleași timp (întrucât ne trebuie ultima valoare la fiecare pas și maximul global).
* în acest caz complexitatea spațială devine $S = O(1)$
** Pentru a ilustra toți pașii posibili într-o astfel de problemă, totul a fost prezentat cât mai simplu (NU în toate problemele putem facem simplificări de tipul "NU am nevoie să stochez tabloul dp").**
==== SCMAX ====
=== Enunț ===
Fie un vector $ v $ cu $ n $ elemente întregi. Un subșir de numere din șir este de forma: $v_{i_1}, v_{i_2}, ... , v_{i_k}$. Un subșir ** nu ** poate fi vid ($k >= 1$).
<note>
**subșir** (**subsequence** în engleză) pentru un vector ** v ** înseamnă un alt vector $u = [v[i_1], v[i_2],..., v[i_k]$ unde $i_1 < i_2 < … < i_k
SCMAX = 12, 13, 15$ ($SCMAX = v[2], v[3], v[5])
100$
* $12$
* $12, 13$
* $12, 13, 15$
* $12, 15$
* $13$
* $13, 15$
* $-1$
* $-1, 15$
* $15$
* $-30
SCMAX = 12, 13, 15$ ($SCMAX = v[2], v[3], v[5])
SCMAX = 12, 13, 14$ ($SCMAX = v[2], v[3], v[6])
100$
* $12$
* $12, 13$
* $12, 13, 15$
* $12, 13, 14$
* $13$
* $13, 15$
* $13, 14$
* $-1$
* $-1, 15$
* $-1, 14$
* $15$
* $14
i-1$ elemente din șir, apoi încercăm să o extindem folosind elementul i (adică ne extindem la dreapta ~CU~ $v[i]
1, 2, .., i-1$ și care se termină cu $v[i-1]$, atunci încercăm să extindem soluția cu $v[i]$ (putem dacă $v[i-1] < v[i]
v[i]
v[i-2]$, $v[i-3]$, ... sau $v[1]
dp[i]
dp[i]$ reprezintă **soluția** pentru problema $v[1], …, v[i]$ și care se termină cu $v[i]$
* Soluția pentru problema inițială este maximul din vectorul $dp[i]
i = 1
dp[1] = 1
dp[1], dp[2], …, dp[i-1] $ și dorim să calculăm $ dp[i] $ (cunoaștem cea mai bună soluție folosind primele j elemente și vedem dacă elementul de pe poziția i o poate îmbunătăți - $j = 1:i-1
v[i]$ (după care v[j]?), încercăm pentru toate valorile posibile ale lui j (unde $j = 1 : n - 1
v[j] < v[i]
j = 1 : i - 1
v[i]
dp[1] = 1$
* $dp[i] = 1 + max(dp[j])$, unde $j = 1 : i-1$ **și** $v[j] < v[i]
prec[i]$ = indicele j al elementului v[j], pentru care $dp[j] + 1 == dp[i]$ (adică acel j pentru care subșirul crescător maximal care se termină cu $v[i]$ este extinderea cu un element a celui care se termină cu $v[j]
prec[i] = 0
T = O(n^2)$
* se poate obține o soluție în complexitate $T = O(n log n)
S = O(n)
n $ obiecte (care nu pot fi tăiate - varianta discretă a problemei). Fiecare obiect i are asociată o pereche ($w_i, p_i
w_i$ = $weight_i
p_i$ = $price_i
w_i >= 0$ si $p_i > 0
W
w_i, p_i$), atunci profitul adus de obiect este $p_i
n = 5$ si $W = 10
n = 5$ și $W = 3
2
i-1
w_i, p_i
dp[i] = …
dp[i][cap]
dp[i][cap]
dp[n][W]
dp[0][cap] = 0
dp[i][cap] = ?
dp[i][cap]
i-1
dp[i][cap] = dp[i - 1][cap]
w_i$
* adică când am selectat dintre primele $i-1$ elemente, nu trebuia să ocup mai mult de $cap - w_i
p_i
dp[i][cap] = dp[i - 1][cap - w_i] + p_i
dp[0][cap] = 0$, pentru $cap = 0 : G$
* $dp[i][cap] = max(dp[i - 1], cap], dp[i - 1][cap - w_i] + p_i)$
* pentru $i = 1: n$, $cap = 0:W
i = 1 : n)
int rucsac(int n, int W, vector<int> &w, vector<int> &p) {
// dp este o matrice de dimensiune (n + 1) x (W + 1)
// pentru că folosim dp[0][*] pentru mulțimea vidă
// dp[*][0] pentru situația în care ghiozdanul are capacitate 0
vector< vector<int> > dp(n + 1, vector<int>(W + 1, 0));
// cazul de bază
for (int cap = 0; cap <= W; ++cap) {
dp[0][cap] = 0;
}
// cazul general
for (int i = 1; i <= n; ++i) {
for (int cap = 0; cap <= W; ++cap) {
// nu folosesc obiectul i => e soluția de la pasul i - 1
dp[i][cap] = dp[i-1][cap];
// folosesc obiectul i, deci trebuie să rezerv w[i] unități în rucsac
// înseamnă ca înainte trebuie să ocup maxim cap - w[i] unități
if (cap - w[i] >= 0) {
int sol_aux = dp[i-1][cap - w[i]] + p[i];
dp[i][cap] = max(dp[i][cap], sol_aux);
}
}
}
return dp[n][W];
}
</code>
</spoiler>
=== Mențiuni ===
Întrucât această soluție presupune calculul iterativ (linie cu linie) a matricei dp, complexitatea este polinomială.
===== Exercitii =====
=== 1. Not again! ===
Gigel are o colectie impresionanta de monede. El ne spune ca are n tipuri de monede, avand un numar nelimitat de monede din fiecare tip. Cunoscand aceasta informatie (data sub forma unui vector v cu n elemente), el se intreaba care este numarul minim de monede cu care poate plati o suma S.
Task-uri:
Este posibil ca pentru anumite valori ale lui S si v, aceasta problema sa nu aiba solutie. In acest caz raspunsul este -1.
$ n = 4$ si $ S = 12$
Raspuns: $2$
Explicatie: Avem 4 tipuri de monede: 1 euro, 2 euro, 3 euro si 6 euro (lui Gigel nu ii mai place sa foloseasca RON). Avem la dispozitie oricate monede din fiecare tip.
Suma 12 poate fi obtinuta in urmatoarele moduri:
Solutia cu numar minim de monede se obtine pentru modul $6 + 6$.
$ n = 3$ si $ S = 11$
Raspuns: $3$
Explicatie: Avem 3 tipuri de monede: 1 euro, 2 euro si 5 euro (lui Gigel nu ii mai place sa foloseasca RON). Avem la dispozitie oricate monede din fiecare tip.
Suma 11 poate fi obtinuta in urmatoarele moduri:
$11 = 5 + 5 + 1$
$11 = 5 + 2 + 2 + 2$
$11 = 5 + 2 + 2 + 1 + 1$
$11 = 5 + 2 + 1 + 1 + 1 + 1$
$11 = 5 + 1 + 1 + 1 + 1 + 1 + 1$
$11 = 2 + 2 + 1 + 1 + 1 + 1 + 1 + 1 + 1$
$11 = 2 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1$
$11 = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1$
Solutia cu numar minim de monede se obtine pentru modul $5 + 5 + 1$.
$ n = 3$ si $ S = 11$
Raspuns: $-1$
Explicatie: Nu putem forma suma 11 folosind tipurile (valorile) 2, 4, 6.
=== 2. CMLSC ===
Fie doi vectori cu numere intregi: v cu n elemente si w cu m elemente. Sa se gaseasca cel mai lung subsir comun (notat CMLSC) care apare in cei doi vectori. Se cere o solutie de complexitate optima. Daca exista mai multe solutii, se poate gasi oricare.
Task-uri:
2.1 Determinare lungime CMLSC. (Hint: DP)
2.2 Reconstituire CMLSC (afisati si care sunt termenii CMLSC).
2.3 Care este complexitatea solutiei (timp + spatiu)? De ce?
Rezolvati in ordine task-urile.
subsir (subsequence in engleza) pentru un vector v inseamna un alt vector $u = [v[i_1], v[i_2],..., v[i_k]]]$ unde $i_1 < i_2 < ... < i_k$.
$ n = 3 $ si $ m = 5$
Raspuns: $lungime = 2$, $CMLSC = [6, 9]$
Explicatie: Toate subsirurile comune posibile sunt:
Solutia mentionata are lungime maxima.
$ n = 8 $ si $ m = 5$
Raspuns: $lungime = 4$, $CMLSC = [1, 5, 5, 7]$
Explicatie: Toate subsirurile comune posibile sunt (duplicatele vor fi mentionate o singura data):
$[1]$
$[1, 5]$
$[1, 7]$
$[1, 4]$
$[1, 5, 5]$
$[1, 5, 7]$
$[1, 5, 4]$
$[1, 5, 5, 7]$
$[5]$
$[5, 5]$
$[5, 7]$
$[5, 4]$
$[4]$
Solutia mentionata are lungime maxima.
$ n = 8 $ si $ m = 5$
Raspuns: $lungime = 3$, $CMLSC = [1, 5, 7]$ (exemplu de solutie)
Explicatie: Toate subsirurile comune posibile sunt (duplicatele vor fi mentionate o singura data):
$[1]$
$[1, 5]$
$[1, 7]$
$[1, 4]$
$[1, 5, 7]$
$[1, 5, 4]$
$[5]$
$[5, 7]$
$[5, 4]$
$[4]$
Solutii pot fi: $[1, 5, 7]$ si $[1, 5, 4]$.
Pentru $[1, 5, 7]$, se observa ca sunt 2 astfel de subsiruri in vectorul v. Oricare este bun.
=== BONUS ===
Rezolvati pe infoarena problema custi.
=== Extra ===
Modificati solutia de la Rucsac prezentata in laborator pentru a obtine o complexitate spatiala mai buna (se va retine un numar minim de linii din matrice).
Puteti testa solutia voastra pe infoarena la problema rucsac.
Se da o matrice de dimensiuni $n * m$ si Q intrebari de forma: “Care este suma din submatricea care are coltul stanga-sus (x, y) si coltul dreapta-jos (p,q)?”
Se considera proprietatea: Q este mult mai mare decat dimensiunile matricei.
Sa se raspunda in mod eficient la cele Q intrebari.
Rezolvatie pe infoarena problema joctv.
Solutie: Se fixeaza 2 linii pentru zona dreptunghiulara. Se reduce problema la SSM in O(n).
Complexitate: $O(n^3)$.
===== Referințe =====
[0] Chapter Dynamic Programming, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein
[1] http://infoarena.ro/problema/ssm
[2] http://infoarena.ro/problema/scmax
[3] http://infoarena.ro/problema/rucsac