This shows you the differences between two versions of the page.
|
pa:laboratoare:laborator-02 [2021/03/08 22:00] mara_ioana.nicolae [Rucsac] |
pa:laboratoare:laborator-02 [2026/03/15 19:10] (current) aureliu.antonie [Gardurile lui Gigel] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Laborator 02 : Greedy====== | + | ====== Laborator 02: Programare Dinamică (2/2) ====== |
| - | Responsabili: | + | |
| - | * [[neatudarius@gmail.com|Darius-Florentin Neațu (2017-2021)]] | + | |
| - | * [[radunichita99@gmail.com | Radu Nichita (2021)]] | + | |
| - | * [[cristianolaru99@gmail.com | Cristian Olaru (2021)]] | + | |
| - | * [[mirunaelena.banu@gmail.com | Miruna-Elena Banu (2021)]] | + | |
| - | * [[maraioana9967@gmail.com | Mara-Ioana Nicolae (2021)]] | + | |
| - | * [[stefanpopa2209@gmail.com | Ștefan Popa (2018-2020)]] | + | |
| - | Autori: | ||
| - | * [[rotarualexandruandrei94@gmail.com| Alex Rotaru (2018)]] | ||
| - | * [[neatudarius@gmail.com|Darius-Florentin Neațu (2018)]] | ||
| - | * [[visanr95@gmail.com|Radu Vișan (2018)]] | ||
| - | * [[cristb@gmail.com|Cristian Banu (2018)]] | ||
| - | | ||
| ===== Obiective laborator ===== | ===== Obiective laborator ===== | ||
| - | *Înțelegerea noțiunilor de bază legate de tehnica greedy | + | * Înțelegerea noțiunilor de bază despre programarea dinamică. |
| - | *Însușirea abilităților de implementare a algoritmilor bazați pe greedy | + | * Însușirea abilităților de implementare a algoritmilor bazați pe programarea dinamică. |
| ===== Precizări inițiale ===== | ===== Precizări inițiale ===== | ||
| <note> | <note> | ||
| - | Toate exemplele de cod se găsesc pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/demo/lab02|pa-lab::demo/lab02]]. | + | Toate exemplele de cod se găsesc pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/algorithms/lab02|pa-lab/algorithms/lab02]]. |
| 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. | 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. | ||
| </note> | </note> | ||
| - | * 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. | + | * 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. | * 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. | * Pentru orice problemă legată de conținutul acestei pagini, vă rugam să dați e-mail unuia dintre responsabili. | ||
| - | ===== Importanță – aplicații practice ===== | + | ===== Ce este DP? ===== |
| - | În general tehnicile de tip greedy sau programare dinamică (lab04) sunt folosite pentru rezolvarea problemelor de optimizare. Acestea pot adresa probleme în sine sau pot fi subprobleme dintr-un algoritm mai mare. De exemplu, algoritmul Dijkstra pentru determinarea drumului minim pe un graf alege la fiecare pas un nod nou urmărind algoritmul greedy. | + | Similar cu greedy, tehnica de programare dinamică este folosită pentru rezolvarea **problemelor de optimizare**. |
| + | În continuare vom folosi acronimul **DP (dynamic programming)**. | ||
| - | Există însă probleme care ne pot induce în eroare. Astfel, există probleme în care urmărind criteriul greedy nu ajungem la soluția optimă. Este foarte important să identificăm cazurile când se poate aplica greedy și cazurile când este nevoie de altceva. Alteori această soluție neoptimă este o aproximare suficientă pentru ce avem nevoie. Problemele NP-complete necesita multă putere de calcul pentru a găsi optimul absolut. Pentru a optimiza aceste calcule mulți algoritmi folosesc decizii greedy și găsesc un optim foarte aproape de cel absolut. | ||
| - | ===== Greedy ===== | + | De asemenea, DP se poate folosi și pentru probleme în care nu căutam un optim, cum ar fi **problemele de numărare**. |
| - | “greedy” = “lacom”. Algoritmii de tip greedy vor să construiască într-un mod cât mai rapid soluția unei probleme. Ei se caracterizează prin luarea unor decizii rapide care duc la găsirea unei potențiale soluții a problemei. Nu întotdeauna asemenea decizii rapide duc la o soluție optimă; astfel ne vom concentra atenția pe identificarea acelor anumite tipuri de probleme pentru care se pot obține soluții optime. | + | Pentru noțiunile prezentate până acum despre DP, vă rugăm să consultați pagina [[pa:laboratoare:laborator-01|laboratorului 01]]. |
| - | În general există mai multe soluții posibile ale problemei. Dintre acestea se pot selecta doar anumite soluții optime, conform unor anumite criterii. Algoritmii greedy se numără printre cei mai direcți algoritmi posibili. Ideea de bază este simplă: având o problema de optimizare, de calcul al unui cost minim sau maxim, se va alege la fiecare pas decizia cea mai favorabilă, fără a evalua global eficiența soluţiei. Scopul este de a găsi una dintre acestea sau dacă nu este posibil, atunci o soluție cât mai apropiată, conform criteriului optimal impus. | + | ===== Exemple clasice ===== |
| - | Trebuie înțeles faptul că rezultatul obținut este optim doar dacă un optim local conduce la un optim global. În cazul în care deciziile de la un pas influențează lista de decizii de la pasul următor, este posibilă obținerea unei valori neoptimale. În astfel de cazuri, pentru găsirea unui optim absolut se ajunge la soluții supra-polinomiale. De aceea, dacă se optează pentru o astfel de soluție, algoritmul trebuie însoțit de o demonstrație de corectitudine. | + | 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. |
| - | Descrierea formală a unui algoritm greedy este următoarea: | + | |
| - | <code cpp> | + | Propunem câteva categorii de recurențe pe care le vom grupa astfel: |
| - | // C este mulțimea candidaților | + | * recurențe de tip **SSM** (Subsecvență de Sumă Maximă) |
| - | function greedy(C) { | + | * recurențe de tip **RUCSAC** |
| - | S ← Ø // în S construim soluția | + | * recurențe de tip **PODM** (Parantezare Optimă de Matrice) |
| - | + | * recurențe de tip **numărat** | |
| - | while !solutie(C) and C ≠ Ø | + | * recurențe pe **grafuri** |
| - | x ← un element din C care minimizează/maximizează select(x) | + | |
| - | C ← C \ {x} | + | |
| - | if fezabil( S ∪ {x}) then S ← S∪{x} | + | |
| - | | + | |
| - | return S | + | |
| - | } | + | |
| - | </code> | + | |
| - | Este ușor de înțeles acum de ce acest algoritm se numește ”greedy”: la fiecare pas se alege cel mai bun candidat de la momentul respectiv, fără a studia alternativele disponibile în momentul respectiv şi viabilitatea acestora în timp. | ||
| - | Dacă un candidat este inclus în soluție, rămâne acolo, fără a putea fi modificat, iar dacă este exclus din soluție, nu va mai putea fi niciodată selectat drept un potențial candidat. | + | <note> |
| + | 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. | ||
| + | </note> | ||
| + | ===== Categoria 3: PODM ===== | ||
| + | Aceste recurențe au o oarecare asemănare cu problema PODM (enunț + soluție). | ||
| - | ===== Exemple ===== | ||
| - | ==== Simple task ==== | + | **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. |
| - | === Enunț === | + | Caracteristici: |
| - | Fie un șir de N numere pentru care se cere determinarea unui subșir de numere cu suma maximă. Un subșir al unui șir este format din elemente (nu neapărat consecutive) ale șirului respectiv, în ordinea în care acestea apar în șir. | + | |
| - | <spoiler Exemplu> | + | * Acest tip de problemă presupune că o putem formula ca pe o problemă de tip **subinterval $[i, j]$**. |
| - | Pentru numerele $1, -5, 6, 2, -2, 4$ răspunsul este $1, 6, 2, 4$ (suma 13). | + | * 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]$**). | ||
| + | * Privind imaginea de ansamblu, adică modul în care se completează matricea dp, observăm că aceasta se completează **diagonală cu diagonală**. | ||
| + | |||
| + | ==== 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țirea este 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 == | ||
| + | <spoiler Exemplu 1> | ||
| + | |||
| + | $n = 3$ | ||
| + | |i|0|1|2|3|| | ||
| + | |d|2|3|4|5|| | ||
| + | |||
| + | 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: | ||
| + | * $(AB)C$ => $24 + 40 = 64$ de înmulțiri | ||
| + | * explicație: $X = (AB)$ generează $2 * 3 * 4 = 24$ înmulțiri, $(XC)$ generează $2 * 4 * 5 = 40$ de înmulțiri | ||
| + | * $A(BC)$ => $60 + 30 = 90$ de înmulțiri | ||
| + | * explicație: $X =(BC)$ generează $3 * 4 * 5 = 60$ înmulțiri, $(AX)$ generează $2 * 3 * 5 = 30$ de înmulțiri | ||
| + | |||
| + | Rezultatul optim se obține pentru prima parantezare: $(AB)C$. | ||
| + | |||
| </spoiler> | </spoiler> | ||
| - | === Soluție === | + | <spoiler Exemplu 2> |
| - | Se observă că tot ce avem de făcut este să verificăm fiecare număr dacă este pozitiv sau nu. În cazul pozitiv, îl introducem în subșirul soluție. | + | |
| - | Dacă toate numerele sunt negative, soluția este dată de cel mai mare numar negativ (cel mai mic în modul). | + | $n = 4$ |
| + | |i|0|1|2|3|4| | ||
| + | |d|2|3|4|2|3| | ||
| + | Răspuns: ** 48 ** (înmulțiri scalare) | ||
| - | ==== Problema spectacolelor ==== | + | Explicație: Avem 4 matrice: |
| - | === Enunț === | + | * A de dimensiuni (2, 3) |
| - | Se dau mai multe spectacole, prin timpii de start și timpii de final. Se cere o planificare astfel încât o persoană să poată vedea cât mai multe spectacole. | + | * 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 | ||
| + | * explicație: $X = (AB)$ generează $2 * 3 *4 = 24$ înmulțiri scalare, $Y = (XC)$ generează $2 * 4 * 2 = 16$ înmulțiri scalare, $Z = YD$ generează $2 * 2 *3 = 12$ înmulțiri scalare | ||
| + | * $(A(BC))D$ => $24 + 12 + 12 = 48$ înmulțiri | ||
| + | * explicație: $X = (BC)$ generează $3 * 4 * 2 = 24$ înmulțiri scalare, $Y = (AX)$ generează $ 2 * 3 * 2= 12$ înmulțiri scalare, $Z = YD$ generează 2 * 2 * 3$ = 12$ înmulțiri scalare | ||
| + | * $(AB)(CD)$ => $ = $ inmulțiri | ||
| + | * explicație: $X = (AB)$ generează $2 * 3 * 4 = 24$ înmulțiri scalare, $Y = (CD)$ generează $4 * 2 * 3 = 24$ înmulțiri scalare, $Z = XY$ generează $2 * 4 * 3 = 24$ înmulțiri scalare | ||
| + | * $A((BC)D)$ => $24 + 18 + 27 = 69$ înmulțiri | ||
| + | * explicație: $X = (BC)$ generează $3 * 4 * 2 = 24$ înmulțiri scalare, $Y = (XD)$ generează $3 * 2 * 3 = 18$ înmulțiri scalare, $Z = AY$ generează $3 * 3 * 3 = 27$ înmulțiri scalare | ||
| + | * $A(B(CD))$ => $24 + 36 + 18 = 78$ înmulțiri | ||
| + | * explicație: $X = (CD)$ generează $4 * 2 * 3 = 24$ înmulțiri scalare, $Y = (BX)$ generează $3 * 4 * 3 = 36$ înmulțiri scalare, $Z = AY$ generează $2 * 3 * 3 = 18$ înmulțiri scalare | ||
| + | Rezultatul optim se obține pentru cea de a treia parantezare: $((A(BC))D)$. | ||
| + | |||
| + | </spoiler> | ||
| - | === Soluție === | + | <spoiler Exemplu 3> |
| - | Rezolvarea constă în **sortarea** spectacolelor crescător după timpii de final, apoi la fiecare pas se alege **primul spectacol** care are timpul de start mai mare decât ultimul timp de final. Timpul inițial de final este inițializat la $-\inf$ (spectacolul care se termină cel mai devreme va fi mereu selectat, având timpul de start mai mare decât timpul inițial). | + | |
| - | === Implementare === | + | $n = 4$ |
| - | <spoiler> | + | |i|0|1|2|3|4| |
| + | |d|13|5|89|3|34| | ||
| + | |||
| + | 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$. | ||
| + | |||
| + | </spoiler> | ||
| + | |||
| + | == 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}$ | ||
| + | |||
| + | Răspunsul la problemă este **dp[1][n]** . | ||
| + | |||
| + | == Găsire recurență == | ||
| + | * **Cazul de bază** : | ||
| + | * $dp[i][i] = 0 $ | ||
| + | * NU avem niciun efort dacă nu avem ce înmulți. | ||
| + | * $dp[i][i+1] = d_{i-1} d_{i} d_{i+1}$ | ||
| + | * Dacă avem două matrice, putem doar să le înmulțim. Nu are sens să folosim paranteze. | ||
| + | * Daca înmulțim 2 matrice de dimensiuni $d_{i-1} * d_{i}$ și $d_{i} * d_{i + 1}$, avem costul $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})$, unde $k = i : j - 1$ | ||
| + | * dacă avem de efectuat șirul de înmulțiri $M_i ... M_j$, atunci putem pune paranteze oriunde și să facem înmulțirile astfel $(M_i ... M_k) (M_{k+1} ... M_{j})$ | ||
| + | * costul minim pentru $(M_i ... M_k)$ este $dp[i][k]$ | ||
| + | * costul minim pentru $(M_{k+1} ... M_j)$ este $dp[k + 1][j]$ | ||
| + | * vom avea, în final, de înmulțit 2 matrice de dimensiune $d_{i-1} * d_{k}$ si $d_{k} * d_{j}$, operație care are costul $d_{i-1}d_{k}d_{j}$ | ||
| + | * însumăm cele 3 costuri intermediare | ||
| + | |||
| + | == Implementare == | ||
| + | Puteți rezolva și testa problema PODM pe infoarena [[https://infoarena.ro/problema/podm|aici]]. | ||
| + | |||
| + | Un exemplu de implementare în C++ se găsește mai jos. | ||
| + | <spoiler Implementare C++> | ||
| <code cpp> | <code cpp> | ||
| - | bool end_hour_comp (pair<int, int>& e1, pair<int, int>& e2) { | + | // INF este valoarea maximă - "infinitul" nostru |
| - | // comparam doar dupa ora de sfarsit | + | const auto INF = std::numeric_limits<unsigned long long>::max(); |
| - | return (e1.second < e2.second); | + | |
| - | } | + | |
| - | vector<pair<int, int>> plan(vector<pair<int, int> >& intervals) { | + | // T = O(n ^ 3) |
| - | vector<pair<int, int>> plan; | + | // S = O(n ^ 2) - stocăm n x n întregi în tabloul dp |
| - | // se sorteaza intervalele pe baza orei de sfarsit a spectacolelor | + | unsigned long long solve_podm(int n, const vector<int> &d) { |
| - | sort(intervals.begin(), intervals.end(), end_hour_comp); | + | // 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, INF)); | ||
| + | |||
| + | // 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]; | ||
| + | } | ||
| - | // se ia ultimul spectacol ca terminat la -oo pt a putea incepe cu | + | // Cazul general: |
| - | // cel mai devreme | + | // dp[i][j] = min(dp[i][k] + dp[k + 1][j] + d[i - 1] * d[k] * d[j]), k = i : j - 1 |
| - | int last_end = INT_MIN; // -oo a.k.a -infinit | + | for (int len = 2; len <= n; ++len) { // fixăm lungimea intervalului (2, 3, 4, ...) |
| - | for (auto interval : intervals) { | + | for (int i = 1; i + len - 1 <= n; ++i) { // fixăm capătul din stânga: i |
| - | // daca inceputul intervalului curent este dupa sfarsitul ultimului | + | int j = i + len - 1; // capătul din dreapta se deduce: j |
| - | // spectacol (last_end) il adaugam in lista de spectacole la care | + | |
| - | // se participa | + | // Iterăm prin indicii dintre capete, spărgând șirul de înmulțiri in două (paranteze). |
| - | if (interval.first >= last_end) | + | for (int k = i; k < j; ++k) { |
| - | { | + | // M_i * ... M_j = (M_i * .. * M_k) * (M_k+1 *... * M_j) |
| - | plan.push_back(interval); | + | unsigned long long new_sol = dp[i][k] + dp[k + 1][j] + 1ULL * d[i - 1] * d[k] * d[j]; |
| - | // dupa ce am adaugat un spectacol, updatam ultimul sfarsit de spectacol | + | |
| - | last_end = interval.second; | + | // actualizăm soluția dacă este mai bună |
| + | dp[i][j] = min(dp[i][j], new_sol); | ||
| + | } | ||
| } | } | ||
| } | } | ||
| - | return plan; | + | |
| + | // 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]; | ||
| } | } | ||
| </code> | </code> | ||
| + | <note> | ||
| + | Sursa a fost scrisă pentru a fi testată pe infoarena. În cazul problemei [[https://infoarena.ro/problema/podm | 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. | ||
| + | </note> | ||
| + | |||
| + | |||
| </spoiler> | </spoiler> | ||
| + | <note> | ||
| + | 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. | ||
| + | </note> | ||
| - | === Complexitate === | ||
| - | Soluția va avea următoarele complexități: | ||
| - | * ** complexitate temporală **: $T(n) = O(n * log(n))$ | ||
| - | * explicație | ||
| - | * sortarea are $O(n * log(n))$ | ||
| - | * facem încă o parcurgere în $O(n)$ | ||
| - | * ** complexitate spațială **: depinde de algoritmul de sortare folosit. | ||
| - | ==== Problema florarului ==== | + | <note> |
| + | **ATENȚIE!** La PA, în general, vom folosi convenția $ expresie \ \% \ MOD $, care va fi detaliată în capitolul următor din acest laborator. | ||
| + | </note> | ||
| + | |||
| + | == Complexitate == | ||
| + | Întrucat soluția presupune fixarea capetelor unui subinterval (i, j), apoi alegerea unui intermediar (k), complexitatea este dată de aceste 3 cicluri. | ||
| + | * **complexitate temporală**: $T(n) = O(n^3)$ | ||
| + | * **complexitate spațială**: $S(n) = O(n^2)$ | ||
| + | |||
| + | |||
| + | |||
| + | ===== Categoria 4: NUMĂRAT ===== | ||
| + | Aceste recurențe au o oarecare asemănare: | ||
| + | * toate numară lucruri! :p | ||
| + | * interesante sunt cazurile când numărul căutat este foarte mare (altfel am putea apela la alte metode - ex. generarea tuturor candidaților posibili cu backtracking) | ||
| + | * în acest caz, deoarece numărul poate să nu încapă pe un tip reprezentabil standard (ex. int pe 32/64 de biți), se cere (de obicei) restul împarțirii numărului căutat la un număr **MOD** (vom folosi în continuare această notație). | ||
| + | ==== Sfaturi / Reguli ==== | ||
| + | * când căutați o recurență pentru o problema de numărare trebuie să aveți grijă la două aspecte: | ||
| + | * 1) să **NU** numărați același obiect de două ori. | ||
| + | * 2) să numărați toate obiectele în cauză. | ||
| + | * 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 | ||
| + | * $ \frac{a}{b} \ \% \ MOD = ((a \ \% \ MOD) * (b ^ {MOD-2} \ \% \ MOD)) \ \% \ MOD)$ | ||
| + | * **DACĂ** MOD este prim; **DACĂ** a și b nu sunt multipli ai lui MOD | ||
| + | <spoiler 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 }$ | ||
| + | |||
| + | |||
| + | </spoiler> | ||
| + | |||
| + | <note> | ||
| + | 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: | ||
| + | * C++ | ||
| + | * **1LL / 1ULL** - constanta 1 pe 64 biti cu semn / făra semn | ||
| + | * **1LL * a * b** - am grijă ca rezultatul să nu dea overflow și să se stocheze direct pe 64 biți (cu semn) | ||
| + | * Java | ||
| + | * **1L** - constanta 1 pe 64 biți cu semn (în Java nu există unsigned types) | ||
| + | * **1L * a * b** - am grijă ca rezultatul să nu dea overflow și să se stocheze direct pe 64 biți (cu semn) | ||
| + | </note> | ||
| + | ==== Gardurile lui Gigel ==== | ||
| === Enunț === | === Enunț === | ||
| - | Se dă un grup de $k$ oameni care vor să cumpere împreună $n$ flori. Fiecare floare are un preț de bază, însă prețul cu care este cumpărată variază în funcție de numărul de flori cumpărate anterior de persoana respectivă. De exemplu dacă George a cumparat $3$ flori (diferite) și vrea să cumpere o floare cu prețul $2$, el va plăti $(3 + 1) * 2 = 8$. Practic el va plăti un preț proporțional cu numărul de flori cumpărate până atunci tot de el. | + | 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ă**. |
| - | Cerința: | + | 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. |
| - | Se cere pentru un număr $k$ de oameni și $n$ flori să se determine care este costul minim cu care grupul poate să achiziționeze toate cele $n$ flori o singură dată. | + | |
| - | Observație: Un tip de floare se cumpără o singură dată. O persoană poate cumpăra mai multe tipuri de flori. În final în grup va exista un singur exemplar din fiecare tip de floare. | + | O piesă poate fi pusă în poziție **orizontală** sau în poziție **verticală**. |
| - | Formal avem $k$ număr de oameni, $n$ număr de flori, $c[i]$ = pretul florii de tip $i$, costul de cumpărare $i$ va fi $(x + 1) * c[i]$, unde $x$ este numărul de flori cumpărate anterior de persoana respectivă. | + | === 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**. | ||
| - | <spoiler Exemplu> | + | <spoiler Exemplu 123> |
| - | n=3 k=3 | + | {{pa:laboratoare:garduri_123.png}} |
| - | c=[2 5 6] | + | |
| - | Cost minim = 13 | + | $n = 1$ sau $n = 2$ sau $n = 3$ |
| - | Explicație: Fiecare individ cumpără câte o floare, deci acestea se cumpăra la prețul nominal. | + | |
| + | 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**. | ||
| </spoiler> | </spoiler> | ||
| - | === Soluție === | ||
| - | Se observă că prețul efectiv de cumpărare va fi mai mare cu cât cumpărăm acea floare mai tarziu. Dacă considerăm cazul în care avem o singură persoană în grup observăm că are sens să cumpărăm obiectele în ordine descrescătoare (deoarece vrem să minimizăm costul fiecărui tip de floare, acesta crește cu cât cumpărăm floarea mai tarziu). | ||
| - | De aici, gândindu-ne la versiunea cu $k$ persoane, observăm că ar fi mai ieftin dacă am repartiza următoarea cea mai scumpa floare la alt individ. Deci împărțim florile sortate descrescător după pret în grupuri de câte $k$, fiecare individ luând o floare din acest grup și ne asigurăm că prețul va creste doar in funcție de numărul de grupuri anterioare. | + | <spoiler Exemplu 4> |
| + | {{pa:laboratoare:garduri_4.png}} | ||
| + | |||
| + | $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! | ||
| + | </spoiler> | ||
| + | |||
| + | |||
| + | <spoiler Exemplu 5> | ||
| + | {{pa:laboratoare:garduri_5.png}} | ||
| + | |||
| + | $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 | ||
| + | </spoiler> | ||
| + | |||
| + | |||
| + | <spoiler Exemplu 6> | ||
| + | {{pa:laboratoare:garduri_6.png}} | ||
| + | |||
| + | $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 | ||
| + | </spoiler> | ||
| + | |||
| + | === 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ță == | ||
| + | * **Caz de bază** | ||
| + | * $dp[1] = dp[2] = dp[3] = 1$; $dp[4]$ = 2 | ||
| + | * ** Caz general ** | ||
| + | * atunci când dorim să formăm un gard de lungime i ($i >= 5$) am văzut că putem alege cum să punem ultima/ultimele piese | ||
| + | * **DACĂ** alegem ca ultima piesă să fie pusă în poziție verticală, atunci la stânga mai rămâne de completat **un subgard de lungime $i-1$** | ||
| + | * numărul de moduri în care putem face acest subgard este $dp[i-1]$ | ||
| + | * **DACĂ** alegem ca ultima piesă să fie în poziție orizontală (de fapt, punem 4 piese în poziție orizontală), atunci la stânga mai rămâne de completat **un subgard de lungime $i-4$** | ||
| + | * numărul de moduri în care putem face acest subgard este $dp[i-4]$ | ||
| + | * $dp[i] = (dp[i-1] + dp[i-4]) \ \% \ MOD$ | ||
| + | * | ||
| + | <note> Așa cum am zis în secțiunea de [[http://ocw.cs.pub.ro/courses/pa/laboratoare/laborator-02?&#sfaturireguli|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 [[http://ocw.cs.pub.ro/courses/pa/laboratoare/laborator-02?&#sfaturireguli|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ă) </note> | ||
| + | == Implementare recurență == | ||
| + | Aici puteți vedea un exemplu simplu de implementare în C++. | ||
| - | === Implementare === | + | <spoiler Implementare în C++> |
| - | <spoiler> | + | |
| <code cpp> | <code cpp> | ||
| - | struct greater_comparator | + | #define MOD 1009 |
| - | { | + | int gardurile_lui_Gigel(int n) { |
| - | template<class T> | + | // cazurile de bază |
| - | bool operator()(T const &a, T const &b) const { return a > b; } | + | if (n <= 3) return 1; |
| - | }; | + | if (n == 4) return 2; |
| - | int minimum_cost(int k, vector<int>& costs) { | + | vector<int> dp(n + 1); // păstrez indexarea de la 1 ca în explicații |
| - | // sortam vectorul de preturi in ordine descrescatoare | + | |
| - | sort(costs.begin(), costs.end(), greater_comparator()); | + | // cazurile de bază |
| - | + | dp[1] = dp[2] = dp[3] = 1; | |
| - | // numarul de flori cumparate de fiecare individ din grup la un moment dat | + | dp[4] = 2; |
| - | int x = 0; | + | |
| - | // o varianta mai putin eficienta spatial ar fi fost sa retinem pt fiecare | + | // cazul general |
| - | // individ din grup numarul de flori cumparate intr-un hashmap | + | for (int i = 5; i <= n; ++i) { |
| - | // costul total | + | dp[i] = (dp[i - 1] + dp[i - 4]) % MOD; |
| - | int total_cost = 0; | + | |
| - | | + | |
| - | // parcurgem fiecare pret de floare si o "asignam" unui individ din grup | + | |
| - | // pretul acesteia fiind proportional cu numarul de achizitii facut pana acum | + | |
| - | // de acesta (x) | + | |
| - | for (int idx = 0; idx < costs.size(); idx++) { | + | |
| - | int customer_idx = idx % k; | + | |
| - | total_cost += (x + 1) * costs[idx]; | + | |
| - | // in momentul in care ultimul individ a cumparat o floare din grupul curent | + | |
| - | // incrementam numarul de flori achizitionate de fiecare | + | |
| - | if (customer_idx == k - 1) { | + | |
| - | x += 1; | + | |
| - | } | + | |
| } | } | ||
| - | return total_cost; | + | |
| + | return dp[n]; | ||
| } | } | ||
| + | |||
| </code> | </code> | ||
| + | |||
| + | 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ă. | ||
| </spoiler> | </spoiler> | ||
| - | === Complexitate === | ||
| - | Soluția va avea următoarele complexități: | ||
| - | * ** complexitate temporală **: $T(n) = O(n * log(n))$ | ||
| - | * explicație | ||
| - | * sortarea are $O(n * log(n))$ | ||
| - | * facem încă o parcurgere în $O(n)$ | ||
| - | * ** complexitate spațială **: depinde de algoritmul de sortare folosit. Fără partea de sortare, spațiul este constant (nu se ia în considerare vectorul de elemente). | ||
| + | == Complexitate == | ||
| + | * **complexitate temporală**: $T = O(n)$ | ||
| + | * explicație: avem o singură parcurgere în care construim tabloul dp | ||
| + | * se poate obține $T=O(log n)$ folosind exponențiere pe matrice! | ||
| + | * **complexitate spațială**: $S = O(n)$ | ||
| + | * explicație: stocăm tabloul dp | ||
| + | * se poate obține $S = O(1)$ folosind exponențiere pe matrice! | ||
| + | ===== 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ă. | ||
| - | ==== Problema cuielor ==== | + | În prima parte a [[pa:laboratoare:laborator-01|laboratorului 01]] 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. |
| - | === Enunț === | + | |
| - | Fie $N$ scânduri de lemn, descrise ca niște **intervale închise** cu capete reale. Găsiți o mulțime **minimă** de cuie astfel încât fiecare scândură să fie bătută de cel puțin un cui. Se cere poziția cuielor. | + | |
| - | + | ||
| - | Formulat matematic: găsiți o mulțime de puncte de cardinal **minim** $M$ astfel încât pentru orice interval [a<sub>i</sub>, b<sub>i</sub>] din cele $N$, să existe un punct $x$ din $M$ care să aparțină intervalului [a<sub>i</sub>, b<sub>i</sub>]. | + | |
| - | <spoiler Exemplu> | + | ==== Exponențiere pe matrice pentru recurențe liniare ==== |
| + | === Recurențe liniare === | ||
| + | O recurență liniară, în contextul laboratorului de DP, este de forma: | ||
| + | * $dp[i] = \sum_{k=1}^{KMAX} c_k * dp[i - k]$ | ||
| + | * pentru **KMAX o constantă** | ||
| + | * de obicei, KMAX este foarte mică comparativ cu dimensiunea n a problemei | ||
| + | * $c_k$ constante reale (unele pot fi nule) | ||
| - | * intrare: N = 5, intervalele: [0, 2], [1, 7], [2, 6], [5, 14], [8, 16] | + | 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}$. |
| - | * ieșire: M = {2, 14} | + | |
| - | * explicație: punctul 2 se află în primele 3 intervale, iar punctul 14 în ultimele 2 | + | <spoiler 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 ...). | ||
| + | * **complexitatea temporală** : $T = O(n * KMAX) = O(n)$ | ||
| + | * reamintim că acea valoare KMAX este o constantă foarte mică în comparație cu n (ex. KMAX < 100) | ||
| + | * **complexitatea spațială** : $S = O(n)$ | ||
| + | |||
| + | * am presupus că avem nevoie să reținem doar tabloul dp | ||
| </spoiler> | </spoiler> | ||
| - | === Soluție === | + | === Exponențiere pe matrice === |
| - | Se observă că dacă $x$ este un punct din $M$ care nu este capăt dreapta al niciunui interval, o translație a lui $x$ la dreapta care îl duce în capătul dreapta cel mai apropiat nu va schimba intervalele care conțin punctul. Prin urmare, există o mulțime de cardinal minim $M$ pentru care toate punctele $x$ sunt capete dreapta. | + | 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: | ||
| + | * $S_k$ = este starea inițială | ||
| + | * pentru a obține starea următoare, aplicăm algoritmul următor | ||
| + | * $S_i = S_{i-1}C$ | ||
| + | |||
| + | == 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} | ||
| + | |||
| + | |||
| + | |||
| + | <spoiler Explicație determinare C> | ||
| + | * ultima coloană conține toți coeficienții $c_k$ întrucât $dp[i] = \sum_{k=1}^{KMAX} c_k * dp[i - k]$ | ||
| + | * celelalte coloane conțin doar câte o valoare nenulă | ||
| + | * pe coloana j vom avea valoarea 1 pe linia $j + 1$ ($j = 1 : KMAX - 1$) | ||
| + | * cum obținem, de exemplu, $dp[i - 1]$? | ||
| + | * păi, avem $dp[i-1]$ chiar și în starea $S_{i-1}$, deci trebuie să îl copiem în starea $S_i$ | ||
| + | * copierea se realizează prin inmulțirea cu 1 | ||
| + | * dacă $dp[i-1]$ era pe ultima poziție (poziția k) în starea $S_{i-1}$, în noua stare $S_i$ este pe penultima poziție (poziția $k-1$) | ||
| + | * deci s-a deplasat la stânga cu o poziție! | ||
| + | * în noua stare, noua poziție este deplasată cu o unitate la stânga față de starea precedentă | ||
| + | * de aceea, pe coloana $j$, vrem să avem elementul 1 pe linia $j + 1$ ($j = 1 : KMAX - 1$) | ||
| + | * când înmulțim $S_{i-1}$ cu coloana $C_j$ **dorim să** | ||
| + | * ce copiem? | ||
| + | * valoarea $dp[i - KMAX + j]$ din $S_{i-1}$ în $S_{i}$ | ||
| + | * adică să copiem a j-a valoare de pe linie | ||
| + | * unde copiem? | ||
| + | * de pe poziția $j + 1$ pe poziția $j$ | ||
| + | </spoiler> | ||
| + | |||
| + | == 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}$$ | ||
| + | |||
| + | Pentru a aduce un plus de viteză, vom folosi un truc numit [[https://en.wikipedia.org/wiki/Exponentiation_by_squaring|exponențiere rapidă]]. Pe scurt, vom ridica o valoare(notată $a$) la puterea n, împărțind n folosind reprezentarea sa binară. | ||
| + | |||
| + | De exemplu, dacă am vrea să calculăm: | ||
| + | $$ 5^{13} = 5^{1101_2}= 5^8 * 5^4 * 5^1 $$ | ||
| + | |||
| + | Exponentul are exact $\lfloor log_2(n) \rfloor + 1$ cifre în baza 2, deci vom avea nevoie de $O(log(n))$ înmulțiri, fiind condiționați de faptul că trebuie să cunoaștem $a^1, a^2, a^4, etc.$ Din fericire, putem calcula rapid acești factori prin ridicarea la pătrat a elementului precedent. | ||
| - | Astfel, vom crea mulțimea $M$ folosind numai capete dreapta în felul următor: | + | În final, putem implementa un algoritm iterativ pentru calcularea rezultatului $a^n$ iterând prin cifrele din baza 2 ale lui n și înmulțind cu ${a^2}^{k}$ atunci când cifra curentă este 1(pentru viteză, vom folosi operații pe biți în loc de o iterare propriu zisă prin cifre). |
| - | * sortăm intervalele dupa capătul dreapta | + | |
| - | * iterăm prin fiecare interval și dacă intervalul curent nu conține ultimul punct introdus în mulțime atunci îl adăugam pe acesta la mulțime | + | |
| - | === Implementare === | + | Mai jos putem vedea o implementare in C++: |
| - | <spoiler> | + | |
| <code cpp> | <code cpp> | ||
| - | bool point_in_interval(const pair<int, int>&interval, int point) { | + | long long pow(long long a, long long n) { |
| - | return point >= interval.first && point <= interval.second; | + | long long res = 1; |
| + | while (n > 0) { | ||
| + | // daca cel mai nesemnificativ bit(LSB) din n este 1 | ||
| + | if (n & 1) { | ||
| + | res = res * a; | ||
| + | } | ||
| + | |||
| + | // calculam urmatorul factor | ||
| + | a = a * a; | ||
| + | |||
| + | // shiftam la dreapta n cu o pozitie(urmatorul bit va deveni LSB) | ||
| + | n >>= 1; | ||
| + | } | ||
| + | return res; | ||
| } | } | ||
| + | </code> | ||
| - | bool right_edge_comparator (pair<int, int>& e1, pair<int, int>& e2) { | + | Acest truc poate fi folosit pentru orice tip de date pentru care înmulțirea este asociativă(inclusiv matrice). |
| - | // comparam scandurile dupa capatul drepata | + | |
| - | return (e1.second < e2.second); | + | |
| - | } | + | |
| - | vector<int> cover_intervals_greedy(vector<pair<int, int>>& intervals) { | + | Obținem astfel o soluție cu următoarele complexități: |
| - | vector<int> nails; // pozitiile cuielor, a.k.a multimea M | + | * ** complexitate temporală **: $T = O(KMAX^3 * log(n))$ |
| - | // ultimul punct inserat | + | * explicație |
| - | int last_point = INT_MIN; | + | * facem doar $O(log n)$ pași, dar un pas implică înmulțire de matrice |
| + | * o înmulțire de matrice patrătică de dimensiune KMAX are $KMAX^3$ operații | ||
| + | * această metodă este eficientă când $KMAX << n$ (KMAX este mult mai mic decât n) | ||
| + | * ** complexitatea spațială **: $S = O(KMAX^2)$ | ||
| + | **Observație!** În ultimele calcule nu am șters constanta KMAX, întrucât apare la puterea a 2-a! $KMAX = 1000$ implică $KMAX^2 = 10^6$, valoare care nu mai poate fi ignorată în practică ($KMAX^2$ poate fi comparabil cu n). | ||
| - | //sortam invervalele dupa capatul drepata | + | === Gardurile lui Gigel (optimizare) === |
| - | sort(intervals.begin(), intervals.end(), right_edge_comparator); | + | După cum am văzut mai sus, în problema cu garduri dată de Gigel, soluția este o recurență liniară: |
| + | * $dp[1] = dp[2] = dp[3] = 1$; $d[4]=2$; | ||
| + | * $dp[i] = dp[i - 1] + dp[i - 4]$, pentru $i > 4$ | ||
| - | for (auto interval : intervals) { | + | == Exponențiere rapidă == |
| - | // daca intervalul nu contine ultimul punct adaugat | + | * $ k = 4 $ |
| - | if (!point_in_interval(interval, last_point)) { | + | * $S_4 = (dp[1], dp[2], dp[3], dp[4]) = (1, 1, 1, 2)$ |
| - | // il adaugam in multimea M | + | * $S_i = (dp[i-3], dp[i-2], dp[i-1], dp[i])$ |
| - | nails.push_back(interval.second); | + | * Răspunsul se află efectuând operația $S_n = S_4 * C^{n - 4}$, unde C are următorul conținut: |
| - | // updatam ultimul punct inserat | + | \begin{gather} |
| - | last_point = interval.second; | + | C = \begin{bmatrix} |
| + | 0 & 0 & 0 & 1\\ | ||
| + | 1 & 0 & 0 & 0\\ | ||
| + | 0 & 1 & 0 & 0\\ | ||
| + | 0 & 0 & 1 & 1\\ | ||
| + | \end{bmatrix} | ||
| + | \end{gather} | ||
| + | |||
| + | <spoiler Implementare în C++> | ||
| + | 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ă. | ||
| + | |||
| + | <code cpp> | ||
| + | |||
| + | #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 că suma încape pe 64 de biți | ||
| + | |||
| + | for (int k = 0; k < KMAX; ++k) { | ||
| + | sum += 1LL * A[i][k] * B[k][j]; | ||
| + | } | ||
| + | |||
| + | tmp[i][j] = sum % MOD; | ||
| } | } | ||
| } | } | ||
| - | return nails; | + | // 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; // rămâne de calculat C^(p/2) | ||
| + | } else { | ||
| + | // reduc la cazul anterior: | ||
| + | multiply_matrix(tmp, C, tmp); // tmp = tmp*C | ||
| + | --p; // rămâne de calculat C^(p-1) | ||
| + | } | ||
| + | } | ||
| + | |||
| + | // avem o parte din rezultat în C și o parte în tmp | ||
| + | multiply_matrix(C, tmp, R); // rezultat = tmp * C | ||
| + | } | ||
| + | |||
| + | int garduri_rapide(int n) { | ||
| + | // cazurile de bază | ||
| + | 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 să 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 află pe ultima poziție din S_n, | ||
| + | // deci voi folosi ultima coloană din C) | ||
| + | int sol = 1 * C[0][3] + 1 * C[1][3] + 1 * C[2][3] + 2 * C[3][3]; | ||
| + | return sol % MOD; | ||
| + | } | ||
| + | |||
| </code> | </code> | ||
| - | </spoiler> | ||
| - | === Complexitate === | + | <note> |
| - | Soluția va avea următoarele complexități: | + | Remarcați faptul că în funcția de înmulțire se folosește o matrice temporară $tmp$. Motivul este că vrem să apelăm funcția $multiply(C, C, C)$, unde C joacă atât rol de intrare cât și de ieșire. Dacă am pune rezultatele direct in C, atunci am strica inputul înainte să obținem rezultatul. |
| - | * ** complexitate temporală **: $T(n) = O(n * log(n))$ | + | |
| - | * explicație | + | |
| - | * sortare: $O(n * log n)$ | + | |
| - | * parcurgerea intervalelor: $O(n)$ | + | |
| - | * ** complexitate spațială **: depinde de algoritmul de sortare folosit. | + | |
| + | Putem spune că acea funcție este **matrix_multiply_safe**, în sensul că pentru orice A,B,C care respectă dimensiunile impuse, funcția va calcula corect produsul. | ||
| - | ===== Concluzii şi observații ===== | + | </note> |
| - | Aspectul cel mai important de reținut este că soluțiile găsite trebuie să reprezinte optimul global, ci nu doar local. Se pot confunda ușor problemele care se rezolvă cu greedy cu cele care se rezolvă prin programare dinamică (vom vedea săptămâna viitoare). | + | </spoiler> |
| + | <spoiler Comparație solutii (studiu de caz pentru curioși)> | ||
| + | Pe git găsiți o sursă completă în care se realizează: | ||
| + | * o verificare a faptului că cele 2 implementări (** gardurile_lui_Gigel** și **garduri_rapide**) produc aceleași rezultate | ||
| + | * un benchmark în care cele 2 implementări sunt comparate | ||
| + | * pe un sistem uzual (laptop) s-au obținut următoarele rezulate: | ||
| + | <code bash> | ||
| + | test case: varianta simplă | ||
| + | n = 100000000 sol = 119; time = 0.984545 s | ||
| + | test case: varianta rapidă | ||
| + | n = 100000000 sol = 119; time = 0.000021 s | ||
| - | ===== Exercitii ===== | + | |
| + | test case: varianta simplă | ||
| + | n = 1000000000 sol = 812; time = 9.662377 s | ||
| + | test case: varianta rapidă | ||
| + | n = 1000000000 sol = 812; time = 0.000022 s | ||
| + | </code> | ||
| + | * se observă clar diferența între cele 2 soluții (am confirmat ceea ce spunea și teoria: $O(n) $ vs $O(log(n))$); această tehnică îmbunătățește drastic o soluție gasită relativ usor. | ||
| + | |||
| + | </spoiler> | ||
| + | |||
| + | ===== Implementarea algoritmilor standard ===== | ||
| <note> | <note> | ||
| - | Scheletul de laborator se găsește pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/skel/lab02|pa-lab::skel/lab02]]. | + | [[https://github.com/acs-pa/pa-lab/tree/main/algorithms/lab02|pa-lab/algorithms/lab02]] conține implementarea algoritmilor standard / tehnicilor / tiparelor de algoritmi pentru acest laborator. |
| </note> | </note> | ||
| - | ==== Rucsac ==== | ||
| - | Fie un set cu $ n $ obiecte (care pot fi **tăiate** - varianta continuă a problemei). Fiecare obiect $i$ are asociată o pereche ($w_i, p_i$) cu semnificația: | ||
| - | * $w_i$ = $weight_i$ = greutatea obiectului cu numărul $i$ | ||
| - | * $p_i$ = $price_i$ = prețul obiectului cu numărul $i$ | ||
| - | * $w_i >= 0$ si $p_i > 0$ | ||
| - | Gigel are la dispoziție un rucsac de **volum infinit**, dar care suportă o **greutate maximă** (notată cu $W$ - weight knapsack). | ||
| - | El vrea să găsească **o submulțime de obiecte** (nu neapărat întregi) pe care să le bage în rucsac, astfel încât suma profiturilor să fie **maximă**. | + | ===== Pool probleme (pentru prezentări) ====== |
| - | Dacă Gigel bagă în rucsac obiectul $i$, caracterizat de ($w_i, p_i$), atunci profitul adus de obiect este $p_i$ (presupunem că îl vinde cu cât valoarează). | + | ======= 1) Rectangle Cutting ======= |
| - | În această variantă a problemei, Gigel poate tăia oricare dintre obiecte, obținând o proporție din acesta. Dacă Gigel alege doar $x$ din greutatea $w_i$ a obiectului $i$, atunci el câștigă doar $\frac{x}{w_i} * p_i$. | + | **Enunt:** |
| + | Se dă un dreptunghi de dimensiuni ''a × b''. Se pot face tăieturi doar pe linii paralele cu laturile dreptunghiului, împărțind dreptunghiul în două dreptunghiuri mai mici. | ||
| - | Task-uri: | + | Determinati numărul minim de tăieturi necesare pentru a împărți dreptunghiul în pătrate. |
| - | * Să se determine profitul maxim pentru Gigel. | + | |
| - | * Care este complexitatea soluției (timp + spatiu)? De ce? | + | |
| - | <spoiler Exemplu 1> | + | **Date de intrare:** |
| - | obiecte: | + | Două numere întregi ''a'' și ''b'' — dimensiunile dreptunghiului. |
| - | | index | 0 | 1 | 2 | | + | |
| - | | greutate | 60 | 100 | 120 | | + | |
| - | | valoare | 10 | 20 | 30 | | + | |
| - | greutate = 50 | + | |
| - | Output: 12.5 | + | **Date de ieșire:** |
| - | Explicație: avem 50 capacitate și toate obiectele au o greutatate mai mare, decidem să luăm cât putem din produsul cu raportul valoare / greutate cel mai mare. profit = 30 / 120 * 50 = 12.5 | + | Se afișează un singur număr întreg — numărul minim de tăieturi necesare. |
| - | </spoiler> | + | |
| + | Problema se poate testa la: | ||
| + | https://cses.fi/problemset/task/1744 | ||
| - | <spoiler Exemplu 2> | + | ======= 2) Removal Game ======= |
| - | obiecte: | + | |
| - | | index | 0 | 1 | 2 | | + | |
| - | | greutate| 20 | 50 | 30 | | + | |
| - | | valoare | 60 | 100 | 120 | | + | |
| - | greutate = 50 | + | |
| - | Output: 180 | + | **Enunt:** |
| - | Explicație: | + | Se dă un șir de ''n'' numere întregi. Doi jucători joacă alternativ. La fiecare mutare, un jucător poate elimina primul sau ultimul element din șir și adaugă valoarea acelui element la scorul său. |
| - | Sortăm obiectele după raportul valoare / profit si avem în ordine: {30, 120}, {20, 60}, {50, 100} | + | Ambii jucători joacă optim. |
| - | Introducem obiecte până când umplem sacul => intră primele 2 obiecte. Calculăm profitul 120 + 60 = 180 | + | |
| - | </spoiler> | + | |
| + | Determinati scorul maxim pe care îl poate obține primul jucător. | ||
| - | ==== Distante ==== | + | **Date de intrare:** |
| - | Consideram 2 localitati $A$ si $B$ aflate la distanta $D$. Intre cele 2 localitati avem un numar de $n$ benzinarii, date prin distanta fata de localitatea $A$. Masina cu care se efectueaza deplasarea intre cele 2 localitati poate parcurge maxim $m$ kilometri avand rezervorul plin la inceput. Se doreste parcurgerea drumului cu un numar minim de opriri la benzinarii pentru realimentare (dupa fiecare oprire la o benzinarie, masina pleaca cu rezervorul plin). | + | Pe prima linie se află un număr întreg ''n''. |
| + | Pe a doua linie se află ''n'' numere întregi reprezentând valorile din șir. | ||
| - | Distantele catre benzinarii se reprezinta printr-o lista de forma $0 < d1 < d2 < ... < dn$, unde $di$ ($1 <= i <= n$) reprezinta distanta de la $A$ la benzinaria $i$. Pentru simplitate, se considera ca localitatea $A$ se afla la $0$, iar $dn = D$ (localitatea $B$ se afla in acelasi loc cu ultima benzinarie). | + | **Date de ieșire:** |
| + | Se afișează un singur număr întreg — scorul maxim al primului jucător. | ||
| - | Se garanteaza ca exista o planificare valida a opririlor astfel incat sa se poata ajunge la localitatea $B$. | + | Problema se poate testa la: |
| + | https://cses.fi/problemset/task/1097/ | ||
| - | <hidden> | + | ======= 3) Array Description ======= |
| - | [Greedy] Se alimentează doar dacă nu se poate ajunge la benzinăria următoare. | + | |
| - | </hidden> | + | |
| - | <spoiler Exemplu> | + | **Enunt:** |
| - | $n = 5$ | + | Se dă un vector de lungime ''n''. Fiecare element este un număr între 1 și ''m''. Unele poziții sunt deja fixate, iar altele au valoarea 0, ceea ce înseamnă că pot fi alese liber. |
| + | Un vector este valid dacă diferența absolută dintre două elemente consecutive este cel mult 1. | ||
| - | $m = 10$ | + | Determinati câte vectori valizi pot fi formați respectând valorile deja fixate. |
| - | $d = (2, 8, 15, 25, 30)$ | + | **Date de intrare:** |
| + | Pe prima linie se află două numere întregi ''n'' și ''m''. | ||
| + | Pe a doua linie se află ''n'' numere întregi reprezentând vectorul inițial (valorile 0 indică poziții nefixate). | ||
| + | **Date de ieșire:** | ||
| + | Se afișează un singur număr întreg — numărul de vectori valizi modulo 10⁹+7. | ||
| - | Raspunsul este $3$, efectuand 3 opriri la a 2-a, a 3-a, respectiv a 4-a benzinarie. | + | Problema se poate testa la: |
| - | </spoiler> | + | https://cses.fi/problemset/task/1746/ |
| + | ======= 4) Student Attendance Record II ======= | ||
| + | **Enunt:** | ||
| + | Un elev are un istoric al prezentețor la școală pe parcursul a ''n'' zile. Pentru fiecare zi, status-ul său poate fi: | ||
| + | * P (present) — prezent | ||
| + | * L (late) — întârziat | ||
| + | * A (absent) — absent | ||
| - | ==== Teme la ACS ==== | + | Un istoric este considerat valid dacă: |
| + | * conține cel mult o absență (A) | ||
| + | * nu conține mai mult de două întârzieri consecutive (L) | ||
| - | Pe parcursul unui semestru, un student are de rezolvat $n$ teme (nimic nou pana aici...). Se cunosc enunțurile tuturor celor $n$ teme de la **începutul semestrului**. | + | Determinati câte istorice de prezență valide de lungime ''n'' există. |
| - | Timpul de rezolvare pentru oricare dintre teme este de **o săptămână** și **nu** se poate lucra la mai multe teme în același timp. Pentru fiecare tema se cunoaște un termen limita $d[i]$ (exprimat în săptămâni - deadline pentru tema $i$) și un punctaj $p[i]$. | + | **Date de intrare:** |
| + | Un număr întreg ''n'' — numărul de zile. | ||
| - | Nicio fracțiune din punctaj nu se mai poate obține după expirarea termenului limită. | + | **Date de ieșire:** |
| + | Se afișează un singur număr întreg — numărul de istorice valide modulo 10⁹+7. | ||
| - | Task-uri: | + | Problema se poate testa la: |
| - | * Să se definească o planificare de realizare a temelor, în așa fel încât punctajul obținut să fie **maxim**. | + | https://leetcode.com/problems/student-attendance-record-ii/description/ |
| - | * Care este complexitatea solutiei (timp + spatiu)? De ce? | + | |
| + | ======= 5) Burst Balloons ======= | ||
| - | <spoiler Exemplu 1> | + | **Enunt:** |
| - | | index | 0 | 1 | 2 | 3 | 4 | | + | Se dau ''n'' baloane, fiecare având un număr asociat. Când se sparge balonul ''i'', se câștigă un număr de monede egale cu produsul numerelor de pe balonul ''i'' și baloanele vecine lui în acel moment. |
| - | | deadline | 6 | 6 | 2 | 7 | 7 | | + | După spargere, balonul dispare, iar vecinii lui devin adiacenți. La extremități se consideră două baloane imaginare cu valoarea 1. |
| - | | punctaj | 5 | 4 | 1 | 5 | 8 | | + | |
| - | Output: $1 + 4 + 5 + 5 + 8 = 23$ | + | Determinati numărul maxim de monede pe care îl puteți obține spargând toate baloanele într-o anumită ordine. |
| - | Explicatie: Putem face toate temele deoarece pana ajungem la deadline-urile lor avem suficiente unitati de timp. | + | **Date de intrare:** |
| + | Un vector de ''n'' numere întregi care reprezintă valorile baloanelor. | ||
| + | |||
| + | **Date de ieșire:** | ||
| + | Se afișează un singur număr întreg — numărul maxim de monede ce poate fi obținut. | ||
| + | |||
| + | Problema se poate testa la: | ||
| + | https://leetcode.com/problems/burst-balloons/description/ | ||
| + | |||
| + | ===== Extra (studiu de caz pentru acasă) ===== | ||
| + | <spoiler Por Costel si Azerath> | ||
| + | Rezolvati pe infoarena problema [[https://infoarena.ro/problema/azerah|Por Costel si Azerah]]. | ||
| + | |||
| + | Hint: Câte subșiruri au suma **impară**? | ||
| </spoiler> | </spoiler> | ||
| - | <spoiler Exemplu 2> | + | <spoiler Parantezare booleană> |
| - | | index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | | + | Rezolvati pe geeksforgeeks problema [[https://www.geeksforgeeks.org/problems/boolean-parenthesization5610/1|Parantezare booleană]]. |
| - | | deadline | 3 | 3 | 3 | 3 | 9 | 11 | 11 | 11 | | + | |
| - | | punctaj | 4 | 9 | 6 | 5 | 10 | 4 | 2 | 6 | | + | |
| - | Output: $5 + 6 + 9 + 10 + 2 + 4 + 6 = 42$ | + | Hint: Complexitate temporală dorită este $O(n ^ 3)$. |
| + | |||
| + | Opțional, se pot defini funcții ajutătoare precum **is_operand**, **is_operator**, **evaluate**. | ||
| - | Explicatie: Pana in deadline 3 avem la dispozitie 3 unitati de timp si 4 teme. Deci sortam dupa punctaj si le includem pe cele mai valoroase: 5, 6 ,9. Pana la deadline 9 avem la dispoztie 6 unitati de timp si 4 teme. Le includem pe toate. | ||
| </spoiler> | </spoiler> | ||
| + | <spoiler Extratereștrii> | ||
| + | Rezolvați problema [[https://www.hackerrank.com/contests/test-practic-pa-2017-v1-plumbus/challenges/test-1-extraterestrii | ||
| + | | extratereștrii]] de la Test PA 2017. | ||
| + | </spoiler> | ||
| + | <spoiler Secvențe> | ||
| + | Rezolvați problema [[https://www.hackerrank.com/contests/test-practic-pa-2017-v1-plumbus/challenges/test-1-secvente | ||
| + | | Secvențe]] de la Test PA 2017. | ||
| + | </spoiler> | ||
| - | ==== BONUS ==== | ||
| - | Rezolvati problema [[http://codeforces.com/problemset/problem/779/C | Dishonest Sellers]]. | ||
| - | Hint: [[http://codeforces.com/blog/entry/50724 | aici ]]. | + | <spoiler PA Country> |
| + | Rezolvați problema [[https://www.hackerrank.com/contests/test-practic-pa-2017-v2-meeseeks/challenges/test-2-pa-country-medie | ||
| + | | PA Country]] de la Test PA 2017. | ||
| + | </spoiler> | ||
| - | ==== Extra ==== | ||
| - | <spoiler MaxSum> | ||
| - | Incercati problema [[https://www.hackerrank.com/contests/test-practic-pa-2017-v1-plumbus/challenges/1-1-usoare | MaxSum ]] de la test PA 2017. | + | |
| + | |||
| + | <spoiler iepuri> | ||
| + | Rezolvați pe infoarena problema [[http://infoarena.ro/problema/iepuri| iepuri]]. | ||
| + | |||
| + | Hint: Exponențiere logaritmică pe matrice | ||
| + | |||
| + | Soluție: | ||
| + | * $dp[0] = X; dp[1] = Y; dp[0] = Z; $ | ||
| + | * $dp[i] = (A * dp[i-1] + B * dp[i-2] + C * dp[i-3]) \ \% \ 666013$ | ||
| + | |||
| + | Pentru punctaj maxim, pentru fiecare test se folosește ecuația matriceală atașată. | ||
| + | Complexitate: $O(T * log(n))$. | ||
| </spoiler> | </spoiler> | ||
| - | <spoiler MyPoints> | + | |
| - | Problema 1 de la tema PA 2017. Puteti descarca enuntul si checkerul de [[https://ocw.cs.pub.ro/courses/_media/pa/teme/pa2017_tema1.zip|aici]]. | + | <spoiler Minimum Path Sum> |
| + | Rezolvați pe leetcode problema [[https://leetcode.com/problems/minimum-path-sum/description/#| Minimum Path Sum]]. | ||
| </spoiler> | </spoiler> | ||
| - | <spoiler Stropitorile lui Gigel> | + | <spoiler Lăcusta> |
| - | Problema 3 de la tema PA 2017. Puteti descarca enuntul si checkerul de [[https://ocw.cs.pub.ro/courses/_media/pa/teme/pa2017_tema1.zip|aici]]. | + | Rezolvați pe infoarena problema [[http://infoarena.ro/problema/Lacusta| Lăcusta]]. |
| </spoiler> | </spoiler> | ||
| + | |||
| + | |||
| + | <spoiler Suma4> | ||
| + | Rezolvați pe infoarena problema [[http://infoarena.ro/problema/Suma4|Suma4]]. | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Subșir> | ||
| + | Rezolvați pe infoarena problema [[https://www.infoarena.ro/problema/subsir|subșir]]. | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler 2șah> | ||
| + | Rezolvați pe infoarena problema [[https://infoarena.ro/problema/2sah | 2șah]]. | ||
| + | |||
| + | Hint: Exponențiere logaritmică pe matrice | ||
| + | |||
| + | O descriere detaliată se află în [[http://olimpiada.info/oji2015/index.php?cid=arhiva | arhiva OJI 2015]]. | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler DP problems> | ||
| + | Articolul de pe [[https://leetcode.com/discuss/general-discussion/458695/Dynamic-Programming-Patterns| leetcode]] conține o listă cu diverse tipuri de probleme de programare dinamică, din toate categoriile discutate la PA. | ||
| + | </spoiler> | ||
| + | |||
| ===== Referințe ===== | ===== Referințe ===== | ||
| - | [0] Capitolul **Greedy Algorithms** din **Introductions to Algorithms** de către T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein | + | [0] Chapter **Dynamic Programming**, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein |
| + | |||
| + | [1] [[http://infoarena.ro/problema/podm]] | ||
| - | [1] [[http://en.wikipedia.org/wiki/Greedy_algorithm]] | + | [2] [[http://infoarena.ro/problema/kfib]] |
| - | [2] [[http://ww3.algorithmdesign.net/handouts/Greedy.pdf]] | ||