This is an old revision of the document!
Laborator 4: Programare Dinamică (continuare)
Obiective laborator
Precizari initiale
Toate exemplele de cod se gasesc in
demo-lab04.zip.
Acestea apar incorporate si in textul laboratorului pentru a facilita parcurgerea cursiva a laboratorului.
Toate bucatile de cod prezentate in partea introductiva a laboratorului (inainte de exercitii) 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 sa nu fie de ajuns pentru a compila codul.
Va rugam sa incercati si codul din arhiva demo-lab04.zip, inainte de a raporta ca ceva nu merge. :D
Pentru orice problema legata de continutul acestei pagini, va rugam sa dati email unuia dintre responsabili.
Ce este DP?
Similar cu greedy, tehnica de programare dinamica este folosită pentru rezolvarea problemelor de optimizare.
In continuare vom folosi acronimul DP (dynamic programming).
De asemenea, DP se poate folosi si pentru probleme in care nu cautam un optim, cum ar fi problemele de numarare.
Pentru restul notiunilor prezentate pana acum despre DP, va rugam sa consulati pagina laboratorului 3.
Exemple clasice
Programarea Dinamică este cea mai flexibilă tehnica din programare. Cel mai ușor mod de a o înțelege presupune parcurgerea cât mai multor exemple.
Propunem cateva categorii de recurente, pe care le vom grupa astfel:
Pentru o problema data, este posibil sa gasim mai multe recurente corecte (mai multe solutii posibile). Evident, criteriul de alegere intre acestea va fi cel bazat pe complexitate.
Categoria 3: PODM
Aceste recurente au o oarecare asemanare cu problema PODM (enunt + solutie).
ATENTIE! Acest tip de recurente poate fi mai greu (decat celelalte). Puteti consulta acasa materialele puse la dispozitie pentru intelege mai bine aceasta categorie.
Caracteristici:
Acest tip de problema presupune ca o putem formula ca pe o problema de tip subinterval $[i, j]$.
Daca dorim sa gasim optimul pentru acest interval, va trebuie sa luam in calcul toate combinatiile de 2 subproblemele care ar fi putut genera solutie pentru probleme $[i, j]$.
Se considera fiecare divizare in 2 subprobleme, data de intermediarul k
$[i, k]$ si $[k + 1, j]$ sunt cele 2 subprobleme pentru care cunoastem solutiile
atunci o solutie pentru $[i,j]$ se poate obtine imbinandu-le pe cele doua
ca sa gasim solutia cea mai buna
Calculul se face de la intervale mici (probleme usoare - $[i,i]$ sau $[i, i+1]$) spre probleme generale (dimensiune generala - $[i, j]$ ). In final se ajunge si la dimensiunile initiale ($[1, n]$).
Exemple clasice
PODM
Enunt
Fie un produs matricial $M = M_1 M_2 ... M_n$. Putem pune paranteze in mai multe moduri si vom obtine acelasi rezultat (inmultire asociativa), dar este posibil sa obtinem numar diferit de inmultiri scalare .
Matricea $M_i$ are (prin conventie), dimensiunile $d_{i-1} d_{i}$.
Cerinta
Se cere sa se gaseasca o parantezare optima de matrice (PODM), adica sa se gaseasca o parantezare care sa minimizeze numarul de inmultiri scalare.
Exemple
$n = 3$
Raspuns: 64 (inmultiri scalare)
Explicatie: Avem 3 matrici:
A de dimensiuni (2, 3)
B de (3, 4)
C de (4, 5)
In functie de ordinea efectuarii inmultirilor matriciale , numarul total de inmultiri scalare poate sa fie foarte diferit:
Rezultatul optim se obtine pentru cea de a treia parantezare: $(AB)C$.
$n = 4$
Raspuns: 48 (inmultiri scalare)
Explicatie: Avem 4 matrici:
A de dimensiuni (2, 3)
B de (3, 4)
C de (4, 2)
D de (2, 3)
In functie de ordinea efectuarii inmultirilor matriciale, numarul total de inmultiri scalare poate sa fie foarte diferit:
$(AB)C)D$ ⇒ $24 + 16 + 12 = 52$ inmultiri
$(A(BC))D$ ⇒ $24 + 12 + 12 = 48$ inmultiri
$(AB)(CD)$ ⇒ $ = $ inmultiri
$A((BC)D)$ ⇒ $24 + 18 + 27 = 69$ inmultiri
$A(B(CD))$ ⇒ $24 + 36 + 18 = 78$ inmultiri
Rezultatul optim se obtine pentru cea de a treia parantezare: $((A(BC))D)$.
$n = 4$
Raspuns: 2856 (inmultiri scalare)
Explicatie: Avem 4 matrici:
A de dimensiuni (13, 5)
B de (5, 89)
C de (89, 3)
D de (3, 34)
In functie de ordinea efectuarii inmultirilor matriciale , numarul total de inmultiri scalare poate sa fie foarte diferit:
$((AB)C)D$ ⇒ 10582 inmultiri
$(AB)(CD)$ ⇒ 54201 inmultiri
$(A(BC))D$ ⇒ 2856 inmultiri
$A((BC)D)$ ⇒ 4055 inmultiri
…
Rezultatul optim se obtine pentru cea de a treia parantezare: $(A(BC))D$.
TIPAR
A fost descris in detaliu mai sus (cand s-a vorbit de categorie).
Numire recurenta
$dp[i][j]$ = numarul minim de inmultiri scalare cu care se poate obtine produsul $M_i * M_{i+1} * ... *{M_j}$
Gasire recurenta
Implementare
Puteti rezolva si testa problema PODM pe infoarena aici.
Un exemplu de implementare in C++ se gaseste mai jos.
// kInf este valoarea maxima - "infinitul" nostru
const unsigned long long kInf = std::numeric_limits<unsigned long long>::max();
// T = O(n ^ 3)
// S = O(n ^ 2) - stocam n x n intregi in tabloul dp
unsigned long long solve_podm(int n, const vector<int> &d) {
// dp[i][j] = numarul MINIM inmultiri scalare cu codare poate fi calculat produsul
// matricial M_i * M_i+1 * ... * M_j
vector<vector<unsigned long long>> dp(n + 1, vector<unsigned long long> (n + 1, kInf));
// Cazul de baza 1: nu am ce inmulti
for (int i = 1; i <= n; ++i) {
dp[i][i] = 0ULL; // 0 pe unsigned long long (voi folosi mai incolo si 1ULL)
}
// Cazul de baza 2: matrice d[i - 1] x d[i] inmultia cu matrice d[i] x d[i + 1]
// (matrice pe pozitii 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) { // fixam lungimea intervalului (2, 3, 4, ...)
for (int i = 1; i + len - 1 <= n; ++i) { // fixam capatul din stanga: i
int j = i + len - 1; // capatul din dreapta se deduce: j
// Iteram prin indicii dintre capete, spargand sirul de inmultiri in doua (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];
// actualizam solutia daca este mai buna
dp[i][j] = min(dp[i][j], new_sol);
}
}
}
// Rezultatul se afla in dp[1][n]: Numarul MINIM de inmultiri scalare
// pe care trebuie sa le facem pentru a obtine produsul M_1 * ... * M_n
return dp[1][n];
}
Sursa a fost scrisa pentru a fi testata pe infoarena. In cazul problemei
PODM, deoarece avem o suma de foarte multe produse, rezultatul este foarte mare. Pe infoarena se cerea ca rezultatul sa fie afisat asa cum e, garantandu-se ca incape pe 64 biti.
Reamintim ca prin inmultirea/adunarea a doua variabile de tipul int, rezultatul poate sa nu incapa pe 32 biti. De aceea, in solutia prezentata, s-a facut cast pe 64biti.
ATENTIE! La PA, in general, vom folosi conventia $ expresie \ \% \ kMod $, care va fi detaliata in capitolul urmatorul din acest laborator.
Complexitate
Intrucat solutia presupune fixarea capetelor unui subinterval (i, j), apoi alegerea unui intermediar (k), complexitatea este data de aceste 3 cicluri.
Categoria 4: NUMARAT
Aceste recurente au o oarecare asemanare:
Sfaturi / Reguli
cand cautati o recurenta pentru o problema de numarare trebuie sa aveti grija la doua aspecte:
de multe ori o problema de numarare implica o partitionare a tuturor posibilelor solutii dupa un anumit criteriu (relevant). Gasirea criteriului este partea esentiala pentru gasirea recurentei.
Regulile de lucru cu clase de resturi
Reamintim cateva proprietati matematice pe care ar trebui sa le aveti in vedere atunci in implementati pentru obtine corect resturile pentru anumite expresii. (corect poate sa insemne, de exemplu, sa evitati overflow :D - lucru neintuitiv cateodata).
proprietati de de baza
$(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; in C++ % nu functioneaza pe numere negative)
invers modular
Explicatii invers modular
Explicatii invers modular
definitie : b este inversul modular a lui a in raport cu MOD daca $ a * b = 1 (modulo \ MOD)$
utilizare : $ \frac{a}{b} \ \% \ MOD = ((a \ \% \ MOD) * (invers(b) \ \% \ MOD)) \ \% \ MOD $
calculare : deoarece la PA aceasta discutie are sens doar in contextul posibilitatii implementarii unei recurente DP in care folosim resturile doar pentru a evita overflow/imposibilitatea de a retine rezultatul pe tipurile standard de tip int (adica nu ne intereseaza sa dam o metoda generala pentru invers modular), vom simplifica problema - MOD este prim!!!
Mica teorema a lui Fermat: Daca p este un numar prim si a este un număr intreg care nu este multiplu al lui p, atunci $a^{p-1} = 1 (modulo \ p)$.
din definitia inversului modular, reiese ca a si b nu sunt multipli ai lui MOD
introducand notatiile noastre in teorema si prelucrand obtinem
Reamintim ca prin inmultirea/adunarea a doua variabile de tipul int, rezultatul poate sa nu incapa pe 32 biti. E posibil sa trebuiasca sa combinam regulile cu resturi cu urmatoarele:
Gardurile lui Gigel
Enunt
Gigel trece de la furat obiecte cu un rucsac la numarat garduri (fiecare are micile lui placeri :D). El doreste sa construiasca un gard folosind in mod repetat un singur tip de piesa.
O piesa are dimensiunile 4 x 1 (o unitate = 1m). Din motive irelevante pentru aceasta problema, orice gard construit trebuie sa aiba inaltime 4m in orice punct.
O piesa poate fi pusa in pozitie orizontala sau in pozitie verticala.
Cerinta
Gigel se intreaba cate garduri de lungime n si inaltime 4 exista? Deoarece celalalt prenume al lui este Bulănel, el intuieste ca acest numar este foarte mare, de aceea va cere restul impartirii acestui numar la 1009.
$n = 1$ sau $n = 2$ sau $n = 3$
Raspuns: 1 (un singur gard)
Explicatie: Se poate forma un singur gard in fiecare caz, dupa cum este ilustrat si in figura Garduri_123.
$n = 4$
Raspuns: 2
Explicatie: Se pot forma 2 garduri, in functie de cum asezam piesele, dupa cum este ilustrat si in figura Garduri_4.
Observam ca de fiecare daca cand punem o piesa in pozitie orizontala, de fapt suntem obligati sa punem 4 piese, una peste alta!
$n = 5$
Raspuns: 3
Explicatie: Se pot forma 3 garduri, in functie de cum asezam piesele, dupa cum este ilustrat si in figura Garduri_5.
daca dorim ca acest gard sa se termine cu 4 piese in pozitie orizontala (una peste alta - marcat cu rosu), atunci la stanga mai ramane de completat un subgard de lungime 1, in toate modurile posibile
daca dorim ca acest gard sa se termine cu o piesa in pozitie verticala (marcat cu rosu), atunci la stanga mai ramane de completat un subgard de lungime 4, in toate modurile posibile
$n = 6$
Raspuns: 4
Explicatie: Se pot forma 4 garduri, in functie de cum asezam piesele, dupa cum este ilustrat si in figura Garduri_6.
daca dorim ca acest gard sa se termine cu o piesa in pozitie verticala (marcat cu rosu), atunci la stanga mai ramane de completat un subgard de lungime 5, in toate modurile posibile
daca dorim ca acest gard sa se termine cu 4 piese in pozitie orizontala (una peste alta - marcat cu rosu), atunci la stanga mai ramane de completat un subgard de lungime 2, in toate modurile posibile
Recurenta
Numire recurenta
$dp[i] $ = numarul de garduri de lungime i si inaltime 4 (nimic special - exact ceeea ce se cere in enunt)
Raspunsul la problema este $dp[n]$.
Gasire recurenta
Asa cum am zis in sectiunea de
sfaturi si reguli vrem sa facem o
partionare dupa un anumit
criteriu, in cazul problemei de fata criteriul de partionare este daca gardul se termina cu o scandura verticala sau orizontala.
De asemenea tot in sectiunea sfaturi si reguli am precizat ca nu vrem sa numaram un obiect (un mod de a construi gardul) de doua ori. Recurenta noastra (dp[i] = dp[i-1] + dp[i-4]) nu ia un obiect de doua ori pentru ca orice solutie care vine din dp[i-4] e diferita de alta care vine din dp[i-1] pentru ca difera in cel putin ultima scandura asezata)
Implementare recurenta
Aici puteti vedea un exemplu simplu de implementare in C++.
#define MOD 1009
int gardurile_lui_Gigel(int n) {
// cazurile de baza
if (n <= 3) return 1;
if (n == 4) return 2;
vector<int> dp(n + 1); // pastrez indexarea de la 1 ca in explicatii
// cazurile de baza
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];
}
Mentionez ca am folosit expresia $dp[i] = (dp[i - 1] + dp[i - 4]) \ \% \ MOD$ in loc de $dp[i] = ((dp[i - 1] \ \% \ MOD) + (dp[i - 4] \ \% \ MOD)) \ \% \ MOD$, deoarece [e valorile anterior calcule in dp a fost deja aplicata operatia $%$.
Am plecat cu numerele $1, 1, 1, 2$ si la fiecare pas rezultatul stocat este $\ \% \ MOD$, deci tot ce este stocat deja in dp este un rest in raport cu MOD. NU mai era nevoie deci sa aplica % si pe termenii din paranteza.
Complexitate
Tehnici folosite in DP
De multe ori este nevoie sa folosim cateva tehnici pentru a obtine performanta maxima cu recurenta gasita.
In laboratorul 3 se mentiona tehnica de memoizare (in prima parte a laboratorului). In acesta ne vom rezuma la cum putem folosi cunostintele de lucru matricial pentru a favoriza implementarea unor anumite tipuri de recurente.
Exponentiere pe matrice pentru recurente liniare
Recurente liniare
O recurenta liniara in contextul laboratorului de DP este de forma:
O astfel de recurenta ar insemna ca pentru a calcula costul problemei i , imbinam costurile problemelor $i - 1, i-2, ...., i-k$, fiecare contribuind cu un anumit coeficient $c_{1}, c_{2}, ..., c_{k}$.
Complexitate recurente liniara
Complexitate recurente liniara
Presupunand ca nu mai exista alte specificatii ale problemei si ca avand cele KMAX cazuri de baza (primele KMAX valori ar trebui stiute/deduse prin alte reguli), atunci un algoritm poate implementa recurenta de mai sus folosind 2 cicluri de tip for (for i = 1 : n, for k = 1 : KMAX …).
Exponentiere pe matrice
Facem urmatoarele notatii:
$S_i$ = starea la pasul i
$S_i = (dp[i - k + 1], dp[i - k + 2], ..., dp[i - 1], dp[i])$
$S_k$ = starea initiala (in care cunoaste cele k cazuri de baza)
$S_k = (dp[1], dp[2], ..., dp[k-1], dp[k])$
$C$ = matrice ce coeficienti constanti
are dimensiune $KMAX * KMAX$
putem pune constante in clar
putem pune constantele $c_k$ care tin de problema curenta
Algoritm naiv
Putem formula problema astfel:
Determinare C
Pentru a determina elementele matricei C, trebuie sa ne uitam la inmultirea matriceala de mai sus si sa alegem elementele lui C astfel incat prin inmultirealui $S_{i-1}$ cu $C$ sa obtinem 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}
Exponentiere logaritmica pe matrice
Algoritmul naiv de mai sus are dezavantajul ca are tot o complexitate temporala $O(n)$.
Sa executam cativa pasi de inductie 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}$$
In laboratorul 2 (Divide et Impera) am invatat ca putem calcula $ x ^ n $ in timp logaritmic. Deoarece si inmultirea matricilor este asociativa, putem calcula $C ^ n$ in timp logaritmic.
Obtinem astfel o solutie cu urmatoarele complexitati:
Observatie! In ultimele calcule nu am sters contanta KMAX, intrucat apare la puterea a 3-a! $KMAX = 100$ implica $KMAX^3 = 10^6$, valoare care nu mai poate fi ignorata in practica ($KMAX^3$ poate fi comparabil cu n).
Gardurile lui Gigel (optimizare)
Dupa cum am vazut mai sus, in problema cu garduri data de Gigel solutia este o recurenta liniara:
Exponentiere rapida :p
$ 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])$
Raspunsul se afla efectuand operatia $S_n = S_4 * C^{n - 4}$, unde C are urmatorul continut:
\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 afla o implementare simplista in C++ care cuprinde toate etapele pe care trebuie sa le realizati in cod, dupa ce stiti cum arata recurenta sub forma matriceala.
#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
In acest laborator vom folosi scheletul de laborator din arhiva
skel-lab04.zip.
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 subsirule 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 subsirule 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 subsirule 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: exista probleme pentru care gasim o solutie cu DP, dar pentru care poate exista si alte solutii mai bune (am ignorat citirea).
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.
Dar cate subsiruri au suma impara?
Problema este preluata de aici. Solutia se gaseste aici.
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.
Referințe