Differences

This shows you the differences between two versions of the page.

Link to this comparison view

pa:laboratoare:laborator-01 [2021/03/08 23:30]
radu.nichita [Exerciții]
pa:laboratoare:laborator-01 [2026/02/25 18:40] (current)
darius.neatu
Line 1: Line 1:
-====== Laborator 01: Divide et Impera ​====== +====== Laborator 01: Programare Dinamică (1/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: 
-  * [[visanr95@gmail.com|Radu Vișan (2018)]] 
-  * [[cristb@gmail.com|Cristian Banu (2018)]] 
-  * [[neatudarius@gmail.com|Darius-Florentin Neațu (2018)]] 
-  ​ 
 ===== Obiective laborator ===== ===== Obiective laborator =====
- +  ​înțelegerea ​noțiunilor de bază despre programare dinamică 
-  ​Înțelegerea ​conceptului teoretic din spatele descompunerii unei probleme +  * însușirea abilităților ​de implementare a algoritmilor bazați programare dinamică.
-  * Rezolvarea ​de probleme abordabile folosind conceptul ​ de Divide et Impera +
  
 ===== 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/​lab01|pa-lab::​demo/​lab01]].+Toate exemplele de cod se găsesc pe pagina [[https://​github.com/​acs-pa/​pa-lab/​tree/​main/​demo/​lab03|pa-lab::​demo/​lab03]].
  
 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ă rugam să compilați **DOAR** codul de pe GitHub. Pentru raportarea problemelor,​ contactați unul dintre maintaineri. ​   * Vă rugam 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? ===== 
 +Similar cu greedy, tehnica de programare dinamică este folosită în general pentru rezolvarea **problemelor de optimizare**.  
 +În continuare vom folosi acronimul **DP (dynamic programming)**.
  
-Paradigma Divide et Impera stă la baza construirii de algoritmi eficienți pentru ​diverse ​probleme+De asemenea, DP se poate folosi și pentru probleme ​în care nu căutăm un optim, cum ar fi **problemele de numărare** ​(vom exemplifica în lab04). 
-  ​Sortări (ex: MergeSort [[http://​www.sorting-algorithms.com/​merge-sort|[1]]],​ QuickSort [[http://​www.sorting-algorithms.com/​quick-sort|[2]]]+===== Aplicatii DP ===== 
-  Înmulțirea numerelor mari (ex: Karatsuba [[http://​en.wikipedia.org/​wiki/​Karatsuba_algorithm|[3]]]) +Programarea dinamică are un **câmp larg de aplicare**, însă la PA ne vom rezuma la câteva aplicații care vor fi menționate pe parcursul laboratoarelor ​și 4. De asemenea, această tehnică va fi folosită și în laboratoarele de grafuri ​(ex. algoritmul Floyd-Warshall - pe care îl veți implementa și la PA; algoritmi pe arbori etc).
-  * Analiza sintactică (ex: parsere top-down [[http://en.wikipedia.org/​wiki/​Top-down_parser|[4]]]) +
-  * Calcularea transformatei Fourier discretă (ex: FFT [[http://en.wikipedia.org/​wiki/​Fast_Fourier_transform|[5]]])+
  
-Un alt domeniu ​de utilizare a tehnicii ​divide et impera ​este programarea paralelă pe mai multe procesoaresub-problemele fiind executate pe mașini diferite.+Programarea dinamică presupune rezolvarea unei probleme prin **descompunerea ei în subprobleme** şi rezolvarea acestora. Spre deosebire ​de divide et impera, ​subproblemele nu sunt disjuncte, ci **se suprapun**.
  
-===== Prezentarea generală ​problemei =====+<spoiler Memoizare and more DP> 
 +Pentru ​evita recalcularea porțiunilor care se suprapun, rezolvarea se face pornind de la cele mai mici subprobleme şi folosindu-ne de rezultatul acestora calculăm subproblema imediat mai mare. Cele mai mici subprobleme sunt numite subprobleme unitare, acestea putând fi rezolvate într-o complexitate constantă, ex: cea mai mare subsecvență dintr-o mulțime de un singur element.
  
-O descriere ​tehnicii D&I: “Divide and Conquer algorithms break the problem into several sub-problems that are similar to the original problem but smaller in sizesolve the sub-problems recursively,​ and then combine these solutions to create a solution to the original ​problem.+Pentru ​nu recalcula soluțiile subproblemelor ce ar trebui rezolvate de mai multe oripe ramuri diferite, se reține soluția subproblemelor folosind o tabelă (matrice uni, bi sau multi-dimensională în funcție de problemă) cu rezultatul fiecărei subprobleme. Această tehnică se numește **memoizare**.
  
-Deci un algoritm D&I **împarte problema** în mai multe subprobleme similare cu problema inițială şi de dimensiuni mai mici, **rezolvă subproblemele** recursiv şi apoi **combină soluțiile** obţinute ​pentru ​a obține ​soluția ​problemei inițiale.+Această tehnică determină ”valoarea” ​soluției pentru ​fiecare din subprobleme. Mergând de la subprobleme mici la subprobleme din ce în ce mai mari ajungem la soluția ​optimă, la nivelul întregii probleme. ”Valoarea” își schimbă înțelesul logic de la o problemă la alta. În problemele de minimizarea costului, ”valoarea” este reprezentată de costul minim. În probleme care presupun identificarea unei componente maxime, ”valoarea” este caracterizată de dimensiunea componentei.
  
-Sunt trei pași pentru aplicarea algoritmului D&I:+După calcularea valorii pentru toate subproblemele se poate determina efectiv mulțimea de elemente care compun soluția. „Reconstrucția” soluţiei se face mergând din subproblemă în subproblemă,​ începând de la problema cu valoarea optimă ​și ajungând în subprobleme unitare. Metoda și recurența variază de la problemă la problemă, dar în urma unor exerciții practice va deveni din ce în ce mai facil să le identificați.
  
-  * **Divide**: împarte problema în una sau mai multe //probleme similare de dimensiuni mai mici//. 
-  * **Impera** (stăpânește):​ rezolvă subproblemele recursiv; dacă dimensiunea sub-problemelor este mică, se rezolvă iterativ. 
-  * **Combină**:​ combină soluțiile subproblemelor pentru a obține soluția problemei inițiale. 
  
-Complexitatea algoritmilor D&I se calculează după formula:+</​spoiler>​
  
-$T(n= D(n) + S(n) + C(n)$,+Cei curioși pot citi [[https://​en.wikipedia.org/​wiki/​Dynamic_programming#​History|aici]] adevărul despre numele acestei tehnici. 8-)
  
-unde $D(n)$, $S(n)$ şi $C(n)$ reprezintă complexitățile celor 3 pași descriși mai sus: dividestăpânește, respectiv combină.+===== Ce determina DP? ===== 
 +Aplicând această tehnică determinăm **una din soluțiile optime**problema putând avea mai multe soluții optime. În cazul în care se dorește determinarea tuturor soluțiilor optimealgoritmul trebuie combinat cu unul de backtracking în vederea construcției soluțiilor.
  
-===== Probleme clasice ​=====+În cazul problemelor de numărare, această tehnică ne va furniza **numărul căutat**. 
 +===== Tipar general DP ===== 
 +Aplicarea acestei tehnici de programare poate fi descompusă în următoarea secvență de pași: 
 +  - Identificarea structurii și a metricilor utilizate în caracterizarea soluției optime; 
 +  - Determinarea unei metode de calcul recursiv pentru a afla valoarea fiecărei subprobleme;​ 
 +  - Calcularea "​bottom-up"​ a acestei valori (de la subproblemele cele mai mici la cele mai mari); 
 +  - Reconstrucția soluției optime pornind de la rezultatele obținute anterior.
  
-==== MergeSort ​====+===== Exemple clasice ===== 
 + 
 +Programarea Dinamică este cea mai flexibilă tehnică din programare. Cel mai ușor mod de a o înțelege presupune parcurgerea cât mai multor exemple. 
 + 
 +Propunem câteva categorii de recurențe, pe care le vom grupa astfel: 
 +  *  recurențe de tip **SSM** (Subsecvența de Sumă Maximă) 
 +  *  recurențe de tip **RUCSAC** 
 +  *  recurențe de tip **PODM** (Parantezare Optimă de Matrici) 
 +  *  recurențe de tip **numărat** 
 +  *  recurențe pe **grafuri** 
 + 
 +<​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 1: SSM ===== 
 +Aceste recurențe au o oarecare asemănare cu problema SSM (enunț + soluție). 
 + 
 + 
 + 
 +<​note>​ 
 +ATENȚIE! Rețineți diferența între următoarele 2 noțiuni! 
 + 
 + 
 +* **subsecvență** ([[https://​en.wikipedia.org/​wiki/​Substring | substring]] în engleză) pentru un vector ** v ** înseamnă un alt vector $u = [v[i], v[i+1],..., v[j]]$ unde $i <= j$. 
 + 
 +* **subșir** ([[https://​en.wikipedia.org/​wiki/​Subsequence | subsequence]] în engleză) pentru un vector ** v ** înseamnă un alt vector $u = [v[i_1], v[i_2],..., v[i_k]]]$ unde $i_1 < i_2 < ... < i_k$. 
 +</​note>​  
 + 
 +==== SSM ====
 === Enunț === === Enunț ===
-Sortarea prin interclasare (MergeSort [[http://​www.sorting-algorithms.com/​merge-sort|[1]]]) este un algoritm ​de sortare de vectori ce folosește paradigma D&I:+Fie un vector $ v $ cu $ n $ elemente întregi. O subsecvență ​de numere din șir este de forma$v_i, v_{i+1}, ... , v_j$ ($i <= j$), având suma asociată $s_{ij} = v_i +  v_{i+1} + ... + v_j$. O subsecvență ** nu ** poate fi vidă.
  
-  * **Divide**: împarte vectorul inițial în doi sub-vectori de dimensiune n/2. 
-  * **Stăpânește**:​ sortează cei doi sub-vectori recursiv folosind sortarea prin interclasare;​ recursivitatea se oprește când dimensiunea unui sub-vector este 1 (deja sortat). 
-  * **Combina**:​ Interclasează cei doi sub-vectori sortați pentru a obține vectorul inițial sortat. 
  
-=== Pseudocod === 
-Mai jos găsiți algoritmul MergeSort scris în pseudocod. 
  
-<spoiler Pseudocod>​ +=== Cerință ==
-<code cpp> +Să se determine subsecvența de sumă maximă (notată **SSM**).
-MergeSort(v,​ start, end) // v – vector, start – limită inferioră, end – limită superioară +
- if (start ​== end) return; // condiția de oprire +
- mid (start + end) / 2; // etapa divide +
- MergeSort(v,​ start, mid); // etapa stăpânește +
- MergeSort(v, mid + 1, end); +
- Merge(v, start, end); // etapa combină +
-  +
-Merge(v, start, end) // interclasare sub-vectori +
- // indecsi +
-        mid = (start + end) / 2; +
- i = start; +
- j = mid + 1; +
- k = 1;+
  
-        tmp buffer temporar in care incap (end - start + 1) numere +=== Exemple ​=== 
- while (i <mid && j <end)  +<spoiler Exemplu 1> 
- if (v[i] <v[j]) tmp[k++] ​v[i++]; +6
-              else tmp[k++] ​v[j++]; +
-  +
- while (i <= mid)  +
- tmp[k++] ​v[i++];+
  
- while (j <= end)  +|i   | 1 | 2 | 3 | 4 | 5| 6 | 
- tmp[k++] = v[j++];+|v[i]| -10 | 2 | 3 | -1| 2 | -3 |
  
- copy(v[start..end]tmp[1..k-1]); +Răspuns: SSM este între 2 și 5 (poziții)Are suma +6($SSM = 2, 3, -1, 2$
-</code>+ 
 +Explicație:​ avem numere pozitive, deci există o soluție simplă în care putem să alegem doar un număr pozitiv/mai multe numere pozitive de pe poziții alăturate (adică încercăm să evităm numere negative). Cele mai lungi subsecvențe cu numere pozitive sunt 2,3 și 2. Observăm că dacă extindem $2, 3$ la $2, 3, -1, 2$, deși am inclus un număr negativ, suma secvenței crește. ​
 </​spoiler>​ </​spoiler>​
  
-<​spoiler ​Implementare in C++> +<​spoiler ​Exemplu 2> 
-Mai jos puteti ​si o implementare în C++.+n = 4 
 + 
 +|i   | 1 | 2 | 3 | 4 | 
 +|v[i]| 10 | 20 | 30 | 40| 
 + 
 +Răspuns: SSM este între 1 și 4 (poziții). Are suma 100. ($SSM = 10, 20, 30, 40$) 
 + 
 +Explicație:​ deoarece toate numerele sunt **pozitive**,​ SSM cuprinde toate numerele. 
 +</​spoiler>​ 
 + 
 +<spoiler Exemplu 3> 
 +n = 4 
 + 
 +|i   | 1 | 2 | 3 | 4 | 
 +|v[i]| -10 | -20 | -30 | -40| 
 + 
 +Răspuns: SSM este între 1 si 1 (poziții). Are suma -10. ($SSM = -10$) 
 + 
 +Explicație:​ deoarece toate numerele sunt **negative**,​ SSM cuprinde doar cel mai mare număr. 
 +</​spoiler>​ 
 + 
 +=== Rezolvare === 
 +== Tipar == 
 +Tiparul acestei probleme ne sugerează că o soluție este obținută incremental,​ în sensul că **putem** privi problema astfel: găsim cea mai bună soluție folosind **primele $i-1$** elemente din șir, apoi încercăm să o **extindem** folosind elementul **i** (adică ne extindem la dreapta ~CU~ $v[i]$). 
 + 
 +== Numire recurență == 
 +Întrucât la fiecare pas trebuie sa reținem ** cea mai bună soluție** folosind un **prefix** din vectorul v, soluția va fi salvată într-un tablou auxiliar definit astfel: 
 + 
 +$ dp[i] $ = suma subsecvenței de sumă maximă (**suma SSM**) folosind ** doar primele ​ i ** elemente din vectorul v și care **se termină pe poziția i** 
 + 
 +== Mențiuni == 
 +  * Pentru a menține o convenție, toate tablourile de acest tip din laborator vor fi notate cu ** dp ** (dynamic programming). 
 +  * Ca să rezolvăm problema dată, trebuie să rezolvăm o mulțime de subprobleme 
 +    * $dp[i]$ reprezintă **soluția** pentru problema $v[1], ..., v[i]$ și care se termină cu $v[i]$ 
 +  * Soluția pentru problema inițială este maximul din vectorul $dp[i]$ - a.k.a. **max(dp[i])**. 
 + 
 +== Găsire recurență == 
 +Întrucât dorim ca această problemă să fie rezolvabilă printr-un algoritm/​bucată de cod, trebuie să descriem o metodă concretă prin care vom calcula $dp[i]$. 
 + 
 +  * **Cazul de bază** 
 +    * În general în probleme putem avea mai multe cazuri de bază, care în principiu se leagă de valori extreme are dimensiunilor subproblemelor. 
 +    * În cazul SSM, avem un singur caz de bază, când avem un singur element în prefix: $dp[1] = v[1] $. 
 +    * Explicație:​ dacă avem un singur element, atunci acesta formează singura subsecvență posibilă, deci $ SSM = v[1] $ 
 + 
 +  * **Cazul general** 
 +    * presupune inductiv că avem rezolvate toate subproblemele mai mici 
 +    * în cazul SSM, presupunem că avem calculat $ dp[i-1] $ și dorim sa calculăm $ dp[i] $ (cunoaștem cea mai bună soluție folosind primele i-1 elememente și vedem dacă elementul de pe poziția i o poate îmbunătăți) 
 +    * la fiecare pas avem de ales dacă $v[i]$ extinde cea mai bună soluție care se termină pe $v[i-1]$ sau se începe o nouă secvență cu $v[i]$ 
 +    * decidem în funcție de $ dp[i - 1]$ și $v[i] $ 
 +      * ** dacă ** $ dp[i - 1] >= 0 $ (cea mai bună soluție care se termină pe i - 1 are cost nenegativ) 
 +        * extindem secvență care se termină cu v[i-1] folosind elementul v[i]: $dp[i] = dp[i-1] ​v[i]$ 
 +        * Explicație:​ $dp[i-1] ​v[i] >= v[i]$ (încă are rost să extind) 
 +      * ** dacă ** $ dp[i - 1] < 0 $ (cea mai bună soluție care se termină pe i - 1 are cost negativ) 
 +        * vom începe o nouă secvență cu $v[i]$, adică $dp[i] = v[i]$ 
 +        * Explicație:​ $v[i] > dp[i-1] + v[i]$, deci prin extindere nu obțin soluție maximă! 
 + 
 +== Implementare recurență == 
 +În majoritatea problemelor de DP, sirea recurenței ocupă cea mai mare parte a timpului de rezolvare (lucru adevărat și în cazul problemelor de la PA). De aceea, faptul că ați reușit să scrieți pe foaie lucruri foarte complicate poate fi un indiciu ca ați pornit pe cale greșită. 
 + 
 +<spoiler Exemplu ​implementare
 + 
 +Problema se poate testa pe infoarena: [[https://​www.infoarena.ro/​problema/​ssm | Subsecvență de sumă maximă]]. 
 + 
 +Mai jos se află un exemplu simplu de implementare a recurenței găsite ​în C++.
  
 <code cpp> <code cpp>
-#include <​bits/​stdc++.h>​ 
-using namespace std; 
  
-vector<​int>​ v;+// găsește SSM pentru vectorul v cu n elemente 
 +// pentru a menține convenția din explicații:​ 
 +//      - elementele sunt indexate de la 0, dar le folosesc doar pe cele care incep de la 1 
 +//                                          => v[1], ..., v[n] 
 +int SSM(int n, vector<​int> ​&v) { 
 + vector<​int>​ dp(n + 1)   // vector cu n + 1 elemente (indexarea începe de la 0) 
 +                                  // am nevoie de dp[1], ..., dp[n]
  
-void merge_halves(int left, int right) { + // caz de bază 
- int mid (left + right) / 2+ dp[1] v[1]
- vector<​int>​ aux; + 
- int i = left, j = mid + 1; + // caz general 
-  + for (int i = 2; i <= n; ++i) { 
-        while (i <= mid && j <= right) { + if (dp[i - 1>0) { 
- if (v[i] <v[j]) { + // extinde la dreapta cu v[i] 
- aux.push_back(v[i]); + dp[i] = dp[i - 1] v[i];
- i++;+
  } else {  } else {
- aux.push_back(v[j]); + // încep o nouă secvență 
- j++;+ dp[i] = v[i];
  }  }
- } 
-  
-        while (i <= mid) { 
- aux.push_back(v[i]);​ 
- i++; 
- } 
-  
-        while (j <= right) { 
- aux.push_back(v[j]);​ 
- j++; 
  }  }
  
- for (int left<= rightk++) { + // soluția e maximul din vectorul dp 
- v[k] = aux[k - left];+ int sol = dp[1]; 
 + for (int 2<= n; ++i) { 
 + if (dp[i> sol) { 
 + sol ​dp[i]; 
 + }
  }  }
 +
 +        return sol; // aceasta este suma asociată cu SSM
 } }
  
 +</​code>​
  
-void merge_sort(int leftint right) { +Dacă dorim să afișăm și indicii între care apare SSMputem să stocăm și poziția de start pentru fiecare soluție intermediară. . 
- if (left >right) return ; +Hint: definiți ** start[i] ** poziția pe care a început subsecvența care dă soluția cu cost dp[i].
- int mid = (left + right) / 2; +
-  +
-        merge_sort(left,​ mid); +
- merge_sort(mid + 1, right); +
- merge_halves(left,​ right); +
-}+
  
-int main() { +</​spoiler>​
- random_device rd; +
- for (int i = 0; i 10; i++) { +
- v.push_back(rd() % 100); +
- }+
  
- cout << "​Vectorul initial"; +=== Mențiuni === 
- for (int = 0; < v.size(); i++{ +Întrucât această soluție presupune calculul iterativ (coloană cu coloană) a matricei dp, complexitatea este liniară. De asemenea, se mai parcurge o dată dp pentru a găsi maximul.  
- cout << v[i] << " "; +  * **complexitate temporală **$T = O(n)$ 
-+  * **complexitate spațială ** : $S = O(n)$ 
- cout << "​\n";​+         * desigur că pentru problema SSM, nu era nevoie sa reținem, tablourile dp/start în memorie. 
 +         * puteam sa construim element cu element șmaximul din dp în aceleaștimp (întrucât ne trebuie ultima valoare la fiecare pas șmaximul global). 
 +         * în acest caz complexitatea spațială devine $S = O(1)$ 
  
- merge_sort(0v.size() - 1);+** Pentru a ilustra toți pașii posibili într-o astfel de problemătotul a fost prezentat cât mai simplu ​(NU în toate problemele putem facem simplificări de tipul "NU am nevoie să stochez tabloul dp").**
  
- cout << "​Vectorul sortat: "; +      
- for (int i 0; i < v.size(); i++) { +==== SCMAX ==== 
- cout << v[i] << " "; +=== Enunț === 
- } +Fie un vector $ $ cu $ n $ elemente întregiUn subșir de numere din șir este de forma: $v_{i_1}, v_{i_2}, ... , v_{i_k}$. Un subșir ** nu ** poate fi vid ($k >= 1$).
- cout << "​\n";​+
  
- return 0; +=== Cerința === 
-}+Să se determine subșirul crescător maximal (notat **SCMAX**) - un subșir ordonat strict crescător și are lungime maximă (dacă sunt mai multe soluții, să se gasească una oarecare).
  
-</​code>​ +=== Exemple === 
-</spoiler>+<​spoiler ​Exemplu 1> 
 +n = 6
  
-== Complexitate === +|i   ​| ​| 3 | 4 | 5| 6 | 
-Complexitatea algoritmului este dată de formula: $T(n) = D(n) + S(n) + C(n)$, unde $D(n)=O(1)$, $S(n) = * T(n / 2)$ ș$C(n) = O(n)$, rezulta $T(n) = 2 * T(n / 2) + O(n)$.+|v[i]| 100 | 12 | 13 | -1| 15 | -30 |
  
-Folosind teorema Master [[http://​people.csail.mit.edu/​thies/​6.046-web/​master.pdf|[8]]] găsim complexitatea algoritmului: $T(n) O(n * log(n))$.+Răspuns: $SCMAX 12, 13, 15$ ($SCMAX = v[2], v[3], v[5])$.
  
 +Explicație: ​
 +Toate subșirurile ordonate strict crescător sunt:
 +  * $100$
 +  * $12$
 +  * $12, 13$
 +  * $12, 13, 15$
 +  * $12, 15$
 +  * $13$
 +  * $13, 15$
 +  * $-1$
 +  * $-1, 15$
 +  * $15$
 +  * $-30$
 +Cel menționat este singurul de lungime 3.
 +</​spoiler>​
  
-  * **complexitate temporală** : $T=O(* log(n))$ +<spoiler Exemplu 2> 
-    * Ce înseamnă această metrică? Măsoara efectiv **timpul de execuție** al algoritmului (nu include citiri, afișări etc). +n = 6
-  * **complexitate spatiala** : $S=O(n)$ +
-    * Ce înseamnă această metrica? Măsoara efectiv **memoria suplimentară** folosită de algoritm (în acest caz ne referim strict la buffer-ul temporar).+
  
-<​note>​ +|  | | 3 | 4 | 5| 6 | 
-Reținețcele două convenții despre complexități de mai sus. Le vom folosi pentru restul semestrului. +|v[i]| 100 | 12 | 13 | -1| 15 | 14 |
-</​note>​ +
-==== Binary Search ==== +
-=== Enunț === +
-Se dă un **vector sortat crescător** ($v[1]$, $v[2]$, ..., $v[n]$) ce conține valori reale distincte și o valoare x. +
  
-Să se găsească la ce **poziție** ​apare x în vectorul dat.+Răspuns: 
 +  ​$SCMAX = 12, 13, 15$ ($SCMAX = v[2], v[3], v[5])$. 
 +  ​$SCMAX = 12, 13, 14$ ($SCMAX = v[2], v[3], v[6])$. 
 + 
 +Explicație:  
 +Toate subșirurile ordonate strict crescător sunt: 
 +  ​$100$ 
 +  ​$12$ 
 +  * $12, 13$ 
 +  * $12, 13, 15$ 
 +  * $12, 13, 14$ 
 +  * $13$ 
 +  * $13, 15$ 
 +  * $13, 14$ 
 +  * $-1$ 
 +  * $-1, 15$ 
 +  * $-1, 14$ 
 +  * $15$ 
 +  * $14$ 
 +Cele 2 soluții indicate au ambele lungime maximă. 
 + 
 +</​spoiler>​
  
 === Rezolvare === === Rezolvare ===
-BinarySearch (Căutare Binară), se rezolva cu un algoritm D&I:+== Tipar== 
 +Verificăm dacă se aplică tiparul de la SSM**găsim cea mai buna soluție folosind primele $i-1$ elemente din șir, apoi încercăm să o extindem folosind elementul i (adică ne extindem la dreapta ~CU~ $v[i]$)**.
  
-  ​**Divide**: împărțim vectorul în doi sub-vectori de dimensiune n/2. +    ​Dacă avem cea mai bună soluție pentru intervalul $1, 2.., i-1$ și care se termină cu $v[i-1]$, atunci încercăm să extindem soluția cu $v[i]$ (putem dacă $v[i-1] < v[i]$) 
-  * **Stăpânește**: aplicăm algoritmul de căutare binară pe sub-vectorul care conține valoarea căutată. +    Altfel.. Unde am putea să îl punem pe $v[i]$? 
-  **Combină**: soluția sub-problemei devine soluția problemei inițialemotiv pentru care nu mai este nevoie de etapa de combinare.+      ​Păi am putea sa încercăm să îl punem la finalul ​soluției care se termină pe $v[i-2]$$v[i-3]$, ..sau $v[1]$
  
-=== Pseudocod === +== Numire recurență ​== 
-<code cpp> +$ dp[i] $ lungimea celui mai lung subșir(**lungime SCMAX**folosind ​(doar o parte) din primele i elemente ​din vectorul ​și care se termină pe poziția i
-BinarySearch(v,​ left, right, x) {             // functia returneaza pozitia x pe care se afla numarul x (oricare pozitie) sa  +
-       // vom cauta cat timp intervalul de cautare nu a fost inca epuizat (are cel putin un element)  +
-       while (left <= right) { +
-            mid = (left + right/ 2          // mijlocul intervalului de cautare +
-              +
-            if (x == v[mid]return mid;      // elementul cautat este cel din mijloc +
-            if (x < v[mid]) ​ right = mid - 1; // elementul cautat este mai mic decat cel din mijloc, ne mutam in prima jumatate +
-            if (x > v[mid]) ​ left  = mid + 1; // elementul cautat este mai mare decat cel din mijloc, ne mutam in a doua jumatate +
-        } +
-         +
-        return -1;                            // in acest punct ajungem daca si numai daca x nu a fost gasit +
-+
-</​code>​+
  
-=== Complexitate === +== Mențiuni ​== 
-  * **complexitate temporală** : $T = O(log (n))$ +  * Ca să rezolvăm problema dată, trebuie să rezolvăm o mulțime de subprobleme 
-    * se deduce din recurența ​$T(n)=T(n/2) + O(1)  +    * $dp[i]reprezintă ​**soluția** pentru problema ​$v[1], ..., v[i]$ și care se termină cu $v[i]
-  ​* **complexitate spațială** $S=O(1)+  Soluția pentru problema inițială este maximul din vectorul ​$dp[i]$.
-    nu avem structuri de date complexe auxiliare +
-    * atragem atenția că acest algoritm se poate implementa și **recursiv**,​ caz în care complexitatea spațiala devine $O(log(n))$,​ întrucât salvăm pe stivă $O(log(n))parametri (întregi, referințe)+
  
-==== Turnurile din Hanoi ==== +== Găsire recurență == 
-=== Enunț === +  * **Cazul de bază*
-Se consideră 3 tije $S$ (**sursa**), $D(**destinatie**), $aux(**auxiliar**) şi n discuri de dimensiuni distincte ($1, 2, ..., n$ - ordinea crescătoare a dimensiunilorsituate inițial ​toate pe tija $Sîn ordinea ​$1,2,...,n(de la vârf către baza)+    * Și în problema SCMAXcazul pentru ​$i = 1este caz de bază.  
 +          ​dacă avem un singur elementatunci avem o singură subsecvență de lungime 1, ea este soluția 
 +          * $dp[1] = 1  
 +   
 +  ​* ** Cazul general ​*
 +      * presupune inductiv că avem rezolvate toate subproblemele mai mici 
 +      * în cazul SCMAX, presupunem că avem calculate ​dp[1]dp[2], ..., dp[i-1] ​și dorim să calculăm $ dp[i] $ (cunoaștem cea mai bună soluție folosind primele j elemente și vedem dacă elementul de pe poziția i o poate îmbunătăți ​$j = 1:i-1$) 
 +      * deoarece nu știm unde e cel mai bine să îl pune pe $v[i]$ (după care v[j]?), încercăm pentru ​toate valorile posibile ale lui j (unde $j = 1 : i - 1$
 +        * **dacă ​$v[j] < v[i] $**, atunci subșirul crescător care se termină pe poziția j, poate fi extins la dreapta cu elementul v[i], generând lungimea ** dp[j] + ** 
 +           * deci dp[i] = max(dp[j] + 1)$j = 1 : i - 1$ (dacă nu există un astfel de jvaloarea lui max(...) este 0) 
 +        * Ce se întamplă totuși dacă nu există un j care să îndeplinească condiția de mai sus? Atunci ​$v[i]$ va forma singur un subșir crescător ​de lungime 1 (care poate fi folosit ​la un pas ulterior 
 +  
 +  Reunind cele spuse mai sus: 
 +  * $dp[1] = 1$ 
 +  * $dp[i] = 1 + max(dp[j])$,​ unde $j = 1 : i-1$ **și** $v[j] < v[i]$; $i=2:n$
  
-Singura operație care se poate efectua este de a selecta un disc ce se află în vârful unei tije şi plasarea lui în vârful altei tije astfel încât să fie așezat deasupra unui disc de dimensiune mai mare decât a sa+== Implementare recurență == 
 +<spoiler Exemplu implementare>​ 
 +Problema ​se poate testa pe infoarena: [[https://​www.infoarena.ro/​problema/​scmax | Subșir crescător maximal]].
  
-Să se găsească un algoritm prin care se mută toate discurile de pe tija $S$ pe tija $D$.(problema turnurilor din Hanoi).+Mai jos se află un exemplu simplu de implementare a recurentei găsite în C++.
  
-=== Soluție === +<code cpp> 
-Pentru rezolvarea problemei folosim următoarea strategie ​[[http://www.mathcs.org/​java/​programs/​Hanoi/​index.html|[9]]]:+// n   numărul de elemente din vector 
 +// v   vectorul dat (v[1], v[2], ..., v[n- indexare de la 1 ca în explicații)
  
-  * mutăm primele $n-1$ discuri de pe tija S pe tija aux folosindu-ne de tija D. +void scmax(int ​n, vector<​int>​ &v) { 
-  * mutăm discul ​pe tija D. + vector<​int>​ dp(1);   // în explicații indexarea începe ​de la 1
-  * mutăm apoi cele $n-1$ discuri ​de pe tija aux pe tija D folosindu-ne de tija S.+
  
-Ideea din spate este că avem mereu o singura sursă si o singură destinație să atingem un scop. Întotdeauna a 3-a tija va fi considerată auxiliară și poate fi folosită pentru a atinge scopul propus.+ // caz de bază 
 + dp[1] = 1;   // [ v[1] ] este singurul subșir (crescător) care se termină pe 1
  
-=== Algoritm === + // caz general 
-<code cpp> + for (int i = 2; i <= n; ++i) { 
-// muta n discuri de pe tija S pe tija D folosind tija aux + dp[i] ​= 1;   // ​[ v[i] ] este un subșir ​(crescătorcare se termină ​pe 
-Hanoi(n, S, D, aux +  
-    if (n >= 1) { + // încerc să îl pun pe v[i] la finalul tuturor soluțiilor disponibile 
-        Hanoi(n - 1, S, aux, D);   // ​mut n-1 discuri de pe sursa (S) pe auxiliar ​(aux+ // o soluție se termină cu un element v[j] 
-                                   ​// in aceasta subproblema sursa este S, destinatia este aux, intermediarul este D + for (int j = 1; j < i; ++j{ 
-                                    + // soluția trivială: v[i] 
-        Muta_disc(S, D);           // acum pot muta direct discul n de pe sursa (Spe destinatie ​(D+ if (v[j] < v[i]
-         + // din (..., v[j]) pot obține ​(..., v[j], v[i]) 
-        ​Hanoi(n ​1aux, D, S);   ​// mut n-discuri de pe sursa (auxaici sunt ele momentanpe destinatie ​(D - scop final+ // ​(caz în care prec[i] = j
-                                   // in aceasta subproblema,​ S este auxiliar, intrucat este tija libera + 
-    }+ // voi alege j-ul curentcând alegerea îmi găsește o soluție mai bună decât ce am deja 
 + if (dp[j] + 1 > dp[i]
 + dp[i] = dp[j] + 1; 
 +
 +
 +
 +
 + 
 + // soluția e maximul din vectorul dp 
 + int sol = dp[1]pos = 1; 
 + for (int i = 2; i <= n; ++i
 + if (dp[i] > sol{ 
 + sol = dp[i]; 
 + pos = i; 
 + } 
 +
 + 
 + return sol;
 } }
 </​code>​ </​code>​
 +</​spoiler>​
  
 +<spoiler Exemplu implementare cu reconstituire>​
 +Problema se poate testa pe infoarena: [[https://​www.infoarena.ro/​problema/​scmax | Subșir crescător maximal]].
  
-=== Complexitate === 
-  * **complexitate temporală** : $T(n) = O(2^n)$ 
-    * se deduce din recurența $T(n)=2*T(n - 1) + O(1)$  ​ 
-  * **complexitate spațială** : $S(n) = O(n)$ 
-    * la un moment dat, nivelul maxim de recursivitate este n 
  
 +În [[https://​github.com/​acs-pa/​pa-lab/​tree/​main/​demo/​lab03/​02-scmax|pa-lab::​demo/​lab03/​02-scmax]] găsiți un exemplu de implementare care arată și cum puteți reconstitui SCMAX.
 +Față de implementarea anterioară,​ în această versiune se folosește un tablou auxiliar prec.
 +
 +$prec[i]$ = indicele j al elementului v[j], pentru care $dp[j] + 1 == dp[i]$ (adică acel j pentru care subșirul crescător maximal care se termină cu $v[i]$ este extinderea cu un element a celui care se termină cu $v[j]$.
 +     * dacă nu există un astfel de j, atunci $prec[i] = 0$ (prin convenție)
 +</​spoiler>​
 +=== Mențiuni ===
 +Întrucât această soluție presupune calculul iterativ (coloană cu coloană) a matricei dp, complexitatea este polinomială (pătratică - pentru fiecare element din tabloul, facem o trecere prin elementele deja calculate).
 +  * **complexitate temporală **: $T = O(n^2)$
 +    * se poate obține o soluție în complexitate $T = O(n log n)$ dacă se folosește o căutare binară pentru a găsi elementul j dorit (ex. [[https://​www.infoarena.ro/​job_detail/​1248867?​action=view-source | implemetare]]). ​
 +  * **complexitate spațială ** : $S = O(n)$
 +    * NU putem obține o complexitate spatială mai bună, întrucât avem nevoie să stocăm cel puțin vectorul dp (stocăm și vectorul **prec** dacă avem nevoie să reconstituim SCMAX)
 +
 +
 +
 +
 +===== Categoria 2: RUCSAC =====
 +Aceste recurențe au o oarecare asemănare cu problema RUCSAC - varianta discretă (enunț + soluție).
 +
 +==== RUCSAC ====
  
-==== ZParcurgere ==== 
 === Enunț === === Enunț ===
-Gigel are o tablă pătratică de dimensiuni ​$2^n * 2^n$. Ar vrea să scrie pe pătrățelele tablei numere naturale cuprinse între ​$1si $2^n 2^nconform unei parcurgeri mai deosebite pe care o numește ​**Z-parcurgere**. ​+Fie un set (vector) cu $ n $ obiecte (care nu pot fi tăiate - varianta discretă 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$
  
-O Z-parcurgere vizitează recursiv cele patru cadrane ale tablei în ordinea: ​**stânga-sus**, **dreapta-sus**, **stânga-jos**, **dreapta-jos**.+Gigel are la dispoziție un rucsac de ** volum infinit**, dar care suportă o **greutate maximă** (notată cu $W$ weight knapsack).
  
-La un moment dat, Gigel ar vrea să știe ce **număr ​de ordine** trebuie să scrie conform Z-parcurgerii ​pe anumite pătrațele date prin coordonatele lor $( xy )$. Gigel incepe umplerea tablei ​**întotdeauna** din colțul din stânga-sus. +El vrea să găsească ​** o submulțime  ​de obiecte** pe care sa le bage în rucsacastfel încât ​**suma profiturilor să fie maximă**.
-  +
-<spoiler Exemplu 1> +
-$n = 1$ si $(x, y) = (2, 2)$+
  
-Răspuns: ​$4$+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 valorează obiectul).
  
-Explicație: Matricea arată ca în exemplul următor. +=== Cerință === 
-|1|2| +Să se determine **profitul maxim** pentru Gigel.
-|3|4| +
-</​spoiler>​+
  
 +=== Exemple ===
 +<spoiler Exemplu 1>
 +$n = 5$ si $W = 10$
  
-<spoiler Exemplu ​2> +| |1|2|3|4|5| 
-$n = 2$ si $(x, y) = (33)$+|w|3|3|1|1|2| 
 +|p|6|3|2|8|5|
  
-Răspuns: ​$13$+Răspuns: ​**24** (profitul maxim)
  
-Explicație: ​Matricea arată ca în exemplul următor. +Explicație: ​va alege toate obiectele :D.
-|1|2|5|6| +
-|3|4|7|8| +
-|9|10|13|14| +
-|11|12|15|16|+
 </​spoiler>​ </​spoiler>​
  
-=== Soluție === +<spoiler Exemplu 2> 
-Analizând modul în care se **completează** tabloul/​matricea din enunț, observăm că la fiecare etapa împărțim matricea (**problema**) în 4 submatrici (**4 subprobleme**). De asemenea, șirul de numere pe care dorim să il punem în matrice se împarte în 4 secvente, fiecare corespunzând unei submatrici.+$n 5$ și $W 3$
  
-Observăm astfel că problema suportă **descompunerea** în **subprobleme disjuncte** și cu **structura similara**, ceea ce ne face să ne gândim la o soluție cu Divide et Impera. +| |1|2|3|4|5| 
- +|w|3|3|1|1|2| 
 +|p|6|3|2|8|5|
  
-=== Complexitate === +Răspuns: **13** (profitul maxim)
-  * **complexitate temporală** $T = O(n)$ +
-    ​$\log_{4} (2^n) = \frac{1}{2} \log _{2} (2^n) =  \frac{1}{2} n $   +
-  ​* **complexitate spatială** : $S=O(n)+
-    * stocăm parametri pentru recursivitate  +
-    * solutia se poate implementa si iterativ, caz in care $S = O(1)$; deoarece dimensiunile spațiului de căutare sunt $2^n * 2^n$, n este foarte mic ($n <= 15$), de aceea o solutie iterativa nu aduce nici un câștig **efectiv** în această situație.+
  
-===== Concluzii =====+Explicație:​ va alege obiectele cu indicii 4 si 5 (profit: 8 + 5) 
 +</​spoiler>​
  
-Divide et impera ​este o tehnică folosită pentru a realiza soluții pentru o anumita clasă de probleme: acestea contin ​**subprobleme disjuncte** șcu **structură similară**. În cadrul acestei tehnici se disting trei etape: dividestăpânește ​și combină.+=== Rezolvare === 
 +== Tipar == 
 +Cum am transpune tiparul de la SSM/SCMAX în problema RUCSAC? 
 +  * știm care este profitul maxim pe care îl obține dacă folosim 
 +    ​doar primul element 
 +    ​doar primele $2$ elemente 
 +    ​... 
 +    ​doar primele $i-1$ elemente 
 +  ​ajung să mă gândesc la obiectul (elementul) i 
 +    ​este posibil ca acesta să nu apară neapărat în soluția cea mai bună, caz în care nu îl folosesc, deci soluția maximă se gasește între cele menționate mai sus 
 +    ​dacă folosesc elementul i caracterizat de ($w_i, p_i$), în primul rând acesta trebuie să încapă în ghiozdan... 
 +        ​cum verific acest lucru?  
 +        * o recurență de tipul $ dp[i] = ... $ nu va fi suficientăpentru că în această problemă am 2 dimensiuni: ** obiectele ** (submulțimile de indici) ​și ** greutățile ** (asociate cu obiectele / submulțmile de obiecte).
  
-Mai multe exemple de algoritmi care folosesc tehnica divide et impera puteți găsi la [[http://​www.cs.berkeley.edu/​~vazirani/​algorithms/​chap2.pdf|[11]]].+== Numire recurență == 
 +Întrucât ​la fiecare pas trebuie să reținem ** cea mai bună soluție** folosind un **prefix** din vectorul de obiecte, dar pentru că trebuie să punem și ** o restricție de greutate** necesară (ocupată in rucsac), soluția va fi salvată într-un tablou auxiliar definit astfel:
  
-===== Exerciții ===== +$ dp[i][cap] $ profitul maxim (**profit RUCSAC**) obținut folosind (doar o parte) din primele i obiecte și având un rucsac ​de **capacitate maximă cap**
-<​note>​ +
-Scheletul ​de laborator se găsește pe pagina [[https://​github.com/​acs-pa/​pa-lab/​tree/​main/​skel/​lab01|pa-lab::​skel/​lab01]]. +
-</​note>​+
  
-=== Count occurrences === +Observații:​ 
-Se dă un șir sortat ​**v** cu **n** elemente. Găsiți numărul de elemente egale cu **x** din șir.+  * **NU** există restricție în folosirea obiectului ​în soluția menționată de $dp[i][cap]$ (a.k.a. se poate folosi sau se poate ignora). 
 +  ​Soluția problemei se găsește în $dp[n][W]$ (profitul maxim folosind (doar o parte) din primele n elemente - adică soluția bazată pe inspectarea tuturor obiectelor; capacitatea maximă folosită este W - adică soluția bazată pe ghiozdanul de capacitate maximă).
  
-<spoiler Exemplu 1> +== Găsire recurență == 
-$6si $x = 10$ +  * **Cazul de bază** 
-|i|1|2|3|4|5|6| +    * Dacă avem o submulțime vidă de obiecte selectate. 
-|v|1|2|4|10|10|20|+      * dp[0][cap] ​
 +      * Explicație:​ Dacă nu alegem obiecte, atunci profitul este 0 indiferent de capacitate.
  
-Răspuns: $2$+  * ** Cazul general ** 
 +    * $ dp[i][cap] = ? $ 
 +    * presupune inductiv că avem rezolvate toate subproblemele mai mici 
 +      * subprobleme mai mici înseamnă să folosească mai puține obiecte sau un rucsac cu capacitatea mai mică 
 +      * vedem dacă prin folosirea obiectului i, obținem cea mai bună soluție in $dp[i][cap]$ 
 +        * **NU folosesc obiectul i** 
 +          * în acest caz, o să alegem cea mai bună soluție formată cu celelalte $i-1$ elemente și aceeași capacitate a rucsacului 
 +          * soluția generată de acest caz: $dp[i][cap] = dp[i - 1][cap]$ 
 +        * **folosesc obiectul i** 
 +          * dacă îl folosesc, înseamnă că pentru el trebuie să am rezervată în rucsac o capacitate egală cu $w_i$ 
 +            * adică când am selectat dintre primele $i-1$ elemente, nu trebuia să ocup mai mult de $cap - w_i$ din capacitatea rucsacului 
 +            * față de subproblema menționată,​ câștig în plus $p_i$ (profitul pe care îl aduce acest obiect 
 +          * soluția generată de acest caz: $dp[i][cap] = dp[i - 1][cap - w_i] + p_i$  
 +   
 +   ​Reunind cele spuse mai sus, obținem: 
 +   * $dp[0][cap] = 0$, pentru $cap = 0 : W$ 
 +   * $dp[i][cap] = max(dp[i - 1][cap], dp[i - 1][cap - w_i] + p_i)$ 
 +     * pentru $i = 1: n$, $cap = 0:W
  
-Explicație10 apare de 2 ori in sir.+== Implementare recurență == 
 +<spoiler Exemplu implementare>​ 
 +Problema se poate testa pe infoarena:[[https://​www.infoarena.ro/​problema/​rucsac | Problema rucsacului]].
  
-</​spoiler>​+Mai jos se află un exemplu simplu de implementare a recurenței găsite in C++.
  
-Task-uri: 
-   * Aceasta problemă este deja rezolvată. Pentru a vă acomoda cu scheletul, va trebui sa faceți câțiva pași: 
-     * Rulați comanda $./​check.sh$ si cititi cum se foloseste checker-ul. 
-     * Rulați comanda necesara pentru a rula task-ul 1. Sursa nu implementeaza corect algoritmul si returneaza valori default. Din acest motiv primiti mesajul **WRONG ANSWER**. 
-     * Copiati următoarea sursă în folderul corespunzător. Rulați comanda anterioară. Observați mesajele afișate când ați rezolvat corect un task. 
-<spoiler Solutie C++> 
-Sursa **main.cpp** asociata cu task 1 este mai jos. 
 <code cpp> <code cpp>
-#include <​bits/​stdc++.h>​ 
-using namespace std; 
  
-class Task { +// n   = numărul de obiecte din colecție 
-public: +// W   = capacitatea maximă a rucsacului 
-    void solve() +// (w[i], p[i]= caracteristicile obiectului i ($i = 1 : n)
-        read_input()+
-        print_output(get_result());​ +
-    }+
  
-private: +int rucsac(int n, int W, vector<​int>​ &w, vector<​int>​ &p) { 
-    ​int n, x; +    // dp este o matrice de dimensiune (n + 1) (W + 1) 
-    vector<​int> ​v;+    // pentru că folosim dp[0][*] pentru mulțimea vidă 
 +    //                   ​dp[*][0] pentru situația în care ghiozdanul are capacitate 0 
 +    ​vector< ​vector<​int> ​> dp(n + 1, vector<​int>​(W + 1, 0));
  
-    ​void read_input() { +    ​// cazul de bază 
-        ​ifstream fin("​in"​);​ +    for (int cap = 0; cap <= W; ++cap) { 
-        fin >> n; +        ​dp[0][cap] = 0;
-        ​for (int = 0, eni++) { +
-            fin >> e; +
-            v.push_back(e);​ +
-        ​+
-        fin >> x; +
-        fin.close();+
     }     }
  
-    int find_first() { +    ​// cazul general 
-        int left = 0, right = n - 1, mid, res = -1; +    for (int i = 1; i <= n; ++i) { 
-        while (left <= right) { +        ​for (int cap = 0; cap <= W; ++cap) { 
-            ​mid (left + right) / 2; +            ​// nu folosesc obiectul i => e soluția de la pasul i - 1 
-            ​if (v[mid>x) { +            ​dp[i][cap] = dp[i-1][cap];
-                res = mid; +
-                right = mid - 1; +
-            } else { +
-                left = mid + 1; +
-            } +
-        } +
-        return res; +
-    }+
  
-    int find_last() { +            // folosesc obiectul ideci trebuie să rezerv w[i] unități în rucsac 
-        int left = 0right = n - 1, mid, res = -1; +            // înseamnă ca înainte trebuie să ocup maxim cap - w[i] unități 
-        while (left <= right) { +            if (cap - w[i>0) { 
-            ​mid = (left + right) ​2; +                ​int sol_aux ​dp[i-1][cap - w[i]] + p[i]
-            if (v[mid<x) { +                 
-                ​res mid+                ​dp[i][cap] ​max(dp[i][cap],​ sol_aux);
-                ​left = mid + 1; +
-            } else { +
-                ​right mid - 1;+
             }             }
         }         }
-        return res; 
     }     }
  
-    ​int get_result() { +    return ​dp[n][W];
-        int first = find_first();​ +
-        int last = find_last();​ +
-        if (first == -1 || last == -1) { +
-            ​return ​0; +
-        } +
-        return last - first + 1; +
-    } +
- +
-    void print_output(int result) { +
-        ofstream fout("​out"​);​ +
-        fout << result; +
-        fout.close();​ +
-    } +
-}; +
- +
-// [ATENTIENU modifica functia main! +
-int main() { +
-    // * se aloca un obiect Task pe heap +
-    // (se presupune ca e prea mare pentru a fi alocat pe stiva) +
-    // * se apeleaza metoda solve() +
-    // (citire, rezolvare, printare) +
-    // * se distruge obiectul si se elibereaza memoria +
-    auto* task = new (std::​nothrow) Task{}; // hint: cppreference/​nothrow +
-    if (!task) { +
-        std::cerr << "new failed\n";​ +
-        return -1; +
-    } +
-    task->​solve();​ +
-    delete task; +
-    return 0;+
 } }
 </​code>​ </​code>​
 +
 </​spoiler>​ </​spoiler>​
  
-<spoiler Solutie Java> +=== Mențiuni === 
-Sursa **Main.java** asociata cu task 1 este mai jos. +Întrucât această soluție presupune calculul iterativ (linie cu linie) a matricei dp, complexitatea este polinomială. ​  
-<code cpp> +  ​* **complexitate temporală ​**: $T = O(n * W)$ 
-import java.io.File;​ +  * **complexitate spațială ** : $S = O(n * W)$ 
-import java.io.IOException;​ +        * dacă nu ne interesează să reconstituim soluția (să afișăm submulțimea efectiv), atunci putem să NU stocăm toată matricea dp 
-import java.io.PrintWriter;​ +        * ca să calculăm o linie, avem nevoie doar de ultima linie 
-import java.util.Scanner;​+        * putem să stocăm la orice moment de timp doar ultima linie și linia curentă 
 +        * complexitatea spațială se reduce astfel la $S = O(W)$ 
  
-public class Main { 
- static class Task { 
- public final static String INPUT_FILE = "​in";​ 
- public final static String OUTPUT_FILE = "​out";​ 
  
- int n; +===== Exercitii ===== 
- int[] v; +<​note>​ 
- int x;+Scheletul de laborator se găsește pe pagina ​[[https://​github.com/​acs-pa/​pa-lab/​tree/​main/​skel/​lab03|pa-lab::​skel/​lab03]]. 
 +</​note>​
  
- private void readInput() { +=== 1Not again! ​=== 
- try { +Gigel are o colectie impresionanta de monedeEl ne spune ca are ** n tipuri** de monede, avand un numar nelimitat de monede din fiecare tip. Cunoscand aceasta informatie ​(data sub forma unui vector **v** cu **n** elemente), el se intreaba care este numarul minim de monede cu care poate plati o **suma S**.
- Scanner sc new Scanner(new File(INPUT_FILE));​ +
-sc.nextInt();​ +
-new int[n]; +
- for (int i 0; i < n; i++) { +
- v[i] ​sc.nextInt();​ +
- +
- x = sc.nextInt()+
- sc.close(); +
- } catch (IOException e) { +
- throw new RuntimeException(e);​ +
-+
- }+
  
- private void writeOutput(int count) { +Task-uri: 
- try { +  * 1.1 **Determinati numarul minim de monede** ​(din cele pe care le arecu care Gigel poate forma suma S.  
- PrintWriter pw = new PrintWriter(new File(OUTPUT_FILE)); +  * 1.2 Care este complexitatea solutiei ​(timp + spatiu)? De ce?
- pw.printf("​%d\n",​ count); +
- pw.close()+
- } catch (IOException e) { +
- throw new RuntimeException(e);​ +
-+
- }+
  
- private int findFirst() { +Este posibil ca pentru anumite valori ale lui S si vaceasta problema sa nu aiba solutie. In acest caz raspunsul este **-1**.
- int left = 0right = n - 1, mid, res = -1; +
- while (left <= right) { +
- mid = (left + right) / 2; +
- if (v[mid] >= x) { +
- res = mid; +
- right = mid - 1; +
- } else { +
- left = mid + 1; +
-+
-+
- return res; +
- }+
  
- private int findLast() { +<spoiler Exemplu 1> 
- int left = 0, right = - 1, mid, res -1; +n = 4$ si $ S 12$ 
- while (left <right) { +|i|1|2|3|4| 
- mid = (left + right) / 2; +|v|1|2|3|6|
- if (v[mid] <= x) { +
- res = mid; +
- left = mid + 1+
- } else { +
- right = mid - 1; +
-+
-+
- return res; +
- }+
  
- private int getAnswer() { +Raspuns: $2$
- int first = findFirst();​ +
- int last = findLast();​ +
- if (first == -1 || last == -1) { +
- return 0; +
-+
- return last - first + 1; +
- }+
  
- public void solve() { +Explicatie: Avem 4 tipuri de monede: 1 euro, 2 euro, 3 euro si 6 euro (lui Gigel nu ii mai place sa foloseasca RON). Avem la dispozitie oricate monede din fiecare tip. 
- readInput();​ +Suma 12 poate fi obtinuta in urmatoarele moduri: 
- writeOutput(getAnswer());​ +  * $12 = 6 + 6$ 
- } +  * $12 = 6 + 3 + 3$ 
- }+  * $12 = 6 + 3 + 2 + 1$ 
 +  * $12 = 6 + 2 + 2 + 2$ 
 +  * $12 = 6 + 3 + 3$ 
 +  * $12 = 3 + 3 + 3 + 3$ 
 +  * $12 = 3 + 3 + 3 + 2 + 1$ 
 +  * $12 = 3 + 3 + 2 + 2 + 2$ 
 +  * $12 = 3 + 2 + 2 + 2 + 2 + 1$ 
 +  * $12 = 2 + 2 + 2 + 2 + 2 + 2$ 
 +  * ... (ati inteles ideea :D)
  
- public static void main(String[] args) { +Solutia cu numar minim de monede se obtine pentru modul $6 + 6$.
- new Task().solve(); +
-+
-+
- +
-</​code>​+
 </​spoiler>​ </​spoiler>​
  
-   * Înțelegețsoluția oferită împreună cu asistentul vostru. +<spoiler Exemplu 2> 
-   * Care este complexitatea soluției (timp + spațiu)? De ce?+$ n = 3$ si $ S = 11$ 
 +|i|1|2|3| 
 +|v|1|2|5|
  
-=== SQRT === +Raspuns: ​$3$
-Se dă un număr real **n**. Scrieți un algoritm de complexitate **O(log n)** care să calculeze ​$sqrt(n)cu o precizie de $0.001$. ​+
  
-<spoiler Exemplu ​1> +Explicatie: Avem 3 tipuri de monede: ​euro, 2 euro si 5 euro (lui Gigel nu ii mai place sa foloseasca RON). Avem la dispozitie oricate monede din fiecare tip. 
-0.25 $+Suma 11 poate fi obtinuta in urmatoarele moduri: 
 +  * $11 5 + 5 + 1$ 
 +  * $11 = 5 + 2 + 2 + 2$ 
 +  * $11 = 5 + 2 + 2 + 1 + 1$ 
 +  * $11 = 5 + 2 + 1 + 1 + 1 + 1$ 
 +  * $11 = 5 + 1 + 1 + 1 + 1 + 1 + 1$ 
 +  * $11 = 2 + 2 + 1 + 1 + 1 + 1 + 1 + 1 + 1$ 
 +  * $11 = 2 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1$ 
 +  * $11 = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1$
  
-Răspuns: orice valoare intre $0.499si $0.501$ (inclusiv)+Solutia cu numar minim de monede se obtine pentru modul $5 + 5 + 1$.
 </​spoiler>​ </​spoiler>​
  
-<​note>​ 
-Pentru a putea trece testele, trebuie sa afișați rezultatul cu **cel puțin ** 4 zecimale. 
  
-</note>+<spoiler Exemplu 3> 
 +$ n = 3$ si $ S = 11$ 
 +|i|1|2|3| 
 +|v|2|4|6|
  
-=== ZParcurgere === +Raspuns: $-1$
-Rezolvați problema ZParcurgere folosind scheletul pus la dispoziție. Enunțul și explicațiile le găsiți în partea de seminar.+
  
-=== Exponențiere logaritmică === +Explicatie: Nu putem forma suma 11 folosind tipurile ​(valorile2, 4, 6. 
-Se dau două numere naturale **base** și  **exponent**. Scrieți un algoritm de complexitate $O(log (exponent))$ care să calculeze ${base} ^ {exponent} \ \% \  MOD $+</​spoiler>​
  
-<note+<hidden
-Întrucât expresia $ {base} ^ {exponent} $ este foarte mare, dorim să aflăm doar **restul** împărțirii lui la un număr **MOD**.+<spoiler Solutie>​ 
 +Problema preluata de [[https://​leetcode.com/​problems/​coin-change/​description/​|aici]]. Solutia ​este [[https://​leetcode.com/​problems/​coin-change/​solution/​ | aici]]. 
 +</​spoiler>​ 
 +</​hidden>​
  
 +=== 2. CMLSC ===
 +Fie doi vectori cu numere intregi: **v** cu **n** elemente si **w** cu **m** elemente. Sa se gaseasca ** cel mai lung subsir comun ** (notat **CMLSC**) care apare in cei doi vectori. Se cere o solutie de complexitate optima. Daca exista mai multe solutii, se poate gasi oricare.
  
-Proprietăți matematice necesare+Task-uri
-    $(a + b  \ \% \   MOD = ((a   \ \% \   MOD) + (b   \ \% \   ​MOD)) ​  \ \% \   MOD $  +  2.1 **Determinare lungime** CMLSC. ​(Hint: DP
-    $(a \ b)   \ \% \   MOD = ((a   \ \% \   MOD) \ * (b   \ \% \   MOD))   \ \% \   MOD $  +  2.2 **Reconstituire** CMLSC (afisati si care sunt termenii CMLSC). 
-  ​+  ​* 2.3 Care este **complexitatea** solutiei (timp + spatiu)? De ce?
  
-Atenție la înmulțire! Rezultatul **temporar** poate provoca un overflow.+Rezolvati in ordine task-urile.
  
-Soluții: + 
-  * **C++**: $a b$ => $1LL b$ +<​note>​ 
-  ​* **Java**: $a * b$ =$1L * a * b+**subsir** (**subsequence** in engleza) pentru un vector ​** **inseamna un alt vector ​$[v[i_1], v[i_2],..., v[i_k]]]unde $i_1 < i_2 < ... < i_k$. 
-</​note>​+</​note> ​
  
 <spoiler Exemplu 1> <spoiler Exemplu 1>
-base  ​$,  ​exponent = 10$, $MOD = 5$+si = 5$ 
 +|i|1| 2|3| 
 +|v|6|-1|9|
  
-Răspuns: $4$+|j|1| 2|3|4|5| 
 +|w|0|6|2|9|8|
  
-Explicatie: $2^{10} \ \% \ 5 4$+Raspuns: $lungime = 2$, $CMLSC ​[6, 9]$ 
 + 
 +Explicatie: Toate subsirurile comune posibile sunt: 
 +  * $[6]$ 
 +  * $[6, 9]$ 
 +  * $[9]$ 
 +Solutia mentionata are lungime maxima.
 </​spoiler>​ </​spoiler>​
  
  
-==== Bonus ====+<spoiler Exemplu 2> 
 +$ n 8 $ si $ m 5$ 
 +|i|1|2|3|4|5|6|7|8| 
 +|v|2|1|5|3|4|5|2|7|
  
-<spoiler Inversiuni>​ +|j|1|2|3|4|5| 
-Se da un sir $S$ de n numere intregi. Sa se detemine cate inversiuni sunt in sirul dat. Numim inversiune o pereche de indici $< = i < j < = n$ astfel incat $S[i] > S[j]$+|w|1|5|5|7|4|
  
-<note tip> +Raspuns$lungime = 4$, $CMLSC = [1, 5, 57]$
- ​Exempluin sirul ''​{0 1 9 4 5 7 6 8 2}''​ sunt 12 inversiuni. +
-</​note>​ +
- +
-<​note>​ +
-Aceasta problema nu are schelet. +
-</​note>​+
  
 +Explicatie: Toate subsirurile comune posibile sunt (duplicatele vor fi mentionate o singura data):
 +  * $[1]$
 +  * $[1, 5]$ 
 +  * $[1, 7]$  ​
 +  * $[1, 4]$
 +  * $[1, 5, 5]$
 +  * $[1, 5, 7]$
 +  * $[1, 5, 4]$
 +  * $[1, 5, 5, 7]$
 +  * $[5]$
 +  * $[5, 5]$
 +  * $[5, 7]$
 +  * $[5, 4]$
 +  * $[4]$
 +Solutia mentionata are lungime maxima.
 </​spoiler>​ </​spoiler>​
  
-<spoiler ClassicTask>​ 
-Testați soluția voastră de la **Exponentiere logaritmica** pe infoarena, la problema [[https://​infoarena.ro/​problema/​classictask | ClassicTask ]] (trebuie să modificați numele fișierelor). 
  
-Identificați problema și modificați sursa astfel încât să luați punctaj maxim. 
-</​spoiler> ​ 
-==== Extra ==== 
-<spoiler Statistici de ordine> 
-Se dă un vector de numere întregi neordonate. Scriind o funcție de partitionare,​ folosiți **Divide et Impera** pentru 
-    * a determina a k-lea element ca mărime din vector 
-    * a sorta vectorii prin QuickSort 
  
-Puteti testa problema partition [[https://​leetcode.com/​problems/​kth-largest-element-in-an-array/​description/​|aici]]. Problema [[https://​en.wikipedia.org/​wiki/​Quicksort|QuickSort]] (chiar si MergeSort) poate fi testata [[https://​infoarena.ro/​problema/​algsort|aici]].+<spoiler Exemplu 3> 
 +$ n = 8 $ si $ m = 5$ 
 +|i|1|2|3|4|5|6|7|8| 
 +|v|2|1|5|3|4|5|2|7|
  
 +|j|1|2|3| 4|5|
 +|w|1|5|7|-5|4|
  
-<note tip> +Raspuns: $lungime = 3$, $CMLSC = [1, 5, 7]$ (exemplu de solutie) 
- Exemplupentru vectorul ''​{0 ​2 4 5 7 6 8 9}''​al 3-lea element ca ordine este 2iar vectorul sortat este {0 4 5 8 9} + 
-</​note>​+ExplicatieToate subsirurile comune posibile sunt (duplicatele vor fi mentionate o singura data): 
 +  * $[1]$ 
 +  * $[1, 5]$  
 +  * $[1, 7]$   
 +  * $[14]$ 
 +  * $[15, 7]$ 
 +  * $[1, 5, 4]$ 
 +  * $[5]$ 
 +  * $[5, 7]$ 
 +  * $[5, 4]$ 
 +  * $[4]$ 
 +Solutii pot fi: $[1, 5, 7]$ si $[1, 5, 4]$. 
 +Pentru $[1, 5, 7]$, se observa ca sunt 2 astfel de subsiruri in vectorul v. Oricare este bun.
 </​spoiler>​ </​spoiler>​
  
-<spoiler Missing number> 
-Se dau $n - 1$ numere naturale distincte intre $0$ si $n - 1$. Scriind o functie de partitionare,​ determinati numarul lipsa. 
  
-<note tip+<hidden
- Exemplupentru ​$9$ si vectorul ​${0 9 4 5 7 6 8 2}$, numarul lipsa este $3$. +<spoiler Solutie>​ 
-</​note>​+Problema preluata de [[https://​infoarena.ro/​problema/​cmlsc|aici]]. 
 + 
 +  * $dp[i][j]$ = lungimea celui mai lung subsir comun pentru sub-vectorii $v[1..i]$ si $w[1..j]$ 
 +  * $dp[i][0] = 0$ ; $dp[0][j] = 0
 +  * $dp[i][j] = (v[i] == w[j] \ ? \ dp[i-1][j-1] + 1 : max(dp[i][j-1],​ dp[i-1][j])$
  
 +Solutia este $dp[n][m]$. Reconstituirea se face pe baza matricei dp. Din starea (i, j) se poate trece in starea $(p, q) = (i-1, j-1) sau (i-1, j) sau (i, j-1)$, pe baza relatiei dintre valorile din dp. Obtinem astfel un CMLSC inversat.
 +Complexitate:​ $O(n * m)$.
 +</​spoiler>​
 +</​hidden>​
 +=== BONUS ===
 +<spoiler Custi>
 +Rezolvati pe infoarena problema [[https://​infoarena.ro/​problema/​custi | custi]].
 </​spoiler>​ </​spoiler>​
  
  
 +=== Extra === 
  
-<spoiler Fractal> 
-Puteți rezolva această problemă pe [[https://​infoarena.ro/​problema/​fractal| infoarena]]. 
-</​spoiler>​ 
  
-<​spoiler ​Hanoi+<​spoiler ​Rucsac
-Puteți rezolva această problemă ​pe [[https://www.infoarena.ro/​problema/​hanoiinfoarena]].+Modificati solutia de la Rucsac prezentata in laborator pentru a obtine o complexitate spatiala mai buna (se va retine un numar minim de linii din matrice). 
 +Puteti testa solutia voastra ​pe infoarena la problema ​[[https://​infoarena.ro/​problema/​rucsac ​rucsac]].
 </​spoiler>​ </​spoiler>​
  
 +<spoiler Sumele lui Gigel>
 +Se da o matrice de dimensiuni $n * m$ si Q intrebari de forma: "Care este suma din submatricea care are coltul stanga-sus (x, y) si coltul dreapta-jos (p,​q)?"​
  
 +Se considera proprietatea:​ Q este mult mai mare decat dimensiunile matricei.
  
-<​spoiler ​SSM> +Sa se raspunda in mod eficient la cele Q intrebari. 
-Puteți rezolva aceasta problema de [[https://​ocw.cs.pub.ro/​courses/​pa/​laboratoare/​laborator-03#​ssm +</spoiler>
-| aici]].+
  
 +<spoiler joctv>
 +Rezolvatie pe infoarena problema [[https://​infoarena.ro/​problema/​joctv| joctv]].
 +    ​
 <​note>​ <​note>​
-Pentru ​problema SSM vom studia o soluție potrivita ​in lab03Puteți să încercați să o rezolvați cu Divide et Impera pentru a exersa cele învățate astăzi.+Solutie: Se fixeaza 2 linii pentru zona dreptunghiulara. Se reduce ​problema ​la SSM in O(n). 
 +Complexitate:​ $O(n^3)$.
 </​note>​ </​note>​
 </​spoiler>​ </​spoiler>​
  
-<spoiler Secventa descrescatoare>​ 
-Dându-se N numere întregi sub forma unei secvenţe de numere strict crescătoare,​ care se continuă cu o secvenţă de întregi strict descrescătoare,​ se doreşte determinarea punctului din întregul şir înaintea căruia toate elementele sunt strict crescătoare,​ şi dupa care, toate elementele sunt strict descrescătoare. Considerăm evident faptul că acest punct nu există dacă cele N numere sunt dispuse într-un şir fie doar strict crescător, fie doar strict descrescător. 
  
 +<​spoiler ​ Interleaving String>
 +Rezolvati pe leetcode problema [[ https://​leetcode.com/​problems/​interleaving-string/​description/​ |  Interleaving String]].
 </​spoiler>​ </​spoiler>​
  
 +<spoiler Cutting rod>
 +https://​www.geeksforgeeks.org/​cutting-a-rod-dp-13/​ [[https://​www.geeksforgeeks.org/​cutting-a-rod-dp-13/​ | Cutting rod]] [0]
 +</​spoiler>​
 +
 +<spoiler Partition Problem>
 +[[ https://​www.geeksforgeeks.org/​partition-problem-dp-18/​|Partition Problem]]
 +</​spoiler>​
  
 ===== Referințe ===== ===== Referințe =====
  
-[0] Chapter **Divide-and-Conquer**, "Introduction to Algorithms", Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford 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/​ssm]] 
 + 
 +[2] [[http://​infoarena.ro/​problema/​scmax]]
  
 +[3] [[http://​infoarena.ro/​problema/​rucsac]]
  
pa/laboratoare/laborator-01.1615239059.txt.gz · Last modified: 2021/03/08 23:30 by radu.nichita
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0