This shows you the differences between two versions of the page.
|
pa:laboratoare:laborator-02 [2018/04/02 13:59] darius.neatu |
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: | + | |
| - | * [[rotarualexandruandrei94@gmail.com| Alex Rotaru]] | + | |
| - | * [[neatudarius@gmail.com|Darius Neațu]] | + | |
| - | * [[visanr95@gmail.com|Radu Vișan]] | + | |
| - | * [[cristb@gmail.com|Cristian Banu]] | + | |
| - | + | ||
| - | + | ||
| - | | + | |
| ===== 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ă. |
| - | ===== Precizari initiale ===== | + | ===== Precizări inițiale ===== |
| <note> | <note> | ||
| - | Toate exemplele de cod se gasesc in arhiva {{pa:new_pa:demo-lab02.zip}}. | + | Toate exemplele de cod se găsesc pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/algorithms/lab02|pa-lab/algorithms/lab02]]. |
| - | Acestea apar incorporate si in textul laboratorului pentru a facilita parcurgerea cursiva a laboratorului. | + | 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 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. | + | * 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. |
| - | * Va rugam sa incercati si codul din arhiva ** demo-lab02.zip**, inainte de a raporta ca ceva nu merge. :D | + | * Vă rugăm să compilați **DOAR** codul de pe GitHub. Pentru raportarea problemelor, contactați unul dintre maintaineri. |
| - | * Pentru orice problema legata de continutul acestei pagini, va rugam sa dati email 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)**. | ||
| - | Exista î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 soluții potențiale 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 exista 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 ca 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 posibila 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 moment 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. |
| - | === Enunt === | + | 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> | ||
| - | === Solutie === | + | <spoiler Exemplu 2> |
| - | Se observa ca tot ce avem de făcut este sa verificam fiecare număr dacă este pozitiv sau nu. În cazul pozitiv, îl introducem în subșirul soluție. | + | |
| - | Daca toate numerele sunt negative, solutia este data de cel mai mare numar negativ (cel mai mic in 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: |
| - | === Enunt === | + | * 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> | ||
| - | === Solutie === | + | <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 timp 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)); | ||
| - | // se ia ultimul spectacol ca terminat la -oo pt a putea incepe cu | + | // Cazul de bază 1: nu am ce înmulți |
| - | // cel mai devreme | + | for (int i = 1; i <= n; ++i) { |
| - | int last_end = INT_MIN; // -oo a.k.a -infinit | + | dp[i][i] = 0ULL; // 0 pe unsigned long long (voi folosi mai încolo și 1ULL) |
| - | for (auto interval : intervals) { | + | } |
| - | // daca inceputul intervalului curent este dupa sfarsitul ultimului | + | |
| - | // spectacol (last_end) il adaugam in lista de spectacole la care | + | // Cazul de bază 2: matrice d[i - 1] x d[i] înmulțită cu matrice d[i] x d[i + 1] |
| - | // se participa | + | // (matrice pe poziții consecutive) |
| - | if (interval.first >= last_end) | + | for (int i = 1; i < n; ++i) { |
| - | { | + | dp[i][i + 1] = 1ULL * d[i - 1] * d[i] * d[i + 1]; |
| - | plan.push_back(interval); | + | } |
| - | // dupa ce am adaugat un spectacol, updatam ultimul sfarsit de spectacol | + | |
| - | last_end = interval.second; | + | // 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); | ||
| + | } | ||
| } | } | ||
| } | } | ||
| - | 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 === | ||
| - | Solutia va avea urmatoarele complexitati: | ||
| - | * ** complexitate temporala **: $T(n) = O(n * log(n))$ | ||
| - | * explicatie | ||
| - | * sortarea are $O(n * log(n))$ | ||
| - | * facem inca o parcurgere in $O(n)$ | ||
| - | * ** complexitate spatiala **: depinde de algoritmul de sortare folosit. | ||
| - | ==== Problema florarului ==== | + | <note> |
| - | === Enunt === | + | **ATENȚIE!** La PA, în general, vom folosi convenția $ expresie \ \% \ MOD $, care va fi detaliată în capitolul următor din acest laborator. |
| - | Se da un grup de $k$ oameni care vor sa cumpere impreuna $n$ flori. Fiecare floare are un pret de baza, insa pretul cu care este cumparata variaza in functie de numarul de flori cumparate anterior de persoana respectiva. De exemplu daca George a cumparat $3$ flori (diferite) si vrea sa cumpere o floare cu pretul $2$, el va plati $(3 + 1) * 2 = 8$. Practic el va plati un pret proportional cu numarul flori cumparate pana atunci tot de el. | + | </note> |
| - | Cerinta: | + | == Complexitate == |
| - | Se cere pentru un numar $k$ de oameni si $n$ flori se cere sa se deterimne care este costul minim cu care grupul poate sa achizitioneze toate cele $n$ flori o singura data. | + | Î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)$ | ||
| - | Observatie: Un tip de floare se cumpara o singura data. O persoana poate cumpara mai multe tipuri de flori. In final in grup va exista un singur exemplar din fiecare tip de floare. | ||
| - | Formal avem $k$ numar de oameni, $n$ numar de flori, $c[i]$ = pretul florii de tip $i$, costul de cumparare $i$ va fi $(x + 1) * c[i]$, unde $x$ este numarul de flori cumparate anterior de persoana respectiva. | ||
| - | <spoiler Exemplu> | + | ===== Categoria 4: NUMĂRAT ===== |
| - | n=3 k=3 | + | Aceste recurențe au o oarecare asemănare: |
| - | c=[2 5 6] | + | * 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 }$ | ||
| - | Cost minim = 13 | ||
| - | Explicatie: Fiecare individ cumpara cate o floare si deci acestea se cumpara la pretul nominal. | ||
| </spoiler> | </spoiler> | ||
| - | === Solutie === | + | <note> |
| - | Se observa ca pretul efectiv de cumpare va fi mai mare cu cat cumparam acea floare mai tarziu. Daca consideram cazul in care avem o singura persoana in grup observam ca are sens sa cumparam obiectele in ordine descrescatoare(deoarece vrem sa minimizam costul fiecarui tip de flori si aceste creste cu cat cumparam floarea mai tarziu). | + | 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ț === | ||
| + | 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ă**. | ||
| - | De aici, gandindu-ne la versiunea cu $k$ persoane, observam ca ar fi mai ieftin daca am repartiza urmatoarea cea mai scumpa floare la alt individ. Deci impartim florile sortate descrescator dupa pret in grupuri de cate $k$, fiecare individ luand o floare din acest grup si ne asiguram ca pretul va creste doar in functie de numarul de grupuri anterioare. | + | 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ă**. | ||
| - | === Implementare === | + | === Cerință === |
| - | <spoiler> | + | 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 123> | ||
| + | {{pa:laboratoare:garduri_123.png}} | ||
| + | |||
| + | $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**. | ||
| + | </spoiler> | ||
| + | |||
| + | |||
| + | <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++. | ||
| + | |||
| + | <spoiler Implementare în C++> | ||
| <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 === | ||
| - | Solutia va avea urmatoarele complexitati: | ||
| - | * ** complexitate temporala **: $T(n) = O(n * log(n))$ | ||
| - | * explicatie | ||
| - | * sortarea are $O(n * log(n))$ | ||
| - | * facem inca o parcurgere in $O(n)$ | ||
| - | * ** complexitate spatiala **: depinde de algoritmul de sortare folosit. Fara partea de sortare, spatiul este constant (nu se ia in 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. |
| - | === Enunt === | + | |
| - | 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 afla î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> | ||
| - | === Solutie === | + | === Exponențiere pe matrice === |
| - | Se observa că dacă $x$ este un punct din $M$ care nu este capăt dreapta al nici unui 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, exista 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ă | ||
| - | Astfel, vom crea mulțimea $M$ folosind numai capete dreapta în felul următor: | ||
| - | * sortăm intervalele dupa capatul 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 === | + | |
| - | <spoiler> | + | == 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. | ||
| + | |||
| + | Î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). | ||
| + | |||
| + | Mai jos putem vedea o implementare in C++: | ||
| <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 punt 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)); | ||
| } | } | ||
| - | </code> | ||
| - | </spoiler> | ||
| - | === Complexitate === | + | // R = C^p |
| - | Soluția va avea următoarele complexități: | + | void power_matrix(int C[KMAX][KMAX], int p, int R[KMAX][KMAX]) { |
| - | * ** complexitate temporala **: $T(n) = O(n * log(n))$ | + | // tmp = I (matricea identitate) |
| - | * explicație | + | int tmp[KMAX][KMAX]; |
| - | * sortare: $O(n * log n)$ | + | for (int i = 0; i < KMAX; ++i) { |
| - | * parcurgerea intervalelor: $O(n)$ | + | for (int j = 0; j < KMAX; ++j) { |
| - | * ** complexitate spațială **: depinde de algoritmul de sortare folosit. | + | 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) | ||
| + | } | ||
| + | } | ||
| - | ===== Concluzii şi observații ===== | + | // avem o parte din rezultat în C și o parte în tmp |
| - | Aspectul cel mai important de reținut este că soluțiile găsite trebuie să reprezinte optimul global și nu doar local. Se pot confunda ușor problemele care se rezolvă cu Greedy cu cele care se rezolvă prin Programare Dinamică (vom vedea saptamana viitoare). | + | 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> | ||
| - | ===== Exercitii ===== | ||
| <note> | <note> | ||
| - | In acest laborator vom folosi scheletul de laborator din arhiva {{pa:new_pa:skel-lab02.zip}}. | + | 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. |
| + | |||
| + | 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. | ||
| </note> | </note> | ||
| + | </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 | ||
| + | |||
| + | 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> | ||
| - | ATENTIE! Au aparut modificari minore la checker. Rulati comanda `./check.sh` si recititi documentatia afisata. | + | [[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 **taiate** - varianta continua a problemei). Fiecare obiect $i$ are asociata o pereche ($w_i, p_i$) cu semnificatia: | ||
| - | * $w_i$ = $weight_i$ = greutatea obiectului cu numarul $i$ | ||
| - | * $p_i$ = $price_i$ = pretul obiectului cu numarul $i$ | ||
| - | * $w_i >= 0$ si $p_i > 0$ | ||
| - | Gigel are la dispozitie un rucsac de **volum infinit**, dar care suporta o **greutate maxima** (notata cu $W$ - weight knapsack). | + | ===== Pool probleme (pentru prezentări) ====== |
| - | El vrea sa gaseasca **o submultime de obiecte** (nu neaparat intregi) pe care sa le bage in rucsac, astfel incat suma profiturilor sa fie **maxima**. | + | ======= 1) Rectangle Cutting ======= |
| - | Daca Gigel baga in rucsac obiectul $i$, caracterizat de ($w_i, p_i$), atunci profitul adus de obiect este $p_i$ (presupunem ca il vinde cu cat valoareaza). | + | **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. | ||
| - | In aceasta varianta a problemei, Gigel poate taia oricare dintre obiecte, obtinand o proportie din acesta. Daca Gigel alege alege doar $x$ din greutatea $w_i$ a obiectului $i$, atunci el castiga doar $\frac{x}{w_i} * p_i$. | + | Determinati numărul minim de tăieturi necesare pentru a împărți dreptunghiul în pătrate. |
| - | Task-uri: | + | **Date de intrare:** |
| - | * Sa se determine profitul maxim pentru Gigel. | + | Două numere întregi ''a'' și ''b'' — dimensiunile dreptunghiului. |
| - | * Care este complexitatea solutiei (timp + spatiu)? De ce? | + | |
| - | <spoiler Exemplu 1> | + | **Date de ieșire:** |
| - | obiecte: | + | Se afișează un singur număr întreg — numărul minim de tăieturi necesare. |
| - | | index | 0 | 1 | 2 | | + | |
| - | | greutate | 60 | 100 | 120 | | + | |
| - | | valoare | 10 | 20 | 30 | | + | |
| - | greutate = 50 | + | |
| - | Output: 12.5 | + | Problema se poate testa la: |
| - | Explicatie: avem 50 capacitate si toate obiectele au o greutatate mai mare, decidem sa luam cat putem din produsul cu raportul valoare / greutate cel mai mare. profitu = 30 / 120 * 50 = 12.5 | + | https://cses.fi/problemset/task/1744 |
| - | </spoiler> | + | |
| + | ======= 2) Removal Game ======= | ||
| - | <spoiler Exemplu 2> | + | **Enunt:** |
| - | obiecte: | + | 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. |
| - | | index | 0 | 1 | 2 | | + | Ambii jucători joacă optim. |
| - | | greutate| 20 | 50 | 30 | | + | |
| - | | valoare | 60 | 100 | 120 | | + | |
| - | greutate = 50 | + | |
| - | Output: 180 | + | Determinati scorul maxim pe care îl poate obține primul jucător. |
| - | Explicatie: | + | |
| - | Sortam obiectele dupa raportul valoare profit si avem in ordine: {30, 120}, {20, 60}, {50, 100} | + | |
| - | Introducem obiecte pana cand umplem sacul => intra primele 2 obiecte. Calculam profitul 120 + 60 = 180 | + | |
| - | </spoiler> | + | |
| + | **Date de intrare:** | ||
| + | Pe prima linie se află un număr întreg ''n''. | ||
| + | Pe a doua linie se află ''n'' numere întregi reprezentând valorile din șir. | ||
| - | ==== Distante ==== | + | **Date de ieșire:** |
| - | 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). | + | Se afișează un singur număr întreg — scorul maxim al primului jucător. |
| - | 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). | + | Problema se poate testa la: |
| + | https://cses.fi/problemset/task/1097/ | ||
| - | Se garanteaza ca exista o planificare valida a opririlor astfel incat sa se poata ajunge la localitatea $B$. | + | ======= 3) Array Description ======= |
| - | [Greedy] Se alimentează doar dacă nu se poate ajunge la benzinăria următoare. | + | **Enunt:** |
| + | 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. | ||
| - | <spoiler Exemplu> | + | Determinati câte vectori valizi pot fi formați respectând valorile deja fixate. |
| - | $n = 5$ | + | |
| - | $m = 10$ | + | **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). | ||
| - | $d = (2, 8, 15, 25, 30)$ | + | **Date de ieșire:** |
| + | Se afișează un singur număr întreg — numărul de vectori valizi modulo 10⁹+7. | ||
| + | Problema se poate testa la: | ||
| + | https://cses.fi/problemset/task/1746/ | ||
| - | Raspunsul este $3$, efectuand 3 opriri la a 2-a, a 3-a, respectiv a 4-a benzinarie. | + | ======= 4) Student Attendance Record II ======= |
| - | </spoiler> | + | |
| + | **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 | ||
| + | Un istoric este considerat valid dacă: | ||
| + | * conține cel mult o absență (A) | ||
| + | * nu conține mai mult de două întârzieri consecutive (L) | ||
| - | ==== Teme la ACS ==== | + | Determinati câte istorice de prezență valide de lungime ''n'' există. |
| - | 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**. | + | **Date de intrare:** |
| + | Un număr întreg ''n'' — numărul de zile. | ||
| - | 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 ieșire:** |
| + | Se afișează un singur număr întreg — numărul de istorice valide modulo 10⁹+7. | ||
| - | Nicio fracțiune din punctaj nu se mai poate obține după expirarea termenului limită. | + | Problema se poate testa la: |
| + | https://leetcode.com/problems/student-attendance-record-ii/description/ | ||
| - | Task-uri: | + | ======= 5) Burst Balloons ======= |
| - | * Să se definească o planificare de realizare a temelor, în așa fel încât punctajul obținut să fie **maxim**. | + | |
| - | * Care este complexitatea solutiei (timp + spatiu)? De ce? | + | |
| + | **Enunt:** | ||
| + | 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. | ||
| + | După spargere, balonul dispare, iar vecinii lui devin adiacenți. La extremități se consideră două baloane imaginare cu valoarea 1. | ||
| - | <spoiler Exemplu 1> | + | Determinati numărul maxim de monede pe care îl puteți obține spargând toate baloanele într-o anumită ordine. |
| - | | index | 0 | 1 | 2 | 3 | 4 | | + | |
| - | | deadline | 6 | 6 | 2 | 7 | 7 | | + | |
| - | | punctaj | 5 | 4 | 1 | 5 | 8 | | + | |
| - | Output: $1 + 4 + 5 + 5 + 8 = 23$ | + | **Date de intrare:** |
| + | Un vector de ''n'' numere întregi care reprezintă valorile baloanelor. | ||
| - | Explicatie: Putem face toate temele deoarece pana ajungem la deadline-urile lor avem suficiente unitati de timp. | + | **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]] | ||