This is an old revision of the document!
Laborator 4: Programare Dinamică (continuare)
Obiective laborator
Precizări inițiale
Toate exemplele de cod se găsesc pe pagina
pa-lab::demo/lab04.
Exemplele de cod apar încorporate și în textul laboratorului pentru a facilita parcurgerea cursivă a acestuia. ATENȚIE! Varianta actualizată a acestor exemple se găsește întotdeauna pe GitHub.
Toate bucățile de cod prezentate în partea introductivă a laboratorului (înainte de exerciții) au fost testate. Cu toate acestea, este posibil ca, din cauza mai multor factori (formatare, caractere invizibile puse de browser, etc.), un simplu copy-paste să nu fie de ajuns pentru a compila codul.
Vă rugăm să compilați DOAR codul de pe GitHub. Pentru raportarea problemelor, contactați unul dintre maintaineri.
Pentru orice problemă legată de conținutul acestei pagini, vă rugam să dați e-mail unuia dintre responsabili.
Ce este DP?
Similar cu greedy, tehnica de programare dinamică este folosită pentru rezolvarea problemelor de optimizare.
În continuare vom folosi acronimul DP (dynamic programming).
De asemenea, DP se poate folosi și pentru probleme în care nu căutam un optim, cum ar fi problemele de numărare.
Pentru noțiunile prezentate până acum despre DP, vă rugăm să consultați pagina laboratorului 3.
Exemple clasice
Programarea Dinamică este cea mai flexibilă tehnică a programării. Cel mai ușor mod de a o înțelege presupune parcurgerea cât mai multor exemple.
Propunem câteva categorii de recurențe pe care le vom grupa astfel:
Pentru o problemă dată, este posibil să găsim mai multe recurențe corecte (mai multe soluții posibile). Evident, criteriul de alegere între acestea va fi cel bazat pe complexitate.
Categoria 3: PODM
Aceste recurențe au o oarecare asemănare cu problema PODM (enunț + soluție).
ATENȚIE! Acest tip de recurențe poate fi mai greu (decât celelalte). Puteți consulta acasă materialele puse la dispoziție pentru a înțelege mai bine această categorie.
Caracteristici:
Acest tip de problemă presupune că o putem formula ca pe o problemă de tip subinterval $[i, j]$.
Dacă dorim să găsim optimul pentru acest interval, va trebui să luăm în calcul toate combinațiile de 2 subprobleme care ar putea genera o soluție pentru problemele $[i, j]$.
Se consideră fiecare divizare în 2 subprobleme, dată de intermediarul k, astfel:
Fie $[i, k]$ și $[k + 1, j]$ cele 2 subprobleme pentru care cunoaștem soluțiile, atunci o soluție pentru $[i,j]$ se poate obține îmbinându-le pe cele două
pentru a gasi soluția cea mai bună:
vom itera prin toate valorile k posibile
o vom alege pe cea care maximizează soluția problemei $[i,j]$
Calculul se face de la intervale mici (probleme ușoare - $[i,i]$ sau $[i, i+1]$) spre probleme generale (dimensiune generală - $[i, j]$ ). În final, se ajunge și la dimensiunile inițiale ($[1, n]$).
Exemple clasice
PODM
Enunț
Fie un produs matriceal $M = M_1 M_2 ... M_n$. Putem pune paranteze în mai multe moduri și vom obține același rezultat (înmulțire asociativă), dar este posibil să obținem un număr diferit de înmulțiri scalare.
Matricea $M_i$ are (prin convenție), dimensiunile $d_{i-1} d_{i}$.
Cerință
Se cere să se găsească o parantezare optimă de matrice (PODM), adică să se găsească o parantezare care să minimizeze numărul de înmulțiri scalare.
Exemple
$n = 3$
Răspuns: 64 (înmulțiri scalare)
Explicație: Avem 3 matrice:
A de dimensiuni (2, 3)
B de (3, 4)
C de (4, 5)
În funcție de ordinea efectuării înmulțirilor matriceale, numărul total de înmulțiri scalare poate să fie foarte diferit:
Rezultatul optim se obține pentru cea de a treia parantezare: $(AB)C$.
$n = 4$
Răspuns: 48 (înmulțiri scalare)
Explicație: Avem 4 matrice:
A de dimensiuni (2, 3)
B de (3, 4)
C de (4, 2)
D de (2, 3)
În funcție de ordinea efectuării înmulțirilor matriceale, numărul total de înmulțiri scalare poate să fie foarte diferit:
$(AB)C)D$ ⇒ $24 + 16 + 12 = 52$ înmulțiri
$(A(BC))D$ ⇒ $24 + 12 + 12 = 48$ înmulțiri
$(AB)(CD)$ ⇒ $ = $ inmulțiri
$A((BC)D)$ ⇒ $24 + 18 + 27 = 69$ înmulțiri
$A(B(CD))$ ⇒ $24 + 36 + 18 = 78$ înmulțiri
Rezultatul optim se obține pentru cea de a treia parantezare: $((A(BC))D)$.
$n = 4$
Răspuns: 2856 (înmulțiri scalare)
Explicație: Avem 4 matrice:
A de dimensiuni (13, 5)
B de (5, 89)
C de (89, 3)
D de (3, 34)
În funcție de ordinea efectuării înmulțirilor matriciale, numărul total de înmulțiri scalare poate să fie foarte diferit:
$((AB)C)D$ ⇒ 10582 înmulțiri
$(AB)(CD)$ ⇒ 54201 înmulțiri
$(A(BC))D$ ⇒ 2856 înmulțiri
$A((BC)D)$ ⇒ 4055 înmulțiri
…
Rezultatul optim se obține pentru cea de a treia parantezare: $(A(BC))D$.
TIPAR
A fost descris în detaliu mai sus (când s-a vorbit de categorie).
Numire recurență
$dp[i][j]$ = numărul minim de înmulțiri scalare cu care se poate obține produsul $M_i * M_{i+1} * ... *{M_j}$
Găsire recurență
Implementare
Puteți rezolva și testa problema PODM pe infoarena aici.
Un exemplu de implementare în C++ se găsește mai jos.
// kInf este valoarea maximă - "infinitul" nostru
const unsigned long long kInf = std::numeric_limits<unsigned long long>::max();
// T = O(n ^ 3)
// S = O(n ^ 2) - stocăm n x n întregi în tabloul dp
unsigned long long solve_podm(int n, const vector<int> &d) {
// dp[i][j] = numărul MINIM înmulțiri scalare cu codare, poate fi calculat produsul
// matriceal M_i * M_i+1 * ... * M_j
vector<vector<unsigned long long>> dp(n + 1, vector<unsigned long long> (n + 1, kInf));
// Cazul de bază 1: nu am ce înmulți
for (int i = 1; i <= n; ++i) {
dp[i][i] = 0ULL; // 0 pe unsigned long long (voi folosi mai încolo și 1ULL)
}
// Cazul de bază 2: matrice d[i - 1] x d[i] înmulțită cu matrice d[i] x d[i + 1]
// (matrice pe poziții consecutive)
for (int i = 1; i < n; ++i) {
dp[i][i + 1] = 1ULL * d[i - 1] * d[i] * d[i + 1];
}
// Cazul general:
// dp[i][j] = min(dp[i][k] + dp[k + 1][j] + d[i - 1] * d[k] * d[j]), k = i : j - 1
for (int len = 2; len <= n; ++len) { // fixăm lungimea intervalului (2, 3, 4, ...)
for (int i = 1; i + len - 1 <= n; ++i) { // fixăm capătul din stânga: i
int j = i + len - 1; // capătul din dreapta se deduce: j
// Iterăm prin indicii dintre capete, spărgând șirul de înmulțiri in două (paranteze).
for (int k = i; k < j; ++k) {
// M_i * ... M_j = (M_i * .. * M_k) * (M_k+1 *... * M_j)
unsigned long long new_sol = dp[i][k] + dp[k + 1][j] + 1ULL * d[i - 1] * d[k] * d[j];
// actualizăm soluția dacă este mai bună
dp[i][j] = min(dp[i][j], new_sol);
}
}
}
// Rezultatul se află în dp[1][n]: Numărul MINIM de inmultiri scalare
// pe care trebuie să le facem pentru a obține produsul M_1 * ... * M_n
return dp[1][n];
}
Sursa a fost scrisă pentru a fi testată pe infoarena. În cazul problemei
PODM, deoarece avem o sumă de foarte multe produse, rezultatul este foarte mare. Pe infoarena se cerea ca rezultatul să fie afișat asa cum e, garantându-se că încape pe 64 biți.
Reamintim că prin înmulțirea/adunarea a două variabile de tipul int, rezultatul poate să nu încapă pe 32 biți. De aceea, în soluția prezentată, s-a făcut cast pe 64 biți.
ATENȚIE! La PA, în general, vom folosi convenția $ expresie \ \% \ kMod $, care va fi detaliată în capitolul următor din acest laborator.
Complexitate
Întrucat soluția presupune fixarea capetelor unui subinterval (i, j), apoi alegerea unui intermediar (k), complexitatea este dată de aceste 3 cicluri.
Categoria 4: NUMĂRAT
Aceste recurențe au o oarecare asemănare:
Sfaturi / Reguli
când căutați o recurență pentru o problema de numărare trebuie să aveți grijă la două aspecte:
de multe ori, o problemă de numărare implică o partiționare a tuturor posibilelor soluții după un anumit criteriu (relevant). Găsirea criteriului este partea esențială pentru aflarea recurenței.
Regulile de lucru cu clase de resturi
Reamintim câteva proprietăți matematice pe care ar trebui să le aveți în vedere atunci când implementați pentru a obține corect resturile anumitor expresii. (corect poate să însemne, de exemplu, să evitați overflow :D - lucru neintuitiv câteodată).
proprietăți de bază:
$(a + b) \ \% \ MOD = ((a \ \% \ MOD) + (b \ \% \ MOD)) \ \% \ MOD $
$(a \ * b) \ \% \ MOD = ((a \ \% \ MOD) \ * (b \ \% \ MOD)) \ \% \ MOD $
$(a - b) \ \% \ MOD = ((a \ \% \ MOD) - (b \ \% \ MOD) + MOD) \ \% \ MOD $ (restul nu poate fi ceva negativ; în C++ % nu funcționează pe numere negative)
invers modular
Explicații invers modular
Explicații invers modular
definiție : b este inversul modular al lui a în raport cu MOD dacă $ a * b = 1 (modulo \ MOD)$
utilizare : $ \frac{a}{b} \ \% \ MOD = ((a \ \% \ MOD) * (invers(b) \ \% \ MOD)) \ \% \ MOD $
calculare : deoarece la PA această discuție are sens doar în contextul posibilității implementării unei recurențe DP în care folosim resturile doar pentru a evita overflow/imposibilitatea de a reține rezultatul pe tipurile standard de tip int (adică nu ne interesează să dăm o metoda generală pentru invers modular), vom simplifica problema - MOD este prim!!!
Mica teoremă a lui Fermat: Dacă p este un număr prim și a este un număr întreg care nu este multiplu al lui p, atunci $a^{p-1} = 1 (modulo \ p)$.
din definiția inversului modular, reiese că a și b nu sunt multipli ai lui MOD
introducând notațiile noastre în teoremă și prelucrând obținem
$a ^ {MOD - 1} = 1 (modulo \ MOD) <=> a * (a ^{MOD-2}) = 1 (modulo \ MOD)$
deci, inversul modular al lui a (în aceste condiții specifice) este $b = a ^ {MOD -2 }$
Reamintim că prin înmulțirea/adunarea a două variabile de tipul int, rezultatul poate să nu încapă pe 32 biți. E posibil să trebuiască să combinăm regulile de la resturi cu următoarele:
Gardurile lui Gigel
Enunț
Gigel trece de la furat obiecte cu un rucsac la numărat garduri (fiecare are micile lui plăceri :D). El dorește să construiască un gard folosind în mod repetat un singur tip de piesă.
O piesă are dimensiunile 4 x 1 (o unitate = 1m). Din motive irelevante pentru această problema, orice gard construit trebuie să aibă înălțimea 4m în orice punct.
O piesă poate fi pusă în poziție orizontală sau în poziție verticală.
Cerință
Gigel se întreabă câte garduri de lungime n și înălțime 4 există? Deoarece celălalt prenume al lui este Bulănel, el intuiește că acest număr este foarte mare, de aceea va cere restul împărțirii acestui numar la 1009.
$n = 1$ sau $n = 2$ sau $n = 3$
Răspuns: 1 (un singur gard)
Explicație: Se poate forma un singur gard în fiecare caz, după cum este ilustrat și în figura Garduri_123.
$n = 4$
Răspuns: 2
Explicație: Se pot forma 2 garduri, în funcție de cum așezăm piesele, după cum este ilustrat și în figura Garduri_4.
Observăm că de fiecare dată când punem o piesă în poziție orizontală, de fapt suntem obligați să punem 4 piese, una peste alta!
$n = 5$
Răspuns: 3
Explicație: Se pot forma 3 garduri, în funcție de cum așezăm piesele, după cum este ilustrat și în figura Garduri_5.
dacă dorim ca acest gard să se termine cu 4 piese în poziție orizontală (una peste alta - marcat cu roșu), atunci la stânga mai ramane de completat un subgard de lungime 1, în toate modurile posibile
dacă dorim ca acest gard să se termine cu o piesă în poziție verticală (marcat cu roșu), atunci la stânga mai rămâne de completat un subgard de lungime 4, în toate modurile posibile
$n = 6$
Răspuns: 4
Explicație: Se pot forma 4 garduri, în funcție de cum așezăm piesele, după cum este ilustrat și în figura Garduri_6.
dacă dorim ca acest gard să se termine cu o piesă în poziție verticală (marcat cu roșu), atunci la stânga mai rămâne de completat un subgard de lungime 5, în toate modurile posibile
dacă dorim ca acest gard să se termine cu 4 piese în poziție orizontală (una peste alta - marcat cu roșu), atunci la stânga mai ramane de completat un subgard de lungime 2, în toate modurile posibile
Recurență
Numire recurență
$dp[i] $ = numărul de garduri de lungime i și înălțime 4 (nimic special - exact ceea ce se cere în enunț)
Răspunsul la problemă este $dp[n]$.
Găsire recurență
Așa cum am zis în secțiunea de
sfaturi și reguli vrem să facem o
parționare după un anumit
criteriu: în cazul problemei de față, criteriul de parționare este dacă gardul se termină cu o scândură verticală sau orizontală.
De asemenea, tot în secțiunea sfaturi și reguli am precizat că nu vrem să număram un obiect (un mod de a construi gardul) de două ori. Recurența noastră (dp[i] = dp[i-1] + dp[i-4]) nu ia un obiect de două ori pentru că orice soluție care vine din dp[i-4] e diferită de alta care vine din dp[i-1] pentru că diferă în cel puțin ultima scândură așezată)
Implementare recurență
Aici puteți vedea un exemplu simplu de implementare în C++.
#define MOD 1009
int gardurile_lui_Gigel(int n) {
// cazurile de bază
if (n <= 3) return 1;
if (n == 4) return 2;
vector<int> dp(n + 1); // păstrez indexarea de la 1 ca în explicații
// cazurile de bază
dp[1] = dp[2] = dp[3] = 1;
dp[4] = 2;
// cazul general
for (int i = 5; i <= n; ++i) {
dp[i] = (dp[i - 1] + dp[i - 4]) % MOD;
}
return dp[n];
}
Menționez că am folosit expresia $dp[i] = (dp[i - 1] + dp[i - 4]) \ \% \ MOD$ în loc de $dp[i] = ((dp[i - 1] \ \% \ MOD) + (dp[i - 4] \ \% \ MOD)) \ \% \ MOD$, deoarece, pe valorile anterior calculate în dp, a fost deja aplicată operația $%$.
Am plecat cu numerele $1, 1, 1, 2$ și, la fiecare pas, rezultatul stocat este $\ \% \ MOD$, deci, tot ce este stocat deja în dp este un rest în raport cu MOD. NU mai era nevoie, deci, să aplicăm % și pe termenii din paranteză.
Complexitate
Tehnici folosite în DP
De multe ori, este nevoie să folosim câteva tehnici pentru a obține performanța maximă cu recurența găsită.
În prima parte a laboratorului 3 se menționa tehnica de memoizare. În acesta, ne vom rezuma la cum putem folosi cunoștințele de lucru matriceal pentru a favoriza implementarea unor anumite tipuri de recurențe.
Exponențiere pe matrice pentru recurențe liniare
Recurențe liniare
O recurență liniară, în contextul laboratorului de DP, este de forma:
O astfel de recurență ar însemna că, pentru a calcula costul problemei i, îmbinăm costurile problemelor $i - 1, i - 2, ...., i - k$, fiecare contribuind cu un anumit coeficient $c_{1}, c_{2}, ..., c_{k}$.
Complexitate recurențe liniară
Complexitate recurențe liniară
Presupunând că nu mai există alte specificații ale problemei și că, având cele KMAX cazuri de bază, (primele KMAX valori ar trebui știute/deduse prin alte reguli), atunci un algoritm poate implementa recurența de mai sus folosind 2 cicluri de tip: for (for i = 1 : n, for k = 1 : KMAX …).
Exponențiere pe matrice
Facem următoarele notații:
$S_i$ = starea la pasul i
$S_i = (dp[i - k + 1], dp[i - k + 2], ..., dp[i - 1], dp[i])$
$S_k$ = starea inițială (în care cunoaște cele k cazuri de bază)
$S_k = (dp[1], dp[2], ..., dp[k-1], dp[k])$
$C$ = matrice de coeficienți constanți
are dimensiune $KMAX * KMAX$
putem pune constante în clar
putem pune constantele $c_k$ care țin de problema curentă
Algoritm naiv
Putem formula problema astfel:
Determinare C
Pentru a determina elementele matricei C, trebuie să ne uităm la înmulțirea matriceală de mai sus și să alegem elementele lui C astfel încât prin înmulțirea lui $S_{i-1}$ cu $C$ să obținem elementele din $S_i$.
\begin{gather}
\begin{bmatrix} dp[i - k + 1] & ... & dp[i-1] & dp[i] \\ \end{bmatrix} =
\begin{bmatrix} dp[i - k] & ... & dp[i-2] & dp[i-1] \\ \end{bmatrix}
\begin{bmatrix}
0 & 0 &... & 0 & 0 & c_{k}\\
1 & 0 &... & 0 & 0 & c_{k-1}\\
0 & 1 &... & 0 & 0 & c_{k-2}\\
... & ... & ... & ... & ...\\
0 & 0 &... & 1 & 0 & c_{2}\\
0 & 0 &... & 0 & 1 & c_{1}\\
\end{bmatrix}
\end{gather}
Exponențiere logaritmică pe matrice
Algoritmul naiv de mai sus are dezavantajul că are tot o complexitate temporală $O(n)$.
Să executăm câțiva pași de inducție pentru a vedea cum este determinat $S_i$.
$$S_i = S_{i-1}C$$
$$S_i = S_{i-2}C^2$$
$$S_i = S_{i-3}C^3$$
$$...$$
$$S_i = S_{k}C^{i -k}$$
În laboratorul 2 (Divide et Impera) am învățat că putem calcula $x ^ n$ în timp logaritmic. Deoarece și înmulțirea matricilor este asociativă, putem calcula $C ^ n$ in timp logaritmic.
Obținem astfel o soluție cu următoarele complexități:
Observație! În ultimele calcule nu am șters constanta KMAX, întrucât apare la puterea a 3-a! $KMAX = 100$ implică $KMAX^3 = 10^6$, valoare care nu mai poate fi ignorată în practică ($KMAX^3$ poate fi comparabil cu n).
Gardurile lui Gigel (optimizare)
După cum am văzut mai sus, în problema cu garduri dată de Gigel, soluția este o recurență liniară:
Exponențiere rapidă
$ k = 4 $
$S_4 = (dp[1], dp[2], dp[3], dp[4]) = (1, 1, 1, 4)$
$S_i = (dp[i-3], dp[i-2], dp[i-1], dp[i])$
Răspunsul se află efectuând operația $S_n = S_4 * C^{n - 4}$, unde C are următorul conținut:
\begin{gather}
C = \begin{bmatrix}
0 & 0 & 0 & 1\\
1 & 0 & 0 & 0\\
0 & 1 & 0 & 0\\
0 & 0 & 1 & 1\\
\end{bmatrix}
\end{gather}
Mai jos se află o implementare simplistă în C++ care cuprinde toate etapele pe care trebuie să le realizați în cod, după ce știți cum arată recurența sub forma matriceală.
#define MOD 1009
#define KMAX 4
// C = A * B
void multiply_matrix(int A[KMAX][KMAX], int B[KMAX][KMAX], int C[KMAX][KMAX]) {
int tmp[KMAX][KMAX];
// tmp = A * B
for (int i = 0; i < KMAX; ++i) {
for (int j = 0; j < KMAX; ++j) {
unsigned long long sum = 0; // presupun ca suma incape pe 64 de biti
for (int k = 0; k < KMAX; ++k) {
sum += 1LL * A[i][k] * B[k][j];
}
tmp[i][j] = sum % MOD;
}
}
// C = tmp
memcpy(C, tmp, sizeof(tmp));
}
// R = C^p
void power_matrix(int C[KMAX][KMAX], int p, int R[KMAX][KMAX]) {
// tmp = I (matricea identitate)
int tmp[KMAX][KMAX];
for (int i = 0; i < KMAX; ++i) {
for (int j = 0; j < KMAX; ++j) {
tmp[i][j] = (i == j) ? 1 : 0;
}
}
while (p != 1) {
if (p % 2 == 0) {
multiply_matrix(C, C, C); // C = C*C
p /= 2; // ramane de calculat C^(p/2)
} else {
// reduc la cazul anterior:
multiply_matrix(tmp, C, tmp); // tmp = tmp*C
--p; // ramane de calculat C^(p-1)
}
}
// avem o parte din rezultat in C si o parte in tmp
multiply_matrix(C, tmp, R); // rezultat = tmp * C
}
int garduri_rapide(int n) {
// cazurile de baza
if (n <= 3) return 1;
if (n == 4) return 2;
// construiesc matricea C
int C[KMAX][KMAX] = { {0, 0, 0, 1},
{1, 0, 0, 0},
{0, 1, 0, 0},
{0, 0, 1, 1}};
// vreau sa aplic formula S_n = S_4 * C^(n-4)
// C = C^(n-4)
power_matrix(C, n - 4, C);
// sol = S_4 * C = dp[n] (se afla pe ultima pozitie din S_n,
// deci voi folosi ultima coloana din C)
int sol = 1 * C[0][3] + 1 * C[1][3] + 1 * C[2][3] + 2 * C[3][3];
return sol % MOD;
}
Remarcati faptul ca in functia de inmultire se foloseste o matrice temporara $tmp$. Motivul este ca vrem sa apelam functia $multiply(C, C, C)$, unde C joaca atat rol de intrare cat si de iesire. Daca am pune rezultatele direct in C, atunci am strica inputul inainte sa obtinem rezultatul.
Putem spune ca acea functie este matrix_multiply_safe, in sensul ca pentru orice A,B,C care respecta dimensiunile impuse, functia va calcula corect produsul.
Comparatie solutii (studiu de caz pentru curiosi)
Comparatie solutii (studiu de caz pentru curiosi)
In arhiva demo-lab04.zip gasiti o sursa completa in care se realizeaza:
test case: varianta simpla
n = 100000000 sol = 119; time = 0.984545 s
test case: varianta rapida
n = 100000000 sol = 119; time = 0.000021 s
test case: varianta simpla
n = 1000000000 sol = 812; time = 9.662377 s
test case: varianta rapida
n = 1000000000 sol = 812; time = 0.000022 s
Exercitii
DP or math?
Fie un sir de numere naturale strict pozitive. Cate subsiruri (submultimi nevide) au suma numerelor para?
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$.
Task-uri:
Se cere o solutie folosind DP.
Inspectand recurenta gasita la punctul precedent, incercati sa o inlocuiti cu o formula matematica.
Care este complexitatea pentru fiecare solutie (timp + spatiu)? Care este mai buna? De ce? :D
Deoarece rezultatul poate fi prea mare, se cere restul impartirii lui la $1000000007$ ($10^9 + 7$).
Pentru punctaj maxim pentru aceasta problema, este necesar sa rezolvati toate subpunctele (ex. nu folositi direct formula, gasiti mai intai recurenta DP). Trebuie sa implementati cel putin solutia cu DP.
$n = 3$
Raspuns: $7$
Explicatie: Toate subsirurile posibile sunt
$[2]$
$[2, 6]$
$[2, 6, 4]$
$[2, 4]$
$[6]$
$[6, 4]$
$[4]$
Toate subsirurile de mai sus au suma para.
$n = 3$
Raspuns: $3$
Explicatie: Toate subsirurile posibile sunt
$[2]$
$[2, 1]$
$[2, 1, 3]$
$[2, 3]$
$[1]$
$[1, 3]$
$[3]$
Subsirurile cu suma para sunt: $[2]$, $[2, 1, 3]$, $[1, 3]$.
$n = 3$
Raspuns: $3$
Explicatie: Toate subsirurile posibile sunt
$[3]$
$[3, 2]$
$[3, 2, 1]$
$[3, 1]$
$[2]$
$[2, 1]$
$[1]$
Subsirurile cu suma para sunt: $[3, 2, 1]$, $[3, 1]$, $[2]$.
Morala: există probleme pentru care găsim o soluție cu DP, dar pentru care pot exista și alte soluții mai bune (am ignorat citirea/afișarea).
In problemele de numarat, exista o sansa buna sa putem gasi (si) o formula matematica, care poate fi implementata intr-un mod mai eficient decat o recurenta DP.
Cate subsiruri au suma impara?
Expresie booleana
Se da o expresie booleana corecta cu n termeni. Fiecare din termeni poate fi unul din stringurile true, false, and, or, xor.
Numarati modurile in care se pot aseza paranteze astfel incat rezultatul sa fie true. Se respecta regulile de la logica (tabelele de adevar pentru operatiile and, or, xor).
Deoarece rezultatul poate fi prea mare, se cere restul impartirii lui la $1000000007$ ($10^9 + 7$).
In schelet vom codifica cu valori de tip char cele 5 stringuri:
false: 'F'
true: 'T'
and: '&'
or: '|'
xor: '^'
Functia pe care va trebui sa o implementati voi va folosi variabilele n (numarul de termeni) si expr (vectorul cu termenii expresiei).
$n = 5$ si $expr = ['T', '&', 'F', '^', 'T']$ (expr = [ true and false xor true])
Raspuns: $2$
Explicatie: Exista 2 moduri corecte de a paranteza expresia astfel incat sa obtinem rezultatul true (1).
Complexitate temporală dorita este $O(n ^ 3)$.
Optional, se pot defini functii ajutatoare precum **is_operand**, **is_operator**, **evaluate**.
Pentru rezolvarea celor doua probleme ganditi-va la ce scrie in sectiunea
Sfaturi / Reguli. Pentru fiecare dintre cele doua probleme facem o
partitionare dupa un anumit criteriu.
Pentru problema DP or math? partitionam toate subsirurile dupa critieriul paritatii sumei subsirului (cate sunt pare/impare).
Pentru problema expresie booleana partitionam toate parantezarile posibile dupa rezultatul lor (cate dau true/false).
Bonus
Asistentul va alege una dintre problemele din sectiunea Extra.
Recomandam sa NU fie una din cele 3 probleme de la Test PA 2017. Recomandam sa le incercati dupa ce recapitulati acasa DP1 si DP2, pentru a verifica daca cunostintele acumulate sunt la nivelul asteptat.
Rezolvati problema Secvente de la Test PA 2017.
Rezolvati pe infoarena problema iepuri.
Hint: Exponentiere logaritmica pe matrice
Solutie:
Pentru punctaj maxim, pentru fiecare test se foloseste ecuatia matriceala atasata.
Complexitate: $O(T * log(n))$.
Rezolvati pe infoarena problema Lacusta.
Rezolvati pe infoarena problema Suma4.
Rezolvati pe infoarena problema subsir.
Rezolvati pe infoarena problema 2sah.
Hint: Exponentiere logaritmica pe matrice
O descrie detaliata se afla in arhiva OJI 2015.
Articolul de pe leetcode conține o listă cu diverse tipuri de probleme de programare dinamică, din toate categoriile discutate la PA.
Referințe