Differences

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

Link to this comparison view

pa:laboratoare:laborator-04 [2022/02/20 21:17]
darius.neatu [Laborator 4: Programare Dinamică (continuare)]
pa:laboratoare:laborator-04 [2026/03/24 13:15] (current)
radu.nichita [Problema damelor (AC3)]
Line 1: Line 1:
-====== Laborator 4: Programare Dinamică (continuare) ​====== +====== Laborator 04: Backtracking ​====== 
-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: +
-  * [[neatudarius@gmail.com|Darius-Florentin Neațu (2018)]] +
-  * [[visanr95@gmail.com|Radu Vișan (2018)]] +
-  * [[cristb@gmail.com|Cristian Banu (2018)]] +
-  * [[razvan.ch95@gmail.com|Răzvan Chițu (2018)]] +
- +
 ===== Obiective laborator ===== ===== Obiective laborator =====
-  * Înțelegerea ​noțiunilor de bază despre ​programarea dinamică. +  * Întelegerea ​noțiunilor de bază despre ​backtracking;​ 
-  * Însușirea abilităților de implementare a algoritmilor bazați pe programarea dinamică.+  * Însușirea abilităților de implementare a algoritmilor bazați pe backtracking;​ 
 +  * Rezolvarea unor probleme NP-complete în timp exponențial.
  
 ===== 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/​lab04|pa-lab::​demo/​lab04]].+Toate exemplele de cod se găsesc pe pagina [[https://​github.com/​acs-pa/​pa-lab/​tree/​main/​demo/​lab05|pa-lab::​demo/​lab05]].
  
-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 cadin cauza mai multor factori (formatare, caractere invizibile puse de browseretc.)un simplu copy-paste să nu fie de ajuns pentru a compila codul. +  * Toate bucățile de cod prezentate în partea introductivă a laboratorului (înainte de exerciții) au fost testate. Cu toate acestea, este posibil ca din cauza mai multor factori (formatare, caractere invizibile puse de browser etc.) un simplu copy-paste să nu fie de ajuns pentru a compila codul. 
-  * Vă rugăm ​să compilați **DOAR** codul de pe GitHub. Pentru raportarea problemelor,​ contactați unul dintre maintaineri.  +  * Vă 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ă rugăm ​să dați e-mail unuia dintre responsabili.
  
-===== Ce este DP? ===== 
-Similar cu greedy, tehnica de programare dinamică este folosită pentru rezolvarea **problemelor de optimizare**. ​ 
-În continuare vom folosi acronimul **DP (dynamic programming)**. 
  
 +===== Ce este Backtracking?​ =====
 +Backtracking este un algoritm care caută **una sau mai multe soluții** pentru o problema, printr-o căutare exhaustiva, mai eficientă însă în general decât o abordare „generează si testează”,​ de tip „forță brută”, deoarece un candidat parțial care nu duce la o soluție este abandonat. Poate fi folosit pentru orice problemă care presupune o căutare în **spațiul stărilor**.
 +În general, în timp ce cautăm o soluție e posibil să dăm de un deadend în urma unei alegeri greșite sau să găsim o soluție, dar să dorim să căutăm în continuare alte soluții. În acel moment trebuie să ne întoarcem pe pașii făcuți (**backtrack**) și la un moment dat să luăm altă decizie.
 +Este relativ simplu din punct de vedere conceptual, dar complexitatea algoritmului este exponentială.
  
-De asemeneaDP se poate folosi ​și pentru probleme în care nu căutam un optimcum ar fi **problemele de numărare**.+===== Importanța – aplicaţii practice ===== 
 +Există foarte multe probleme (de exempluproblemele NP-complete sau NP-dificile) care pot fi rezolvate prin algoritmi de tip backtracking mai eficient decât prin „forta bruta” (adică generarea tuturor alternativelor ​și selectarea soluțiilor). Atenție însă, complexitatea computațională este de cele mai multe ori **exponențială**. O eficientizare se poate face prin combinarea cu tehnici de propagare a restricțiilor. Orice problemă care are nevoie de parcurgerea spațiului de stări se poate rezolva cu backtracking
  
-Pentru noțiunile prezentate până acum despre DP, vă rugăm să consultați pagina laboratorului 3. +===== Descrierea problemei și a rezolvărilor ​===== 
-===== Exemple clasice ​=====+Pornind de la strategiile clasice de parcurgere a **spațiului de stări**, algoritmii de tip backtracking practic enumeră un set de candidați parțiali, care, după completarea definitivă,​ pot deveni soluții potențiale ale problemei inițiale. Exact ca strategiile de **parcurgere în lățime/​adâncime** și backtracking-ul are la bază expandarea unui nod curent, iar determinarea soluției se face într-o manieră incrementală. Prin natura sa, backtracking-ul este recursiv, iar în arborele expandat top-down se aplică operații de tipul pruning (tăiere) dacă soluția parțială nu este validă.
  
-Programarea Dinamică este cea mai flexibilă tehnică a programării. Cel mai ușor mod de a înțelege presupune parcurgerea cât mai multor exemple.+===== Algoritm de baza ===== 
 +<code C> 
 +/* Domain - domeniul curent (cu un cardinal ​mai mic decat la pasul trecut) 
 +Solution - solutia curenta pe care extindem catre cea finala */ 
 +back(Domain,​ Solution):​ 
 +    if check(Solution):​ 
 +        print(Solution) 
 +        return
  
-Propunem câteva categorii de recurențe pe care le vom grupa astfel+    for value in Domain
-  ​* ​ recurențe de tip **SSM** ​(Subsecvență de Sumă Maximă+        ​NextSolution = Solution.push(value
-  ​* ​ recurențe de tip **RUCSAC** +        ​NextDomain = Domain.erase(value
-  *  recurențe de tip **PODM** ​(Parantezare Optimă de Matrice+        ​back(NextDomain,​ NextSolution) 
-  ​* ​ recurențe de tip **numărat** +</​code>​
-  ​* ​ recurențe pe **grafuri**+
  
 +{{ pa:​laboratoare:​perm_gen_viz.png?​700|}}
  
-<​note>​ +===== Algoritm de baza (modificat pentru transmitere prin referinta) ​===== 
-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. +<code C> 
-</​note>​ +/* Domain - domeniul curent (cu un cardinal mai mic decat la pasul trecut) 
-===== Categoria 3: PODM ===== +Solution - solutia curenta pe care o extindem catre cea finala */ 
-Aceste recurențe au o oarecare asemănare ​cu problema PODM (enunț + soluție).+back(Domain, Solution)
 +    if check(Solution):​ 
 +        print(Solution) 
 +        return
  
 +    for value in Domain:
 +        /* DO */
 +        Solution = Solution.push(value)
 +        Domain = Domain.erase(value)
 +        ​
 +        /* RECURSION */
 +        back(Domain,​ Solution)
 +        ​
 +        /* UNDO */
 +        Solution = Solution.pop()
 +        Domain = Domain.insert(value)
 +</​code>​
  
-**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.+{{ :​pa:​laboratoare:​perm_ref_viz.png?700 |}}
  
-Caracteristici:+===== Exemple clasice ===== 
 +Ne vom ocupa în continuare de următoarele probleme: 
 +  * Permutări 
 +  * Combinări 
 +  * Aranjamente 
 +  * Submulțimi 
 +  * Generare de șiruri 
 +  * Problema damelor 
 +  * Problema șoricelului 
 +  * Tic-Tac-Toe 
 +  * Sudoku 
 +  * Ultimate Tic-Tac-Toe
  
-  * Acest tip de problemă presupune că o putem formula ca pe o problemă de tip **subinterval $[i, j]$**. ​ +<​note>​ 
-  * 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]$**. +Sudoku ​și Ultimate Tic-Tac-Toe sunt probleme ​foarte grele. În general nu putem explora tot spațiul stărilor pentru un input arbitrar dat
-  * Se consideră fiecare divizare în 2 subprobleme,​ dată de intermediarul k, astfel: +</​note>​
-    * 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 ​==== +==== Permutări ​====
-=== PODM ===+
  
-== Enunț == +=== Enunț ===
-Fie un produs matriceal $M M_1 M_2 ... M_n$. Putem pune paranteze în mai multe moduri și vom obține același rezultat (înmulțire asociativă),​ dar este posibil să obținem un număr diferit de **înmulțiri scalare**.+
  
-Matricea $M_i$ are (prin convenție), dimensiunile $d_{i-1} d_{i}$.+Se dă un număr N. Să se genereze toate permutările mulțimii formate din toate numerele de la la N.
  
-== Cerință ​== +=== Exemple ===
-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> <spoiler Exemplu 1>
  
-$n = 3+= 3 => M = {123}
-|i|0|1|2|3|| +
-|d|2|3|4|5||+
  
-Răspuns: ** 64 **  ​(înmulțiri scalare)+Solutie: 
 +  ​{1, 2, 3} 
 +  ​{1, 3, 2} 
 +  ​{2, 1, 3} 
 +  ​{2, 3, 1} 
 +  * {3, 1, 2} 
 +  * {3, 2, 1}
  
-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>​
  
-<spoiler Exemplu 2>+=== Soluții ===
  
-$n 4$ +=== Backtracking (algoritmul în cazul general) === 
-|i|0|1|2|3|4| +<spoiler Implementare>​
-|d|2|3|4|2|3|+
  
-Răspuns: ​** 48 **  ​(înmulțiri scalare)+<code cpp> 
 +/deoarece numerele sunt sterse din domeniu odata ce sunt folosite, soluția generata este garantata 
 +sa nu contina duplicate. Astfel, atunci cand domeniul ajunge vid, soluția este intotdeauna corecta ​*
 +bool check(std::​vector<​int>​ solution
 +    return true; 
 +}
  
-ExplicațieAvem 4 matrice: +void printSolution(std::vector<​int>​ solution{ 
-   * A de dimensiuni (2, 3+    ​for ​(auto &s : solution{ 
-   * B de (3, 4) +        std::cout << s << " "; 
-   * C de (4, 2) +    } 
-   * D de (2, 3) +    std::cout << "​\n";​ 
-    +}
-   Î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>​+
  
-<spoiler Exemplu 3>+void back(std::​vector<intdomain, std::​vector<​int>​ solution) { 
 +    /* dupa ce am folosit toate elementele din domeniu putem verifica daca 
 +    am gasit o solutie */ 
 +    if (domain.size() == 0) { 
 +        if(check(solution)) { 
 +            printSolution(solution);​ 
 +        } 
 +        return; 
 +    }
  
-$n = 4$ +    /* incercam sa adaugam in solutie toate valorile din domeniu, pe rand */ 
-|i|0|1|2|3|4| +    for (unsigned int 0; i < domain.size();​ ++i) { 
-|d|13|5|89|3|34|+        /* cream o solutie noua si un domeniu nou care sunt identice cu cele 
 +        de la pasul curent */ 
 +        ​std::​vector<​int>​ newSolution(solution),​ newDomain(domain);​
  
-Răspuns: ​** 2856 **  (înmulțiri scalare)+        /adaugam in noua solutie elementul ales din domeniu ​*
 +        newSolution.push_back(domain[i]);​ 
 +        /stergem elementul ales din noul domeniu ​*
 +        newDomain.erase(newDomain.begin() + i);
  
-Explicație:​ Avem 4 matrice: +        /apelam recursiv backtracking pe noul domeniu si noua solutie */ 
-   A de dimensiuni ​(135+        back(newDomainnewSolution)
-   * B de (5, 89+    } 
-   ​de (893) +
-   ​D de (334+ 
-   ​ +int main() { 
-   În funcție de ordinea efectuării înmulțirilor matricialenumărul total de înmulțiri scalare poate să fie foarte diferit: +    /dupa ce am citit n initializam domeniul cu n elemente, numerele ​de la 1 la n
-   * $((AB)C)D$ =10582 înmulțiri +    iar solutia este vida initial ​*
-   * $(AB)(CD)$ =54201 înmulțiri +    std::​vector<​int>​ domain(n)solution; 
-   * $(A(BC))D$ =>  2856 înmulțiri +    for (int i = 0; i < n; ++i{ 
-   * $A((BC)D)$ = 4055 înmulțiri +        ​domain[i] = i + 1; 
-   * ...+    } 
 + 
 +    /* apelam backtracking pe domeniul nostrucautand solutia in vectorul solution */ 
 +    back(domain, solution)
 +
 +</code> 
 + 
 +Apelarea inițială (din "​main"​se face astfel: "back(domain, solution);", unde domain reprezintă un vector cu elementele de la 1 la N, iar solution este un vector gol. 
 + 
 +<note important
 +Nu este indicată implementarea backtracking-ului astfel deoarece este foarte costisitor din punct de vedere al memoriei(se creează noi domenii și soluții la fiecare pas). 
 +</note>
  
-Rezultatul optim se obține pentru cea de a treia parantezare:​ $(A(BC))D$. 
-  
 </​spoiler>​ </​spoiler>​
  
-== TIPAR == +== Complexitate ​==
-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]** .+Soluția va avea următoarele complexitati:​
  
-== Găsire recurență == +  ​complexitate temporala ​: $T(n)=O(n * n!)
-  * **Cazul de bază** :  +     ​explicație : Complexitatea generarii permutarilor, ​$O(n!)$, se înmultește cu complexitatea copierii vectorilor soluție si domeniu si a stergerii elementelor din domeniu, $O(n)
-    * $dp[i][i] ​+  * complexitate spatiala ​: $S(n)=O(n^2)$ 
-      NU avem niciun efort dacă nu avem ce înmulți. +     ​explicație : Fiecare nivel de recursivitate are propria lui copie a soluției ​și a domeniului. Sunt n nivele ​de recursivitatedeci complexitatea spatială este $O(n= O(n^2)$
-    * $dp[i][i+1] = d_{i-1} d_{i} d_{i+1}$ +
-      * Dacă avem două matriceputem 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. +=== Backtracking (date transmise prin referinta) === 
-<spoiler Implementare ​C++>+<spoiler Implementare>​ 
 <code cpp> <code cpp>
-// INF este valoarea maximă - "​infinitul"​ nostru 
-const auto INF = std::​numeric_limits<​unsigned long long>::​max();​ 
  
-// T = O(n ^ 3)  +/* deoarece numerele sunt sterse din domeniu odata ce sunt folosite, soluția generata este garantata sa nu contina duplicate. Astfel, atunci cand domeniul ajunge vid, soluția este intotdeauna corecta *
-// S = O(n ^ 2) - stocăm n x n întregi în tabloul dp +bool check(std::vector<​int> ​solution) { 
- ​unsigned long long solve_podm(int n, const vector<​int> ​&d) { +    ​return true; 
-    ​// dp[i][j] = numărul MINIM înmulțiri scalare cu codare, poate fi calculat produsul +}
-    // ​           matriceal M_i * M_i+1 * ... * M_j +
-    vector<​vector<​unsigned long long>> ​ dp(n + 1, vector<​unsigned long long> (n + 1, INF));+
  
-    // Cazul de bază 1nu am ce înmulți ​ +void printSolution(std::​vector<​int>​ &​solution) { 
-    for (int i = 1; i <= n; ++i) { +    for (int s : solution) { 
-        ​dp[i][i] = 0ULL // 0 pe unsigned long long (voi folosi mai încolo și 1ULL)+        ​std::cout << s << " ";
     }     }
 +    std::cout << "​\n";​
 +}
  
-    // Cazul de bază 2matrice d[i - 1] x d[i] înmulțită cu matrice d[i] x d[i + 1]  +void back(std::​vector<​int>​ &​domain,​ std::​vector<​int>​ &​solution) { 
-    // (matrice pe poziții consecutive) +    /* dupa ce am folosit toate elementele din domeniu putem verifica daca 
-    ​for (int i 1; i < n; ++i) { +    am gasit o solutie *
-        ​dp[i][i + 1] =  1ULL * d[i - 1] * d[i] * d[i + 1] +    ​if (domain.size() ​== 0) { 
 +        ​if(check(solution)) { 
 +            printSolution(solution);​ 
 +        } 
 +        return;
     }     }
  
-    // Cazul general: +    /* incercam sa adaugam in solutie toate valorile din domeniu, pe rand */ 
-    // dp[i][j] = min(dp[i][k] + dp[k + 1][j] + d[i - 1] d[k] d[j]), k = i : j - 1 +    for (unsigned ​int 0domain.size(); ++i) { 
-    for (int len 2len <= n; ++len) {            // fixăm lungimea intervalului ​(2, 3, 4, ...) +        ​/* retinem valoarea pe care o scoatem ​din domeniu ca sa o readaugam dupa 
-        for (int i = 1; i + len - 1 <= n; ++i) {    // fixăm capătul ​din stânga: i +        ​apelarea recursiva a backtracking-ului */ 
-            int = i + len - 1                   // capătul din dreapta se deduce: j +        ​int tmp domain[i]
-             + 
-            // Iterăm prin indicii dintre capete, spărgând șirul de înmulțiri in două (paranteze). +        /* adaug elementul curent la potentiala solutie */ 
-            for (int k = i; k < j; ++k) { +        solution.push_back(domain[i])
-                // M_i * ... M_j  = (M_i * .. * M_k* (M_k+1 *... * M_j+        /* sterg elementul curent din domeniu ca sa il pot pasa prin referinta 
-                ​unsigned long long new_sol = dp[i][k] + dp[k + 1][j] + 1ULL d[i - 1] d[k] * d[j]+        si sa nu fie nevoie sa creez alt domeniu */ 
-                 +        domain.erase(domain.begin() + i); 
-                // actualizăm soluția dacă este mai bună + 
-                ​dp[i][j] = min(dp[i][j]new_sol);  +        /apelez recursiv backtracking pe domeniul si solutia modificate ​*
-            } +        back(domain,​ solution)
-        ​}+ 
 +        /* refac domeniul si solutia la modul in care aratau inainte de apelarea 
 +        recursiva a backtracking-ului,​ adica readaug elementul eliminat in 
 +        domeniu si il sterg din solutie *
 +        ​domain.insert(domain.begin() + i, tmp); 
 +        ​solution.pop_back();​
     }     }
 +}
  
-    ​// Rezultatul se află în dp[1][n]: Numărul MINIM de inmultiri scalare +int main() { 
-    ​// pe care trebuie să le facem pentru a obține produsul M_1 * ... M_n +    ​/* dupa ce am citit initializam domeniul cu n elemente, numerele ​de la 1 la n, 
-    ​return dp[1][n];+    ​iar solutia este vida initial ​*/ 
 +    ​std::​vector<​int>​ domain(n), solution; 
 +    for (int i = 0; i < n; ++i) { 
 +        domain[i= i + 1; 
 +    }
  
 +    /* apelam backtracking pe domeniul nostru, cautand solutia in vectorul solution */
 +    back(domain,​ solution);
 } }
 </​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>​ 
- 
  
 +Apelarea initiala (din "int main") se face astfel: "​back(domain,​ solution);",​ unde domain reprezinta un vector cu elementele de la 1 la N, iar solution este un vector gol.
 </​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>​ 
  
- 
-<​note>​ 
-**ATENȚIE!** La PA, în general, vom folosi convenția $ expresie \ \%  \ MOD $, care va fi detaliată în capitolul următor din acest laborator. ​ 
-</​note>​ 
  
 == Complexitate == == Complexitate ==
-Întrucat soluția presupune fixarea capetelor unui subinterval (i, j), apoi alegerea unui intermediar (k), complexitatea este dată de aceste 3 cicluri. 
-    * **complexitate temporală**:​ $T(n) = O(n^3)$ 
-    * **complexitate spațială**:​ $S(n) = O(n^2)$ ​ 
  
 +Soluția va avea următoarele complexități:​
  
 +  * complexitate temporală : $T(n)=O(n * n!)$
 +    * explicație : Complexitatea generării permutărilor,​ $O(n!)$, se înmulțește cu complexitatea ștergerii elementelor din domeniu, $O(n)$
 +  * complexitate spatială : $S(n)=O(n)$
 +    * explicație : Spre deosebire de solutia anterioară,​ toate nivelele de recursivitate folosesc aceeași soluție și același domeniu. Complexitatea spatială este astfel redusă la $O(n)$
 +<​note>​
  
-===== Categoria 4: NUMĂRAT ===== +Această abordare este mai eficientă decât cea generală, deoarece se evită folosirea memoriei auxiliare.
-Aceste recurențe au o oarecare asemănare: +
-  * toate numară lucruri! :p +
-  * interesante sunt cazurile când numărul căutat este foarte mare (altfel am putea apela la alte metode - ex. generarea tuturor candidaților posibili cu backtracking) +
-      * în acest caz, deoarece ​numărul poate să nu încapă pe un tip reprezentabil standard (ex. int pe 32/64 de biți), ​se cere (de obicei) restul împarțirii numărului căutat la un număr **MOD** (vom folosi în continuare această notație). +
-==== Sfaturi / Reguli ==== +
-  * când căutați o recurență pentru o problema de numărare trebuie să aveți grijă la două aspecte: +
-    * 1) să **NU** numărați același obiect de două ori. +
-    * 2) să numărați toate obiectele în cauză.  +
-  * de multe ori, o problemă de numărare implică o partiționare a **tuturor** posibilelor soluții după un anumit criteriu (relevant). Găsirea criteriului este partea esențială pentru aflarea recurenței.  +
-==== Regulile de lucru cu clase de resturi ==== +
-Reamintim câteva proprietăți matematice pe care ar trebui să le aveți în vedere atunci când implementați pentru a obține corect resturile anumitor expresii. (corect poate să însemne, de exemplu, să evitați overflow :D - lucru neintuitiv câteodată). +
-  * proprietăți de bază: +
-    * $(a + b)   \ \% \   MOD = ((a   \ \% \   MOD) + (b   \ \% \   ​MOD)) ​  \ \% \   MOD $  +
-    * $(a \ * b)   \ \% \   MOD = ((a   \ \% \   MOD) \ * (b   \ \% \   ​MOD)) ​  \ \% \   MOD $  +
-    * $(a - b)   \ \% \   MOD = ((a   \ \% \   MOD) - (b   \ \% \   MOD) + MOD)   \ \% \   MOD $ (restul nu poate fi ceva negativ; în C++ **%** nu funcționează pe numere negative)  +
-  * invers modular +
-    * $ \frac{a}{b} ​  \ \% \   MOD = ((a   \ \% \   MOD) * (b ^ {MOD-2} ​  \ \% \   ​MOD)) ​  \ \% \   ​MOD)$ +
-         ​* ​ **DACĂ** MOD este prim; **DACĂ** a și b nu sunt multipli ai lui MOD   +
-<spoiler Explicații invers modular> ​    +
-    * ** definiție **: **b** este inversul modular al lui **a** în raport cu **MOD** dacă $ a * b = 1 (modulo \ MOD)$ +
-    * ** utilizare **: $ \frac{a}{b} ​  \ \% \   MOD = ((a   \ \% \   MOD) * (invers(b) ​  \ \% \   ​MOD)) ​  \ \% \   MOD $ +
-    * ** calculare **: deoarece la PA această discuție are sens doar în contextul posibilității implementării unei recurențe DP în care folosim resturile doar pentru a evita overflow/​imposibilitatea de a reține rezultatul pe tipurile standard de tip int (adică nu ne interesează să dăm o metoda generală pentru invers modular), vom simplifica problema - **MOD este prim!!!** +
-      * ** Mica teoremă a lui Fermat**: Dacă p este un număr prim și a este un număr întreg care nu este multiplu al lui p, atunci $a^{p-1} = 1 (modulo \ p)$. +
-            * din definiția inversului modular, reiese că ** a ** și **b** nu sunt multipli ai lui **MOD** +
-            * introducând notațiile noastre în teoremă și prelucrând obținem +
-              * $a ^ {MOD - 1} = 1 (modulo \ MOD) <=> a * (a ^{MOD-2}) = 1 (modulo \ MOD)$ +
-              * deci, inversul modular al lui a (în aceste condiții specifice) este $b = a ^ {MOD -2 }$ +
  
-</​spoiler>​ 
- 
-<​note>​ 
-Reamintim că prin înmulțirea/​adunarea a două variabile de tipul int, rezultatul poate să nu încapă pe 32 biți. E posibil să trebuiască să combinăm regulile de la resturi cu următoarele:​ 
-  * C++ 
-    * **1LL / 1ULL** - constanta 1 pe 64 biti cu semn / făra semn 
-      * **1LL * a * b** - am grijă ca rezultatul să nu dea overflow și să se stocheze direct pe 64 biți (cu semn) 
-  * Java 
-    * **1L** - constanta 1 pe 64 biți cu semn (în Java nu există unsigned types) 
-      * **1L * a * b** - am grijă ca rezultatul să nu dea overflow și să se stocheze direct pe 64 biți (cu semn) 
 </​note>​ </​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ă**. 
  
-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ă**. ​+=== Backtracking (tăierea ramurilor nefolositoare) === 
 +<spoiler Implementare>​
  
-=== Cerință === +<code cpp>
-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> +bool check(std::​vector<int&​solution) ​{ 
-{{pa:​laboratoare:​garduri_123.png}}+    return true; 
 +}
  
-$= 1$ sau $n = 2$ sau $n = 3$+void printSolution(std::​vector<​int>​ &​solution) { 
 +    for (auto s : solution) { 
 +        std::cout << s << " "; 
 +    } 
 +    std::cout << "\n"; 
 +}
  
 +void back(int step, int stop, std::​vector<​int>​ &​domain,​
 +        std::​vector<​int>​ &​solution,​ std::​unordered_set<​int>​ &​visited) {
 +    ​
 +    /* vom verifica o solutie atunci cand am adaugat deja N elemente in solutie,
 +    adica step == stop */
 +    if (step == stop) {
 +        /* deoarece am avut grija sa nu se adauge duplicate, "​check()"​ va returna
 +        intotdeauna "​true"​ */
 +        if(check(solution)) {
 +            printSolution(solution);​
 +        }
 +        return;
 +    }
  
-Răspuns: ​**1**  (un singur gard)+    /Adaugam in solutie fiecare element din domeniu care *NUa fost vizitat 
 +    deja renuntand astfel la nevoia de a verifica duplicatele la final prin 
 +    functia "​check()"​ */ 
 +    for (unsigned int i = 0; i < domain.size();​ ++i) { 
 +        /* folosim elementul doar daca nu e vizitat inca */ 
 +        if (visited.find(domain[i]) == visited.end()) { 
 +            /* il marcam ca vizitat si taiem eventuale expansiuni nefolositoare 
 +            viitoare (ex: daca il adaug in solutie pe 3 nu voi mai avea 
 +            niciodata nevoie sa il mai adaug pe 3 in continuare) ​*
 +            visited.insert(domain[i]);
  
-Explicație:​ Se poate forma un singur gard în fiecare caz, după cum este ilustrat și în figura ​**Garduri_123**. ​ +            /adaugam elementul curent in solutie pe pozitia pasului curent 
-</spoiler>+            ​(step) */ 
 +            solution[step] = domain[i];
  
 +            /* apelam recursiv backtracking pentru pasul urmator */
 +            back(step + 1, stop, domain, solution, visited);
  
-<spoiler Exemplu 4> +            /* stergem vizitarea elementului curent (ex: pentru N = 3, dupa ce 
-{{pa:​laboratoare:​garduri_4.png}}+            la pasul "step = 0" l-am pus pe 1 pe prima pozitie in solutie si 
 +            am continuat recursiv pana am ajuns la solutiile ​{1, 2, 3} si  
 +            ​{1, 3, 2}, ne dorim sa il punem pe 2 pe prima pozitie in solutie si 
 +            sa continuam recursiv pentru a ajunge la solutiile {2, 1, 3} etc.) */ 
 +            visited.erase(domain[i]);​ 
 +        ​} 
 +    } 
 +}
  
-$n = 4$+int main() { 
 +    /* dupa ce am citit initializam domeniul cu n elemente, numerele de la 1 la n, 
 +    iar solutia este initializata cu un vector de n elemente (deoarece o permutare 
 +    contine n elemente) */ 
 +    std::​vector<​int>​ domain(n), solution(n);​ 
 +    std::​unordered_set<​int>​ visited; 
 +    for (int i = 0; i < n; ++i) { 
 +        domain[i] ​i + 1; 
 +    }
  
-Răspuns: ​**2**  ​+    /apelam back cu step = 0 (atatea elemente avem adaugate in solutie), 
 +    stop = n (stim ca vrem sa adaugam n elemente in solutie pentru ca o 
 +    permutare e alcatuita din n elemente), domain este vectorul de valori 
 +    posibile, solution este vectorul care simuleaza stiva pe care o vom 
 +    umple, visited este un unordered_set (initial gol) in care retinem daca 
 +    un element din domeniu se afla deja in solutia curenta la un anumit pas *
 +    back(0, n, domain, solution, visited); 
 +
 +</​code>​ 
 + 
 +Apelarea inițială (din "​main"​) se face astfel: "​back(0,​ n, domain, solution, visited);",​ unde domain reprezintă un vector cu elementele de la 1 la N, iar solution este un vector de n elemente, 0 este pasul curent, n este pasul la care dorim să ne oprim, iar visited este map-ul care ne permite să ținem cont de ce elemente au fost vizitate sau nu.
  
-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>​
  
 +== Complexitate ==
  
-<spoiler Exemplu 5> +Soluția va avea următoarele complexitați:
-{{pa:laboratoare:​garduri_5.png}}+
  
-$n = 5$+  * complexitate temporală : $T(n)=O(n * n!)$ 
 +    * explicație : Complexitatea generării permutărilor,​ O(n!), se înmulțește cu complexitatea iterării prin domeniu, $O(n)$ 
 +  * complexitate spatială : $S(n)=O(n)$ 
 +    * explicație : Toate nivelele de recursivitate folosesc aceeași soluție și același domeniu.
  
-Răspuns: **3**  ​+<​note>​
  
-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**. +Această soluție este optimă și are complexitatea temporală $T(n) = O(n!)$Nu putem să obținem o soluție mai bună, întrucât trebuie ​să generăm n! permutări.
-  * 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>​+
  
 +De asemenea, este optimă și din punct de vedere spatial, întrucât trebuie să avem $S(n) = O(n)$, din cauza stocării permutării generate.
 +</​note>​
  
-<spoiler Exemplu 6> 
-{{pa:​laboratoare:​garduri_6.png}} 
  
-$n 6$+==== Combinări ====
  
-Răspuns: **4**  ​+=== Enunț ===
  
-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**. +Se dau numerele N si KSă se genereze toate combinările mulțimii formate din toate numerele ​de la 1 la Nluate câte K.
-  * 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ță ​=== +=== Exemple ​===
-== 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ț)+
  
 +<spoiler Exemplu 1>
  
-Răspunsul la problemă este $dp[n]$.+N = 4, K = 2 => M = {1, 2, 3, 4}
  
-== Găsire recurență == +Soluție: 
-  * **Caz de bază** +  * {12} 
-    * $dp[1] = dp[2] = dp[3] = 1$; $dp[4]$ = +  * {13} 
-  * ** Caz general ** +  {1, 4} 
-    * 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 +  {23} 
-      * **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$** ​ +  {2, 4} 
-        numărul de moduri în care putem face acest subgard este $dp[i-1]$ +  * {34}
-      **DACĂ** alegem ca ultima piesă să fie în poziție orizontală (de faptpunem 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-04?&#​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ă.+
  
 +</​spoiler>​
  
-De asemenea, tot în secțiunea [[http://​ocw.cs.pub.ro/​courses/​pa/​laboratoare/​laborator-04?&#​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> ​+=== Soluții ===
  
-== Implementare recurență == +===Backtracking (tăierea ramurilor nefolositoare) === 
-Aici puteți vedea un exemplu simplu de implementare în C++.  +<spoiler Implementare>​
- +
-<spoiler Implementare ​în C++>+
 <code cpp> <code cpp>
  
-#define MOD 1009 +bool check(std::​vector<​int> &​solution) { 
-int gardurile_lui_Gigel(int n) { +    return ​true
-    ​// cazurile de bază +}
-    if (n <= 3) return ​1+
-    if (n == 4) return 2;+
  
-    ​vector<​int> ​dp(1); // păstrez indexarea de la 1 ca în explicații+void printSolution(std::​vector<​int> ​&​solution,​ std::​vector<​int>​ &​domain,​ int stop) { 
 +    for (unsigned i = 0; i < stop; ++i
 +        std::cout << domain[solution[i]] << " "; 
 +    } 
 +    std::cout << "​\n"​; 
 +}
  
-    /cazurile de bază +void back(int step, int stop, std::​vector<​int>​ &​domain,​ 
-    ​dp[1] dp[2] dp[3] = 1; +        std::​vector<​int>​ &​solution) { 
-    ​dp[4] 2; +    ​/* vom verifica o solutie atunci cand am adaugat deja K elemente in solutie, 
- +    ​adica step == stop */ 
-    ​// cazul general +    ​if (step == stop) { 
-    ​for ​(int i = 5; i <= n; ++i) { +        /* deoarece am avut grija sa se adauge elementele doar in ordine 
-        dp[i] = (dp[i - 1] + dp[i - 4]% MOD;+        crescatoare,​ "​check()"​ va returna intotdeauna "​true"​ *
 +        if(check(solution)) { 
 +            ​printSolution(solution, domain, stop)
 +        } 
 +        return;
     }     }
  
-    ​return dp[n];+    ​/* daca este primul pas, alegem fiecare element din domeniu ca potential 
 +    candidat pentru prima pozitie in solutie; altfel, pentru a elimina ramurile 
 +    in care de exemplu {2, 1} se va genera dupa ce s-a generat {1, 2} (adica 
 +    ar fi duplicat), vom folosi doar elementele din domeniu care sunt mai mari 
 +    decat ultimul element adaugat in solutie (solution[step - 1]) */ 
 +    unsigned i = step > 0 ? solution[step - 1] + 1 : 0; 
 +    for (; i < domain.size();​ ++i) { 
 +        solution[step] = i; 
 +        back(step + 1, stop, domain, solution); 
 +    }
 } }
  
 +int main() {
 +    /* dupa ce citim n si k initializam domeniul cu valorile de la 1 la n,
 +    iar solutia este initializata cu un vector de k elemente (fiindca o
 +    combinare de "n luate cate k" are k elemente) */
 +    std::​vector<​int>​ domain(n), solution(k);​
 +    for (int i = 0; i < n; ++i) {
 +        domain[i] = i + 1;
 +    }
 +
 +    back(0, k, domain, solution);
 +}
 </​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$, deoarecepe valorile anterior calculate în dp, a fost deja aplicată operația $%$.+În această soluție ne bazăm pe faptul ​că toate combinările pot fi generate ​în 
 +ordine crescătoareadică soluția {1, 3, 4} e echivalentă cu {4, 1, 3}.
  
-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ă. +<note>
-</spoiler>+
  
 +Această soluție este optimă întrucât toate soluțiile generate sunt corecte (de aceea funcția check întoarce true). Deoarece problema cere obținerea tuturor combinărilor,​ aceasta complexitate nu poate fi mai mică de Combinări(n,​ k).
  
-== Complexitate == +</​note>​
-  * **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 ===== +</​spoiler>​
-De multe ori, este nevoie să folosim câteva tehnici pentru a obține performanța maximă cu recurența găsită.+
  
-În prima parte a laboratorului 3 se menționa tehnica de memoizare. În acesta, ne vom rezuma la cum putem folosi cunoștințele de lucru matriceal pentru a favoriza implementarea unor anumite tipuri de recurențe.+=== Complexitate ===
  
-==== Exponențiere pe matrice pentru recurențe liniare ==== +Soluția va avea următoarele complexități:
-=== 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)+
  
-O astfel de recurență ar însemna că, pentru a calcula ​**costul problemei i**, îmbinăm costurile problemelor ​$i - 1, i - 2, ...., i - k$, fiecare contribuind cu un anumit coeficient ​$c_{1}, c_{2}..., c_{k}$.+  * complexitate temporală : $T(n)=O(Combinari(nk))$ 
 +  ​complexitate spatială $S(n)=O(n+k)=O(n)$ 
 +    * explicație : $k <= n$, deci $O(n+k)=O(n)$
  
-<spoiler Complexitate recurențe liniară>​  +==== Problema șoricelului ====
-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   +=== Enunț ===
-</​spoiler>​+
  
-=== Exponențiere pe matrice ​=== +Se dă un număr N și o matrice ​pătratică de dimensiuni N x N în care elementele 
-Facem următoarele notații+egale cu 1 reprezintă ziduri (locuri prin care nu se poate trece), iar cele egale 
-  * $S_i$ = starea la pasul i +cu 0 reprezintă spații goale. Această matrice are un șoricel în celula ​(00) ș
-    * $S_i = (dp[i - k + 1]dp[- k + 2], ..., dp[i - 1], dp[i])$ +o bucată de brânză în celula ​(N - 1, - 1). Scopul șoricelului e să ajungă la 
-  * $S_k$ = starea inițială (în care cunoaște cele k cazuri ​de bază+bucata ​de brânză. Afișați toate modurile ​în care poate face asta știind că 
-    * $S_k = (dp[1]dp[2], ..., dp[k-1], dp[k])$ +acesta poate merge doar în dreapta sau în jos cu câte o celulă la fiecare pas.
-  * $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ă+
  
 +=== Exemple ===
  
 +<spoiler Exemplu 1>
  
-== Algoritm naiv == +  ​2 
-Putem formula problema astfel: +  * 
-  ​$S_k$ = este starea inițială +  ​* 0 0
-  * 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} +Există ​drum posibil:
- ​\begin{bmatrix} dp[i - k + 1] &  ... & dp[i-1] &  dp[i] \\ \end{bmatrix} =+
  
 +  * (0, 0)->(1, 0)->(1, 1)
  
-\begin{bmatrix} dp[i - k] &  ... & dp[i-2] &  dp[i-1] \\ \end{bmatrix} ​ +</​spoiler>​ 
-\begin{bmatrix} + 
-   0 & 0 &... & 0 & 0 & c_{k}\\ +<spoiler Exemplu ​2> 
-   1 & &... & & c_{k-1}\\ + 
-   ​&... & & 0 & c_{k-2}\\ +  * 3 
-   ... & ... & ... & ... & ...\\ +  ​* ​0 0 0 
-   &... & 1 & & c_{2}\\ +  ​* ​0 1 0 
-   0 & 0 &... & 0 & 1 & c_{1}\\ +  ​* ​0 0 0
-   ​\end{bmatrix} +
-\end{gather}+
  
 +Există 2 drumuri posibile:
  
 +  * (0,​0)->​(0,​1)->​(0,​2)->​(1,​2)->​(2,​2)
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(2,​1)->​(2,​2)
  
-<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>​ </​spoiler>​
  
-== Exponențiere logaritmică pe matrice == +<spoiler Exemplu 3>
-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$. +  * 4 
-$$S_i = S_{i-1}C$$ +  * 0 0 0 
-$$S_i = S_{i-2}C^2$$ +  * 0 1 1 0 
-$$S_i = S_{i-3}C^3$$ +  * 0 0 0 0 
-$$...$$ +  * 0 0 0 0
-$$S_i = S_{k}C^{i -k}$$+
  
 +Există 4 drumuri posibile:
  
-În laboratorul ​2 (Divide et Imperaam învățat că putem calcula $x ^ n$ în timp logaritmic. Deoarece și înmulțirea matricilor este asociativăputem calcula $C ^ n$ in timp logaritmic.+  * (0,​0)->​(1,​0)->​(2,0)->(2,1)->(2,2)->​(2,​3)->​(3,​3) 
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(2,​1)->​(2,​2)->​(3,​2)->​(3,​3) 
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(2,​1)->​(3,​1)->​(3,​2)->​(3,​3) 
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(3,​0)->​(3,​1)->​(3,​2)->​(3,​3)
  
-Obținem astfel o soluție cu următoarele complexități:​ +</​spoiler>​
-  * ** complexitate temporală **: $T = O(KMAX^3 * log(n))$ +
-    * explicație  +
-      * 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^3)$  +
-    * explicație  +
-      * este nevoie să stocăm câteva matrice +
-**Observație!** În ultimele calcule nu am șters constanta KMAX, întrucât apare la puterea a 3-a! $KMAX = 100$ implică $KMAX^3 = 10^6$, valoare care nu mai poate fi ignorată în practică ($KMAX^3$ poate fi comparabil cu n).+
  
-=== Gardurile lui Gigel (optimizare) === +=== Soluții ===
-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$+
  
-== Exponențiere rapidă == +=== Backtracking (transmitere prin referință) === 
-  * $ k = 4 $ +<spoiler Implementare>​
-  * $S_4 = (dp[1], dp[2], dp[3], dp[4]) = (1, 1, 1, 2)$ +
-  * $S_i (dp[i-3], dp[i-2], dp[i-1], dp[i])$ +
-  * Răspunsul se află efectuând operația $S_n S_4 * C^{n - 4}$, unde C are următorul conținut: ​  +
-\begin{gather}  +
-   C = \begin{bmatrix} +
-   0 & 0 & 0 & 1\\ +
-   1 & 0 & 0 & 0\\ +
-   0 & 1 & 0 & 0\\ +
-   0 & 0 & 1 & 1\\ +
-   ​\end{bmatrix} +
-\end{gather}+
  
-<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> <code cpp>
  
-#define MOD  1009 +bool check(std::​vector<​std::​pair<​int,​ int> > &​solution,​ int walls[100][100]) { 
-#define KMAX 4+    for (unsigned i = 0; i < solution.size() - 1; ++i) { 
 +        /* line_prev si col_prev reprezinta celula in care se afla soricelul la 
 +        pasul i; line_next si col_next reprezinta celula in care se afla 
 +        la pasul i + 1; trebuie sa fim siguri ca soricelul nu a ajuns pe zid 
 +        si ca urmatoarea celula este sub sau in dreapta celulei curente */ 
 +        int line_prev = solution[i].first;​ 
 +        int line_next = solution[i + 1].first; 
 +        int col_prev = solution[i].second;​ 
 +        int col_next = solution[i + 1].second;
  
-// C = A +        ​/* walls[x][y== 1 inseamna ca este zid pe linia xcoloana y */ 
-void multiply_matrix(int A[KMAX][KMAX], int B[KMAX][KMAX], int C[KMAX][KMAX]) { +        if (walls[line_prev][col_prev== 1 || 
-    int tmp[KMAX][KMAX];+                !((line_next == line_prev + 1 && col_next == col_prev) || 
 +                (line_next == line_prev && col_next == col_prev + 1))) { 
 +            ​return false; 
 +        } 
 +    }
  
-    ​// tmp = A * B +    ​return true
-    for (int i = 0i < 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) { +void printSolution(std::​vector<​std::​pair<​int, int> > &​solution) { 
-                sum += 1LL * A[i][k] * B[k][j]+    for (std::pair<int, int> s : solution) { 
-            }+        ​std::​cout << "​("​ << s.first << ","​ << s.second << "​)->";​ 
 +    } 
 +    std::cout << "​\n"​
 +}
  
-            tmp[i][j] = sum % MOD;+void back(std::​vector<​std::​pair<​int,​ int> > &​domain,​ int walls[100][100]
 +        std::​vector<​std::​pair<​int,​ int> > &​solution,​ int max_iter) { 
 +    /* daca am facut "​max_iter"​ pasi ma opresc si verific daca este corecta 
 +    solutia */ 
 +    if (solution.size() ​== max_iter) { 
 +        if(check(solution,​ walls)) { 
 +            printSolution(solution);
         }         }
 +        return;
     }     }
  
-    //  C = tmp +    /* avand domeniul initializat cu toate celulele din matrice, incercam sa 
-    ​memcpy(C, tmp, sizeof(tmp));+    adaugam oricare dintre aceste celule la solutie, verificand la final daca 
 +    solutia este buna *
 +    ​for (unsigned int i = 0; i < domain.size();​ ++i) { 
 +        /* pastram elementul curent pentru a-l readauga in domeniu dupa 
 +        apelarea recursiva */ 
 +        std::​pair<​intint> ​tmp = domain[i];​ 
 + 
 +        /* adaugam elementul curent la solutia candidat */ 
 +        solution.push_back(domain[i]);​ 
 +        /* stergem elementul curent din domeniu */ 
 +        domain.erase(domain.begin() + i); 
 + 
 +        /* apelam recursiv backtracking */ 
 +        back(domainwalls, solution, max_iter);​ 
 + 
 +        /* adaugam elementul sters din domeniu inapoi */ 
 +        domain.insert(domain.begin() + i, tmp)
 +        /* stergem elementul curent din solutia candidat pentru a o forma pe 
 +        urmatoarea */ 
 +        solution.pop_back(); 
 +    }
 } }
  
-// R = C^p +int main() { 
-void power_matrix(int C[KMAX][KMAX]int pint R[KMAX][KMAX]{ +    ​/* initializam domeniul si solutia ca vectori de perechi de int-uri; 
-    // tmp = I (matricea identitate) +    ​domeniul va contine initial toate perechile de indici posibile din 
-    int tmp[KMAX][KMAX]+    matrice ​((00)(0, 1... (n - 1, n - 1)), iar solutia va fi initial 
-    for (int i = 0; i < KMAX; ++i) { +    ​vida *
-        for (int j = 0; j < KMAX; ++j) { +    ​std::​vector<​std::​pair<​int, int> > domain, solution; 
-            ​tmp[i][j] = (i == j) ? 1 : 0;+ 
 +    fin >> n
 +    for (i = 0; i < n; ++i) { 
 +        for (j = 0; j < n; ++j) { 
 +            ​/* walls[i][j] == 1 daca pe pozitia ​(ij) este zid; altfel */ 
 +            fin >> walls[i][j];​ 
 +            domain.push_back({i,​ j});
         }         }
     }     }
  
-    ​while (p != 1) { +    ​/* apelam back cu domeniul format initial, cu matricea de ziduri, cu 
-        ​if  ​(p % == 0{ +    solutia vida si cu numarul maxim de iteratii ​2 * n - pentru ca 
-            ​multiply_matrix(CCC);     ​// C C*C +    mergand doar in dreapta si in jos, in 2 * n - 1 pasi va ajunge din 
-            p /= 2;                       // rămâne de calculat C^(p/2) +    (0, 0in (n - 1, n - 1) */ 
-        else +    back(domain, walls, solution, ​* n - 1); 
-            // reduc la cazul anterior+
-            ​multiply_matrix(tmp, Ctmp); // tmp tmp*C +</​code>​ 
-            ​--p; ​                         /rămâne de calculat C^(p-1)+ 
 +Apelarea initiala (din "int main") se face astfel: "back(domainwallssolution, 2 * n - 1);", unde domain reprezinta un vector cu perechi in care sunt toate celulele matricii, solution este un vector gol, walls este matricea care ne arata daca este zid sau nu pe o anumita pozitie, iar 2 * n - 1 e numarul e pasi in care ar trebui ca soricelul sa ajunga la branza pe oriunde ar merge. 
 + 
 +</spoiler>​ 
 +=== Complexitate === 
 + 
 +Soluția va avea următoarele complexități:​ 
 + 
 +  ​complexitate temporală : $T(n)=O(Aranjamente(n^2,​ 2n - 1))$ 
 +    * explicație:​ Initial in domeniu avem $n^2$ valori. Noi dorim sa generam toate submultimile ordonate de cate $2n-1$ elemente. Acestea sunt tocmai aranjamentele de $n^2$ luate cate $2n-1$. 
 +  * complexitate spatială : $S(n)=O(n^2)$ 
 +    * explicație:​ Trebuie să stocăm informație despre drum, care are $2n-1$ celulestocăm domeniul care are $n^2$ elemente 
 + 
 +=== Backtracking (tăierea ramurilor nefolositoare=== 
 +<spoiler Implementare>​ 
 + 
 +<code cpp> 
 + 
 +bool check(std::​vector<​std::​pair<​int,​ int> > &​solution) { 
 +    return true; 
 +} 
 + 
 +void printSolution(std::​vector<​std::​pair<​int,​ int> > &​solution) ​
 +    for (std::​pair<​int,​ int> s : solution) { 
 +        ​std::​cout << "(" << s.first << "," << s.second << ")->"; 
 +    } 
 +    std::cout << "​\n";​ 
 +
 + 
 +void back(int step, int stop, int walls[100][100],​ 
 +        std::​vector<​std::​pair<​int,​ int> > &​solution,​ int line_moves[2],​ 
 +        int col_moves[2]) { 
 +    ​/* ne oprim dupa ce am ajuns la pasul "​stop"​ si verificam daca solutia este 
 +    corecta */ 
 +    if (step == stop) { 
 +        /deoarece am eliminat ramurile nefolositoare am ajuns la o solutie care 
 +        sigur este corecta */ 
 +        if(check(solution)) { 
 +            printSolution(solution);​
         }         }
 +        return;
     }     }
  
-    // avem o parte din rezultat în C și o parte în tmp +    /* daca este primul pas stiu ca soricelul este in pozitia (0, 0) *
-    ​multiply_matrix(C, tmp, R);           // rezultat = tmp C +    ​if (step == 0
-}+        ​/* adaugam (0, 0) la solutia candidat */ 
 +        ​solution.push_back({0,​ 0});
  
-int garduri_rapide(int n) { +        ​/* apelam backtracking recursiv la pasul urmator *
-    ​// cazurile de bază +        back(step + 1, stop, walls, solution, line_moves, col_moves);
-    ​if ​(n <= 3) return ​1+
-    if (n == 4return 2;+
  
-    ​// construiesc matricea C +        ​/* scoatem (0, 0) din solutie */ 
-    int C[KMAX][KMAX] = { {0, 0, 0, 1}, +        ​solution.pop_back();​ 
-                          {1, 0, 0, 0}, +        ​return;​ 
-                          {0, 1, 0, 0}, +    }
-                          {0, 0, 1, 1}}; +
-   // vreau să aplic formula S_n = S_4 * C^(n-4)+
  
-   // ​C^(n-4+    /* sunt doar doua mutari pe care le pot face intr-un pas: dreapta si jos; 
-   power_matrix(C, - 4, C);+    acestea sunt encodate prin vectorii de directii line_moves[2] = {0, 1} si 
 +    col_moves[2] = {1, 0} care reprezinta la indicele 0 miscarea in dreapta, iar 
 +    la indicele 1 miscarea in jos */ 
 +    for (unsigned int i = 0; i < 2; ++i) { 
 +        ​/* cream noua linie si noua coloana cu ajutorul vectorilor de directii */ 
 +        int new_line ​solution.back().first + line_moves[i];​ 
 +        int new_col = solution.back().second + col_moves[i];​ 
 +        int = (stop + 1/ 2;
  
-   // sol = S_4 C = dp[n] (se află pe ultima poziție ​din S_n, +        ​/* daca linia si coloana sunt valide ​(nu ies din matricesi nu este 
-   // deci voi folosi ultima coloană din C+        zid pe pozitia lor, putem continua pe acea celula ​*
-   int sol = 1 C[0][3+ 1 * C[1][3] ​1 * C[2][3] ​C[3][3]+        if (new_line < n && new_col < n && walls[new_line][new_col== 0) { 
-   return sol % MOD;+            /adaugam noua celula in solutia candidat; 
 +            NOTE: {new_line, new_col} este echivalent cu 
 +            std::​pair<​int,​ int>​(new_line,​ new_col) si se numeste "​initializer 
 +            list", feature in C++11 *
 +            solution.push_back({new_line,​ new_col})
 + 
 +            /* apelam backtracking recursiv la pasul urmator */ 
 +            back(step + 1, stop, walls, solution, line_moves, col_moves); 
 + 
 +            /* scoatem celula adaugata din solutie */ 
 +            solution.pop_back();​ 
 +        } 
 +    }
 } }
  
-</code>+int main() { 
 +    ​/* initializam solutia ca vector de perechi de int-uri */ 
 +    std::​vector<​std::​pair<​int,​ int> ​solution;
  
-<note+    fin >> n; 
-Remarcaț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 șde ieșire. Dacă am pune rezultatele direct in C, atunci am strica inputul înainte să obținem rezultatul.+    for (= 0; i < n; ++i) { 
 +        for (j = 0; j < n; ++j
 +            /* citim matricea zidurilor; 1 pentru zid0 altfel */ 
 +            fin >> walls[i][j]; 
 +        } 
 +    }
  
-Putem spune că acea funcție este **matrix_multiply_safe**, în sensul că pentru orice A,B,care respectă dimensiunile impusefuncția ​va calcula corect produsul.+    /apelam back cu step = 0, stop = 2 n - 1 deoarece in 2 n - 1 
 +    pasi soricelul va ajunge la branzavectorul de zidurivectorul in 
 +    care vom stoca solutia, vectorii de directii line_moves[2] = {0, 1} si 
 +    col_moves[2] = {10}; nu avem nevoie de domeniu deoarece folosind 
 +    vectorii de directii vom sti din ultima pozitie pusa in solutie cele 
 +    doua solutii in care putem mergeastfel domeniul nostru ​va fi alcatuit 
 +    din doua solutii la fiecare pas (daca ultima pozitie din solutie a fost 
 +    (5, 7) => domeniul pasului curent = {(5 + 0, 7 + 1) si (5 + 1, 7 + 0)} 
 +    care este egal cu {(5, 8), (6, 7)}. */ 
 +    back(0, 2 * n - 1, walls, solution, line_moves, col_moves);​ 
 +
 + 
 +</​code>​ 
 + 
 +Pentru aceasta abordare la fiecare pas de backtracking vom merge doar in doua directii, in loc sa mergem in oricare celula din matrice, lucru care imbunatateste semnificativ complexitatea temporala.
  
-</​note>​ 
 </​spoiler>​ </​spoiler>​
  
-<spoiler Comparație solutii (studiu de caz pentru curioși)>​ +=== Complexitate ​===
-Pr 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 sistem uzual (laptop) s-au obținut următoarele rezulate: +
-<code bash> +
-test case: varianta simplă +
-100000000 sol 119; time 0.984545 s +
-test case: varianta rapidă +
-100000000 sol 119; time 0.000021 s+
  
 +Soluția va avea urmatoarele complexitati:​
  
-test case: varianta simplă +  * complexitate temporală : $T(n)=O(2^{2n})$ 
-n = 1000000000 sol = 812; time = 9.662377 s +      explicație: avem de urmat un șir de $2n-1$ mutari, iar la fiecare pas avem variante posibile 
-test casevarianta rapidă +  * complexitate spatiala ​: $S(n)=O(n)$ 
-n = 1000000000 sol = 812; time = 0.000022 s +      * explicație:​ stocam maximum $2n-1$ căsuțe
-</​code>​  +
-     se observă clar diferența între cele 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>​ 
-  
 ===== Exerciții ===== ===== Exerciții =====
 <​note>​ <​note>​
-Scheletul de laborator se găsește pe pagina [[https://​github.com/​acs-pa/​pa-lab/​tree/​main/​skel/​lab04|pa-lab::​skel/​lab04]].+Scheletul de laborator se găsește pe pagina [[https://​github.com/​acs-pa/​pa-lab/​tree/​main/​skel/​lab05|pa-lab::​skel/​lab05]].
 </​note>​ </​note>​
-=== DP or math? === +==== Aranjamente ==== 
-Fie un șir de **numere naturale strict pozitive**. ​Câte **subșiruri** (submulțimi nevide) au suma numerelor ​**pară**?+Fie și K două **numere naturale strict pozitive**. ​Se cere afișarea tuturor aranjamentelor de N elemente luate cate K din mulțimea {1, 2, ..., N}. 
 + 
 +<spoiler Exemplu 1> 
 + 
 +Fie N = 3, K = 2 => M = {1, 2, 3} 
 + 
 +Soluție: 
 +  ​{1, 2} 
 +  ​{1, 3} 
 +  ​{2, 1} 
 +  ​{2, 3} 
 +  ​{3, 1} 
 +  ​{3, 2} 
 + 
 +</​spoiler>​
  
 <​note>​ <​note>​
-**subșir** (**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$.+Se dorește o complexitate $T(n, k) = A(n,k)$.
 </​note>​ </​note>​
  
 +<spoiler Hint>
 +Folosiți-vă de problema **Permutări**.
 +</​spoiler>​
  
-Task-uri: 
-  * Se cere o **soluție folosind DP**. 
-  * Inspectând recurența gasită la punctul precedent, încercați să o înlocuiți cu o **formulă matematică**. 
-  * Care este **complexitatea** pentru fiecare soluție (timp + spațiu)? Care este mai bună? De ce? :D 
  
-Deoarece rezultatul poate fi prea mare, se cere **restul împărțirii** lui la $1000000007$ ($10^9 + 7$).+<​note>​ 
 +Soluțiile se vor genera în ordine lexico-grafica!
  
-Pentru punctaj maxim pentru această problemă, este necesar ​să rezolvați toate subpunctele (ex. nu folosiți direct formula, găsiți mai întâi recurența DP). Trebuie să implementați **cel puțin** soluția cu DP.+Checkerul așteaptă să le stocați în această ordine. 
 +</​note>​
  
 +==== Submulțimi ====
 +Fie N un **număr natural strict pozitiv**. Se cere afișarea tuturor submulțimilor mulțimii {1, 2, ..., N}.
  
 <spoiler Exemplu 1> <spoiler Exemplu 1>
-$n = 3$ 
-|i|1|2|3| 
-|v|2|6|4| 
  
-Răspuns$7$+Fie N = 4 => M = {1, 2, 3, 4} 
 + 
 +Soluție: 
 +{} - mulțimea vidă 
 +{1} 
 +{1, 2} 
 +{1, 2, 3} 
 +{1, 2, 3, 4} 
 +{1, 2, 4} 
 +{1, 3} 
 +{1, 3, 4} 
 +{1, 4} 
 +{2} 
 +{2, 3} 
 +{2, 3, 4} 
 +{2, 4} 
 +{3} 
 +{3, 4} 
 +{4}
  
-Explicație:​ Toate subșirurile posibile sunt 
-  * $[2]$ 
-  * $[2, 6]$ 
-  * $[2, 6, 4]$ 
-  * $[2, 4]$ 
-  * $[6]$ 
-  * $[6, 4]$ 
-  * $[4]$ 
-Toate subșirurile de mai sus au suma pară. 
 </​spoiler>​ </​spoiler>​
  
  
-<spoiler Exemplu 2+<note
-$n = 3+Se dorește o complexitate ​$T(nO(2^n)$. 
-|i|1|2|3| +</​note>​
-|v|2|1|3|+
  
-Răspuns: $3$ +<spoiler Hint>
- +
-Explicație:​ Toate subșirurile posibile sunt +
-  * $[2]$ +
-  * $[2, 1]$ +
-  * $[2, 1, 3]$ +
-  * $[2, 3]$ +
-  * $[1]$ +
-  * $[1, 3]$ +
-  * $[3]$+
  
-Subșirurile cu sumă pară sunt: $[2]$, $[2, 1, 3]$, $[1, 3]$.+Folosiți-vă de problema **Combinari**.
  
 </​spoiler>​ </​spoiler>​
  
 +<​note>​
 +Soluțiile se vor genera în ordine lexico-grafica!
  
-<spoiler Exemplu 3> +Checkerul așteaptă să le stocați în această ordine. 
-$n = 3$ +</note>
-|i|1|2|3| +
-|v|3|2|1|+
  
-Răspuns: $3$+==== Problema damelor ====
  
-Explicație: Toate subșirurile posibile sunt +Problema damelor (sau problema reginelor) tratează plasarea a 8 regine de sah pe o tablă de șah de dimensiuni 8 x 8 astfel încat să nu existe două regine care se amenință reciproc. Astfel, se caută **o soluție** ​astfel încât nicio pereche de doua regine să nu fie pe același rândpe aceeași coloanăsau pe aceeași diagonală. Problema cu opt regine este doar un caz particular pentru problema generalăcare presupune plasarea a N regine pe o tablă de șah N x N în aceleasi condiții. Pentru această problemăexistă soluții pentru toate numerele naturale N cu excepția lui N = si N = 3.
-  ​$[3]$ +
-  ​$[32]$ +
-  * $[321]$ +
-  * $[31]$ +
-  * $[2]$ +
-  * $[2, 1]$ +
-  * $[1]$+
  
-Subșirurile cu sumă pară sunt: $[3, 2, 1]$, $[3, 1]$, $[2]$.+<spoiler Exemplu ​1>
  
-</​spoiler>​+Fie N = 5
  
-<​note>​ +Soluție:
-Morala: există probleme pentru care găsim o soluție cu DP, dar pentru care pot exista și alte soluții mai bune (am ignorat citirea/​afișarea).+
  
-În problemele de numărat, există o **șansă** bună să putem găsi (și) o formulă matematică,​ ce poate fi implementată într-un mod mai eficient decât o recurență DP.  +|X|-|-|-|-| 
-</​note>​+|-|-|X|-|-| 
 +|-|-|-|-|X| 
 +|-|X|-|-|-| 
 +|-|-|-|X|-|
  
-<spoiler Hint> +X reprezintă o damă, - reprezintă spațiu gol.
-Câte subșiruri au suma **impară**? +
-</​spoiler>​+
  
-<​hidden>​ 
-<spoiler Soluție>​ 
-Problema este preluată de [[https://​infoarena.ro/​problema/​azerah|aici]]. Soluția se găsește [[https://​www.infoarena.ro/​onis-2015/​solutii-runda-1#​azerah|aici]]. 
 </​spoiler>​ </​spoiler>​
-</​hidden>​ 
  
-=== Expresie booleană === 
-Se dă o expresie booleană corectă cu n termeni. Fiecare din termeni poate fi unul din stringurile **true**, **false**, **and**, **or**, **xor**. 
  
-Numărați modurile în care se pot așeza paranteze astfel încât rezultatul ​să fie **true**. Se respectă regulile de la logică (tabelele de adevăr pentru operațiile **and**, **or**, **xor**).+<spoiler Hint> 
 + 
 +E nevoie ​să facem backtracking pe matrice sau e suficient pe vector? 
 + 
 +</​spoiler>​ 
  
-Deoarece rezultatul poate fi prea mare, se cere **restul împărțirii** lui la $1000000007$ ($10^9 + 7$). 
  
 <​note>​ <​note>​
-În schelet vom codifica cu valori de tip char cele 5 stringuri:​ +Se va caută o singură soluție ​ (**oricare** soluție corectă), care va fi returnată sub forma unui vector cu $n + 1$ elemente. 
-  * **false**: '​F'​ + 
-  * **true**: '​T'​ + 
-  * **and**: '&'​ +Soluția este $sol[0], sol[1], ..., sol[n]$, unde $sol[i]$ = coloana unde vom plasa regina de pe linia i. 
-  * **or**: '​|'​ + 
-  * **xor**: '​^'​+Elementul 0 este nefolosit, dorim să păstrăm convenția cu indexare de la 1.
  
-Funcția pe care va trebui să o implementați voi va folosi variabilele **n** (numărul de termeni) și **expr** (vectorul cu termenii expresiei). 
 </​note>​ </​note>​
 +==== Generare de șiruri ====
  
 +Vi se dă o listă de caractere și o lista de frecvențe (pentru caracterul de pe
 +poziția i, frecvența de pe poziția i). Vi se cere să generați toate șirurile
 +care se pot forma cu aceste caractere și aceste frecvențe știind că nu pot fi
 +mai mult de K apariții consecutive ale aceluiași caracter.
  
 <spoiler Exemplu 1> <spoiler Exemplu 1>
-$n = 5$ și $expr = ['​T',​ '&',​ '​F',​ '​^',​ '​T'​]$ (expr = [** true and false xor true**]) 
  
-Răspuns: $2$+Fie caractere[] = {'​a',​ '​b',​ '​c'​},​ freq[] = {1, 1, 2}, K = 5
  
-Explicație: Există 2 moduri corecte de a paranteza expresia astfel încât să obținem rezultatul ​**true** (1). +Soluție: 
-  * $ T&(F^T) $ +  ​abcc 
-  * $ (T&F)^T $ +  ​acbc 
-</​spoiler>​+  ​accb 
 +  ​bacc 
 +  * bcac 
 +  * bcca 
 +  * cabc 
 +  * cacb 
 +  * cbac 
 +  * cbca 
 +  * ccab 
 +  * ccba
  
-<spoiler Hint> 
-    Complexitate temporală dorită este $O(n ^ 3)$. 
-    ​ 
-    Opțional, se pot defini funcții ajutătoare precum **is_operand**,​ **is_operator**,​ **evaluate**. 
 </​spoiler>​ </​spoiler>​
  
-<note tip> +<spoiler Exemplu 2>
-Pentru rezolvarea celor două probleme gândiți-vă la ce scrise în secțiunea [[http://​ocw.cs.pub.ro/​courses/​pa/​laboratoare/​laborator-04?&#​sfaturireguli| Sfaturi / Reguli]]. Pentru fiecare dintre cele două probleme facem o **partiționare după un anumit criteriu**.+
  
 +Fie caractere[] = {'​b',​ '​c'​},​ freq[] = {3, 2}, K = 2
  
-Pentru problema ​**DP or math?** partiționăm toate subșirurile după critieriul ​**parității sumei subșirului** (câte sunt pare/impare).\\ ​ +Solutie: 
-Pentru problema **expresie booleană** partiționăm **toate parantezările posibile după rezultatul lor** (câte dau true/​false).+  ​bbcbc 
 +  ​bbccb 
 +  ​bcbbc 
 +  ​bcbcb 
 +  ​bccbb 
 +  ​cbbcb 
 +  ​cbcbb 
 + 
 +</spoiler> 
 + 
 + 
 +<​note>​ 
 +Soluțiile se vor genera în ordine lexico-grafica!
  
 +Checkerul așteaptă să le stocați în această ordine.
 </​note>​ </​note>​
-=== Bonus === +===== Bonus =====
-Asistentul va alege una dintre problemele din secțiunea Extra.+
  
-<spoiler Hint> +==== Problema damelor (AC3) ====
-Recomandăm să **NU** fie una dintre cele 3 probleme de la Test PA 2017. Recomandăm să le incercați după ce recapitulați acasă DP1 și DP2, pentru a verifica dacă cunoștințele acumulate sunt la nivelul așteptat. +
-</​spoiler>​+
  
-=== Extra === +**Aplicați AC3 pe problema ​damelor.**
-<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>​ +Algoritmul AC-3 (Arc Consistency Algorithm) este de obicei folosit în probleme de satisfacere a constrângerilor (CSP)Acesta șterge arcele din arborele ​de stări care sigur nu se vor folosi niciodata.
-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>​+
  
 +AC-3 lucrează cu:
 +    * constrângeri
 +    * variabile
 +    * domenii de variabile
  
 +O variabilă poate lua orice valoare din domeniul său la orice pas. O constrângere este o relație sau o limitare a unor variabile.
  
-<spoiler PA Country>​ +=== Exemplu AC-3 ===
-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>​+
  
 +Considerăm A, o variabilă ce are domeniul D(A) = {0, 1, 2, 3, 4, 5, 6} și B o variabila ce are domeniul D(B) = {0, 1, 2, 3, 4}. Cunoaștem constrângerile:​ C1 = "A trebuie să fie impar" și C2 = "A + B trebuie să fie egal cu 5".
  
 +Algoritmul AC-3 va elimina în primul rând toate valorile pare ale lui A pentru a respecta C1 => D(A) = {1, 3, 5}. Apoi, va încerca să satisfacă C2, așa că va păstra în domeniul lui B toate valorile care adunate cu valori din D(A) pot da 5 => D(B) = {0, 2, 4}.
  
-  +AC-3 a redus astfel domeniile lui A si B, reducând semnificativ timpul folosit de algoritmul backtracking.
-<spoiler iepuri>​ +
-Rezolvați pe infoarena problema [[http://​infoarena.ro/​problema/​iepuri| iepuri]].+
  
-Hint: Exponențiere logaritmică pe matrice+===== Pool probleme (pentru prezentări) ======
  
-Soluție: +======= 1) Word Search =======
-  * $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ă. +**Enunt:** Se dă o matrice de dimensiuni ''​m × n''​ formată din litere ​și un cuvânt ''​word''​. Determinați dacă acest cuvânt poate fi format în matrice  
-Complexitate:​ $O(T * log(n))$.+Cuvântul se construiește unind litere din celule adiacente ​(pe orizontală sau verticală). Nu aveți voie să folosiți aceeași celulă de două ori în formarea aceluiași cuvânt.
  
-</​spoiler>​+**Date de intrare:** O matrice de caractere de dimensiuni ''​m × n''​ și un șir de caractere ''​word''​.
  
 +**Date de ieșire:** Se afișează ''​true''​ dacă cuvântul există în matrice, altfel ''​false''​.
  
-<spoiler Minimum Path Sum> +Problema se poate testa la:  ​ 
-Rezolvați pe leetcode problema [[https://​leetcode.com/​problems/​minimum-path-sum/​description/#​| Minimum Path Sum]]. +https://​leetcode.com/​problems/​word-search/
-</spoiler>+
  
 +======= 2) Combination Sum II =======
  
-<spoiler Lăcusta> +**Enunt:** Se dă un șir de numere (care poate conține duplicate) șun număr țintă ''​target''​Găsiți toate combinațiile unice de elemente din șir a căror sumă este exact ''​target''​  
-Rezolvați pe infoarena problema [[http://​infoarena.ro/​problema/​Lacusta| Lăcusta]]+Fiecare element de pe o anumită poziție din șir poate fi folosit cel mult o dată într-o combinație. Setul final de soluții nu trebuie să conțină combinații duplicate.
-</​spoiler>​+
  
 +**Date de intrare:** Un vector de numere întregi ''​candidates''​ și un număr întreg ''​target''​.
  
-<spoiler Suma4> +**Date de ieșire:** O listă de liste de numere întregi, reprezentând combinațiile unice valide.
-Rezolvați pe infoarena problema [[http://​infoarena.ro/​problema/​Suma4|Suma4]]. +
-</​spoiler>​+
  
-<spoiler Subșir> +Problema se poate testa la:  ​ 
-Rezolvați pe infoarena problema [[https://www.infoarena.ro/problema/subsir|subșir]]. +https://leetcode.com/problems/combination-sum-ii/
-</spoiler>+
  
-<spoiler 2șah> +======= 3) Gray Code =======
-Rezolvați pe infoarena problema [[https://​infoarena.ro/​problema/​2sah | 2șah]].+
  
-HintExponențiere logaritmică pe matrice+**Enunt:** Codul Gray de ordin ''​n''​ este o secvență ce conține toate cele $2^n$ șiruri binare de lungime ''​n'',​ cu proprietatea că oricare două șiruri consecutive diferă prin exact un singur bit.   
 +Cerința este să generați o astfel de secvență validă pentru un ''​n''​ dat. 
  
-O descriere detaliată se află în [[http://​olimpiada.info/​oji2015/​index.php?​cid=arhiva | arhiva OJI 2015]]. +**Date de intrare:** Un număr întreg ''​n''​ — lungimea șirurilor de biți.
-</​spoiler>​+
  
-<spoiler DP problems>​ +**Date de ieșire:** Se afișează secvența de $2^n$ numere (în format zecimal sau binar), respectând regula codului Gray. 
-Articolul ​de pe [[https://​leetcode.com/​discuss/general-discussion/458695/Dynamic-Programming-Patterns| ​leetcode]] conține listă cu diverse tipuri ​de probleme ​de programare dinamică, din toate categoriile discutate ​la PA.+ 
 +Problema se poate testa la:   
 +https://​cses.fi/​problemset/​task/​2205 
 + 
 +======= 4) Sudoku Solver ======= 
 + 
 +**Enunt:** Vi se cere să scrieți un program care rezolvă un puzzle Sudoku clasic (9 × 9) prin completarea celulelor goale. ​  
 +Pentru ca soluția să fie validă, trebuie respectate regulile clasice: fiecare cifră ​de la 1 la 9 trebuie să apară o singură dată pe fiecare rând, pe fiecare coloană și în fiecare dintre cele nouă careuri 3 × 3. 
 + 
 +**Date de intrare:** O matrice 9 × 9 de caractere reprezentând tabla de Sudoku inițială (celulele goale sunt marcate cu caracterul ''​.''​). 
 + 
 +**Date de ieșire:** Matricea 9 × 9 completată cu soluția corectă. 
 + 
 +Problema se poate testa la:   
 +https://​leetcode.com/​problems/sudoku-solver/ 
 + 
 +======= 5) Palindrome Partitioning ======= 
 + 
 +**Enunt:** Se dă un șir de caractere ''​s''​. Se cere să împărțiți șirul în fragmente, astfel încât fiecare fragment (subșir) rezultat să fie un palindrom. ​  
 +Returnați toate aceste partiționări posibile. 
 + 
 +**Date de intrare:** Un șir de caractere ''​s''​. 
 + 
 +**Date de ieșire:** O listă de liste de șiruri de caractere, unde fiecare listă interioară reprezintă o partiționare validă. 
 + 
 +Problema se poate testa la:   
 +https://leetcode.com/​problems/​palindrome-partitioning/​ 
 + 
 +======= 6) Knight'​s Tour ======= 
 + 
 +Enunt: Se cere să găsițparcurgere validă a unei table de șah de dimensiuni 8 × 8 folosind un cal, astfel încât acesta să viziteze fiecare celulă a tablei exact o singură dată. Mutările trebuie să respecte regulile clasice de șah pentru cal (în formă de "​L"​). 
 + 
 +Date de intrare: Două numere întregi ''​x''​ și ''​y''​care indică poziția inițială a calului pe tablă (coloana și rândul). 
 + 
 +Date de ieșire: O matrice 8 × 8 în care fiecare celulă conține numărul pasului (de la 1 la 64) la care a fost vizitată respectiva poziție. 
 + 
 +Problema se poate testa la: 
 + 
 +https://​cses.fi/​problemset/​task/​1689 
 + 
 +===== Extra ===== 
 + 
 +==== Immortal ==== 
 + 
 +[[https://​www.infoarena.ro/​problema/​immortal | Enunt]] 
 +<spoiler Solutie>​ 
 +[[http://​olimpiada.info/​oji2010/​index.php?​cid=arhiva | OJI 2010 - clasele 11-12]]
 </​spoiler>​ </​spoiler>​
  
-===== Referințe ===== 
  
-[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]] 
  
-[2] [[http://infoarena.ro/problema/kfib]]+<spoiler Backtracking problems>​ 
 +Articolul de pe [[https://leetcode.com/tag/backtracking/​| leetcode]] conține o listă cu diverse tipuri de probleme de programare dinamică, din toate categoriile discutate la PA (plus multe altele). 
 +</​spoiler>​
  
 +===== Referințe =====
  
 +[0] Chapter **Backtracking**,​ “Introduction to Algorithms”,​ Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein
pa/laboratoare/laborator-04.1645384673.txt.gz · Last modified: 2022/02/20 21:17 by darius.neatu
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