This is an old revision of the document!


Laborator 4: Programare Dinamică (continuare)

Obiective laborator

  • Înțelegerea noțiunilor de bază despre programarea dinamică.
  • Însușirea abilităților de implementare a algoritmilor bazați pe programarea dinamică.

Precizări inițiale

Toate exemplele de cod se găsesc pe pagina pa-lab::demo/lab04.

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.

  • 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.
  • Pentru orice problemă legată de conținutul acestei pagini, vă rugam 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).

De asemenea, DP se poate folosi și pentru probleme în care nu căutam un optim, cum ar fi problemele de numărare.

Pentru noțiunile prezentate până acum despre DP, vă rugăm să consultați pagina laboratorului 3.

Exemple clasice

Programarea Dinamică este cea mai flexibilă tehnică a programării. Cel mai ușor mod de a o înțelege presupune parcurgerea cât mai multor exemple.

Propunem câteva categorii de recurențe pe care le vom grupa astfel:

  • recurențe de tip SSM (Subsecvență de Sumă Maximă)
  • recurențe de tip RUCSAC
  • recurențe de tip PODM (Parantezare Optimă de Matrice)
  • recurențe de tip numărat
  • recurențe pe grafuri

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.

Categoria 3: PODM

Aceste recurențe au o oarecare asemănare cu problema PODM (enunț + soluție).

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.

Caracteristici:

  • Acest tip de problemă presupune că o putem formula ca pe o problemă de tip subinterval $[i, j]$.
  • Dacă dorim să găsim optimul pentru acest interval, va trebui să luăm în calcul toate combinațiile de 2 subprobleme care ar putea genera o soluție pentru problemele $[i, j]$.
  • Se consideră fiecare divizare în 2 subprobleme, dată de intermediarul k, astfel:
    • Fie $[i, k]$ și $[k + 1, j]$ cele 2 subprobleme pentru care cunoaștem soluțiile, atunci o soluție pentru $[i,j]$ se poate obține îmbinându-le pe cele două
    • pentru a gasi soluția cea mai bună:
      • vom itera prin toate valorile k posibile
      • o vom alege pe cea care maximizează soluția problemei $[i,j]$
  • Calculul se face de la intervale mici (probleme ușoare - $[i,i]$ sau $[i, i+1]$) spre probleme generale (dimensiune generală - $[i, j]$ ). În final, se ajunge și la dimensiunile inițiale ($[1, n]$).
    • Privind imaginea de ansamblu, adică modul în care se completează matricea dp, observăm că aceasta se completează diagonală cu diagonală.

Exemple clasice

PODM

Enunț

Fie un produs matriceal $M = M_1 M_2 ... M_n$. Putem pune paranteze în mai multe moduri și vom obține același rezultat (înmulț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}$.

Cerință

Se cere să se găsească o parantezare optimă de matrice (PODM), adică să se găsească o parantezare care să minimizeze numărul de înmulțiri scalare.

Exemple

Exemplu 1

Exemplu 1

$n = 3$

i0123
d2345

Răspuns: 64 (înmulțiri scalare)

Explicație: Avem 3 matrice:

  • A de dimensiuni (2, 3)
  • B de (3, 4)
  • C de (4, 5)

În funcție de ordinea efectuării înmulțirilor matriceale, numărul total de înmulțiri scalare poate să fie foarte diferit:

  • $(AB)C$ ⇒ $24 + 40 = 64$ de înmulțiri
    • explicație: $X = (AB)$ generează $2 * 3 * 4 = 24$ înmulțiri, $(XC)$ generează $2 * 4 * 5 = 40$ de înmulțiri
  • $A(BC)$ ⇒ $60 + 30 = 90$ de înmulțiri
    • explicație: $X =(BC)$ generează $3 * 4 * 5 = 60$ înmulțiri, $(AX)$ generează $2 * 3 * 5 = 30$ de înmulțiri

Rezultatul optim se obține pentru cea de a treia parantezare: $(AB)C$.

Exemplu 2

Exemplu 2

$n = 4$

i01234
d23423

Răspuns: 48 (înmulțiri scalare)

Explicație: Avem 4 matrice:

  • A de dimensiuni (2, 3)
  • B de (3, 4)
  • C de (4, 2)
  • D de (2, 3)

În funcție de ordinea efectuării înmulțirilor matriceale, numărul total de înmulțiri scalare poate să fie foarte diferit:

  • $(AB)C)D$ ⇒ $24 + 16 + 12 = 52$ înmulțiri
    • explicație: $X = (AB)$ generează $2 * 3 *4 = 24$ înmulțiri scalare, $Y = (XC)$ generează $2 * 4 * 2 = 16$ înmulțiri scalare, $Z = YD$ generează $2 * 2 *3 = 12$ înmulțiri scalare
  • $(A(BC))D$ ⇒ $24 + 12 + 12 = 48$ înmulțiri
    • explicație: $X = (BC)$ generează $3 * 4 * 2 = 24$ înmulțiri scalare, $Y = (AX)$ generează $ 2 * 3 * 2= 12$ înmulțiri scalare, $Z = YD$ generează 2 * 2 * 3$ = 12$ înmulțiri scalare
  • $(AB)(CD)$ ⇒ $ = $ inmulțiri
    • explicație: $X = (AB)$ generează $2 * 3 * 4 = 24$ înmulțiri scalare, $Y = (CD)$ generează $4 * 2 * 3 = 24$ înmulțiri scalare, $Z = XY$ generează $2 * 4 * 3 = 24$ înmulțiri scalare
  • $A((BC)D)$ ⇒ $24 + 18 + 27 = 69$ înmulțiri
    • explicație: $X = (BC)$ generează $3 * 4 * 2 = 24$ înmulțiri scalare, $Y = (XD)$ generează $3 * 2 * 3 = 18$ înmulțiri scalare, $Z = AY$ generează $3 * 3 * 3 = 27$ înmulțiri scalare
  • $A(B(CD))$ ⇒ $24 + 36 + 18 = 78$ înmulțiri
    • explicație: $X = (CD)$ generează $4 * 2 * 3 = 24$ înmulțiri scalare, $Y = (BX)$ generează $3 * 4 * 3 = 36$ înmulțiri scalare, $Z = AY$ generează $2 * 3 * 3 = 18$ înmulțiri scalare

Rezultatul optim se obține pentru cea de a treia parantezare: $((A(BC))D)$.

Exemplu 3

Exemplu 3

$n = 4$

i01234
d13589334

Răspuns: 2856 (înmulțiri scalare)

Explicație: Avem 4 matrice:

  • A de dimensiuni (13, 5)
  • B de (5, 89)
  • C de (89, 3)
  • D de (3, 34)

În funcție de ordinea efectuării înmulțirilor matriciale, numărul total de înmulțiri scalare poate să fie foarte diferit:

  • $((AB)C)D$ ⇒ 10582 înmulțiri
  • $(AB)(CD)$ ⇒ 54201 înmulțiri
  • $(A(BC))D$ ⇒ 2856 înmulțiri
  • $A((BC)D)$ ⇒ 4055 înmulțiri

Rezultatul optim se obține pentru cea de a treia parantezare: $(A(BC))D$.

TIPAR

A fost descris în detaliu mai sus (când s-a vorbit de categorie).

Numire recurență

$dp[i][j]$ = numărul minim de înmulțiri scalare cu care se poate obține produsul $M_i * M_{i+1} * ... *{M_j}$

Găsire recurență
  • Cazul de bază :
    • $dp[i][i] = 0 $
      • NU avem niciun efort dacă nu avem ce înmulți.
    • $dp[i][i+1] = d_{i-1} d_{i} d_{i+1}$
      • Dacă avem două matrice, putem doar să le înmulțim. Nu are sens să folosim paranteze.
      • Daca înmulțim 2 matrice de dimensiuni $d_{i-1} * d_{i}$ și $d_{i} * d_{i + 1}$, avem costul $d_{i-1} d_{i} d_{i+1}$
  • Cazul general: $dp[i][j] = min(dp[i][k] + dp[k+1][j] + d_{i-1} d_{k} d_{j})$, unde $k = i : j - 1$
    • dacă avem de efectuat șirul de înmulțiri $M_i ... M_j$, atunci putem pune paranteze oriunde și să facem înmulțirile astfel $(M_i ... M_k) (M_{k+1} ... M_{j})$
      • costul minim pentru $(M_i ... M_k)$ este $dp[i][k]$
      • costul minim pentru $(M_{k+1} ... M_j)$ este $dp[k + 1][j]$
      • vom avea, în final, de înmulțit 2 matrice de dimensiune $d_{i-1} * d_{k}$ si $d_{k} * d_{j}$, operație care are costul $d_{i-1}d_{k}d_{j}$
    • însumăm cele 3 costuri intermediare
Implementare

Puteți rezolva și testa problema PODM pe infoarena aici.

Un exemplu de implementare în C++ se găsește mai jos.

Implementare C++

Implementare C++

// kInf este valoarea maximă - "infinitul" nostru
const unsigned long long kInf = std::numeric_limits<unsigned long long>::max();
 
// T = O(n ^ 3) 
// S = O(n ^ 2) - stocăm n x n întregi în tabloul dp
 unsigned long long solve_podm(int n, const vector<int> &d) {
    // 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, kInf));
 
    // Cazul de bază 1: nu am ce înmulți 
    for (int i = 1; i <= n; ++i) {
        dp[i][i] = 0ULL;  // 0 pe unsigned long long (voi folosi mai încolo și 1ULL)
    }
 
    // Cazul de bază 2: matrice d[i - 1] x d[i] înmulțită cu matrice d[i] x d[i + 1] 
    // (matrice pe poziții consecutive)
    for (int i = 1; i < n; ++i) {
        dp[i][i + 1] =  1ULL * d[i - 1] * d[i] * d[i + 1];  
    }
 
    // Cazul general:
    // dp[i][j] = min(dp[i][k] + dp[k + 1][j] + d[i - 1] * d[k] * d[j]), k = i : j - 1
    for (int len = 2; len <= n; ++len) {            // fixăm lungimea intervalului (2, 3, 4, ...)
        for (int i = 1; i + len - 1 <= n; ++i) {    // fixăm capătul din stânga: i
            int j = i + len - 1;                    // capătul din dreapta se deduce: j
 
            // Iterăm prin indicii dintre capete, spărgând șirul de înmulțiri in două (paranteze).
            for (int k = i; k < j; ++k) {
                // M_i * ... M_j  = (M_i * .. * M_k) * (M_k+1 *... * M_j)
                unsigned long long new_sol = dp[i][k] + dp[k + 1][j] + 1ULL * d[i - 1] * d[k] * d[j];
 
                // actualizăm soluția dacă este mai bună
                dp[i][j] = min(dp[i][j], new_sol); 
            }
        }
    }
 
    // Rezultatul se află în dp[1][n]: Numărul MINIM de inmultiri scalare
    // pe care trebuie să le facem pentru a obține produsul M_1 * ... * M_n
    return dp[1][n];
 
}

Sursa a fost scrisă pentru a fi testată pe infoarena. În cazul problemei 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.

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.

ATENȚIE! La PA, în general, vom folosi convenția $ expresie \ \% \ kMod $, care va fi detaliată în capitolul următor din acest laborator.

Complexitate

Întrucat soluția presupune fixarea capetelor unui subinterval (i, j), apoi alegerea unui intermediar (k), complexitatea este dată de aceste 3 cicluri.

  • complexitate temporală: $T(n) = O(n^3)$
  • complexitate spațială: $S(n) = O(n^2)$

Categoria 4: NUMĂRAT

Aceste recurențe au o oarecare asemănare:

  • toate numară lucruri! :p
  • interesante sunt cazurile când numărul căutat este foarte mare (altfel am putea apela la alte metode - ex. generarea tuturor candidaților posibili cu backtracking)
    • în acest caz, deoarece numărul poate să nu încapă pe un tip reprezentabil standard (ex. int pe 32/64 de biți), se cere (de obicei) restul împarțirii numărului căutat la un număr MOD (vom folosi în continuare această notație).

Sfaturi / Reguli

  • când căutați o recurență pentru o problema de numărare trebuie să aveți grijă la două aspecte:
    • 1) să NU numărați același obiect de două ori.
    • 2) să numărați toate obiectele în cauză.
  • de multe ori, o problemă de numărare implică o partiționare a tuturor posibilelor soluții după un anumit criteriu (relevant). Găsirea criteriului este partea esențială pentru aflarea recurenței.

Regulile de lucru cu clase de resturi

Reamintim câteva proprietăți matematice pe care ar trebui să le aveți în vedere atunci când implementați pentru a obține corect resturile anumitor expresii. (corect poate să însemne, de exemplu, să evitați overflow :D - lucru neintuitiv câteodată).

  • proprietăți de bază:
    • $(a + b) \ \% \ MOD = ((a \ \% \ MOD) + (b \ \% \ MOD)) \ \% \ MOD $
    • $(a \ * b) \ \% \ MOD = ((a \ \% \ MOD) \ * (b \ \% \ MOD)) \ \% \ MOD $
    • $(a - b) \ \% \ MOD = ((a \ \% \ MOD) - (b \ \% \ MOD) + MOD) \ \% \ MOD $ (restul nu poate fi ceva negativ; în C++ % nu funcționează pe numere negative)
  • invers modular
    • $ \frac{a}{b} \ \% \ MOD = ((a \ \% \ MOD) * (b ^ {MOD-2} \ \% \ MOD)) \ \% \ MOD)$
      • DACĂ MOD este prim; DACĂ a și b nu sunt multipli ai lui MOD

Explicații invers modular

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 }$

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)

Gardurile lui Gigel

Enunt

Gigel trece de la furat obiecte cu un rucsac la numarat garduri (fiecare are micile lui placeri :D). El doreste sa construiasca un gard folosind in mod repetat un singur tip de piesa.

O piesa are dimensiunile 4 x 1 (o unitate = 1m). Din motive irelevante pentru aceasta problema, orice gard construit trebuie sa aiba inaltime 4m in orice punct.

O piesa poate fi pusa in pozitie orizontala sau in pozitie verticala.

Cerinta

Gigel se intreaba cate garduri de lungime n si inaltime 4 exista? Deoarece celalalt prenume al lui este Bulănel, el intuieste ca acest numar este foarte mare, de aceea va cere restul impartirii acestui numar la 1009.

Exemplu 123

Exemplu 123

$n = 1$ sau $n = 2$ sau $n = 3$

Raspuns: 1 (un singur gard)

Explicatie: Se poate forma un singur gard in fiecare caz, dupa cum este ilustrat si in figura Garduri_123.

Exemplu 4

Exemplu 4

$n = 4$

Raspuns: 2

Explicatie: Se pot forma 2 garduri, in functie de cum asezam piesele, dupa cum este ilustrat si in figura Garduri_4. Observam ca de fiecare daca cand punem o piesa in pozitie orizontala, de fapt suntem obligati sa punem 4 piese, una peste alta!

Exemplu 5

Exemplu 5

$n = 5$

Raspuns: 3

Explicatie: Se pot forma 3 garduri, in functie de cum asezam piesele, dupa cum este ilustrat si in figura Garduri_5.

  • daca dorim ca acest gard sa se termine cu 4 piese in pozitie orizontala (una peste alta - marcat cu rosu), atunci la stanga mai ramane de completat un subgard de lungime 1, in toate modurile posibile
  • daca dorim ca acest gard sa se termine cu o piesa in pozitie verticala (marcat cu rosu), atunci la stanga mai ramane de completat un subgard de lungime 4, in toate modurile posibile

Exemplu 6

Exemplu 6

$n = 6$

Raspuns: 4

Explicatie: Se pot forma 4 garduri, in functie de cum asezam piesele, dupa cum este ilustrat si in figura Garduri_6.

  • daca dorim ca acest gard sa se termine cu o piesa in pozitie verticala (marcat cu rosu), atunci la stanga mai ramane de completat un subgard de lungime 5, in toate modurile posibile
  • daca dorim ca acest gard sa se termine cu 4 piese in pozitie orizontala (una peste alta - marcat cu rosu), atunci la stanga mai ramane de completat un subgard de lungime 2, in toate modurile posibile

Recurenta

Numire recurenta

$dp[i] $ = numarul de garduri de lungime i si inaltime 4 (nimic special - exact ceeea ce se cere in enunt)

Raspunsul la problema este $dp[n]$.

Gasire recurenta
  • Caz de baza
    • $dp[1] = dp[2] = dp[3] = 1$; $dp[4]$ = 2
  • Caz general
    • atunci dorim sa formam un gard de lungime i ($ i >= 5 $) am vazut ca putem alege cum sa punem ultima/ultimele piese
      • DACA alegem ca ultima piesa sa fie pusa in pozitie verticala, atunci la stanga mai ramane de completat un subgard de lungime $i-1$
        • numarul de moduri in care putem face acest subgard este $dp[i-1]$
      • DACA alegem ca ultima piesa sa fie in pozitie orizontala (de fapt punem 4 piese in pozitie orizontala), atunci la stanga mai ramane de completat un subgard de lungime $i-4$
        • numarul de moduri in care putem face acest subgard este $dp[i-4]$
    • $dp[i] = (dp[i-1] + dp[i-4]) \ \% \ MOD$

Asa cum am zis in sectiunea de sfaturi si reguli vrem sa facem o partionare dupa un anumit criteriu, in cazul problemei de fata criteriul de partionare este daca gardul se termina cu o scandura verticala sau orizontala.

De asemenea tot in sectiunea sfaturi si reguli am precizat ca nu vrem sa numaram un obiect (un mod de a construi gardul) de doua ori. Recurenta noastra (dp[i] = dp[i-1] + dp[i-4]) nu ia un obiect de doua ori pentru ca orice solutie care vine din dp[i-4] e diferita de alta care vine din dp[i-1] pentru ca difera in cel putin ultima scandura asezata)

Implementare recurenta

Aici puteti vedea un exemplu simplu de implementare in C++.

Implementare in C++

Implementare in C++

#define MOD 1009
int gardurile_lui_Gigel(int n) {
    // cazurile de baza
    if (n <= 3) return 1;
    if (n == 4) return 2;
 
    vector<int> dp(n + 1); // pastrez indexarea de la 1 ca in explicatii
 
    // cazurile de baza
    dp[1] = dp[2] = dp[3] = 1;
    dp[4] = 2;
 
    // cazul general
    for (int i = 5; i <= n; ++i) {
        dp[i] = (dp[i - 1] + dp[i - 4]) % MOD;
    }
 
    return dp[n];
}

Mentionez ca am folosit expresia $dp[i] = (dp[i - 1] + dp[i - 4]) \ \% \ MOD$ in loc de $dp[i] = ((dp[i - 1] \ \% \ MOD) + (dp[i - 4] \ \% \ MOD)) \ \% \ MOD$, deoarece pe valorile anterior calculate in dp a fost deja aplicata operatia $%$.

Am plecat cu numerele $1, 1, 1, 2$ si la fiecare pas rezultatul stocat este $\ \% \ MOD$, deci tot ce este stocat deja in dp este un rest in raport cu MOD. NU mai era nevoie deci sa aplica % si pe termenii din paranteza.

Complexitate
  • complexitate temporala: $T = O(n)$
    • explicatie: avem o singura parcurgere in care construim tabloul dp
    • se poate obtine $T=O(log n)$ folosind exponentiere pe matrice!
  • complexitate spatiala: $S = O(n)$
    • explicatie: stocam tabloul dp
    • se poate ontine $S = O(1)$ folosind exponentiere pe matrice!

Tehnici folosite in DP

De multe ori este nevoie sa folosim cateva tehnici pentru a obtine performanta maxima cu recurenta gasita.

In laboratorul 3 se mentiona tehnica de memoizare (in prima parte a laboratorului). In acesta ne vom rezuma la cum putem folosi cunostintele de lucru matricial pentru a favoriza implementarea unor anumite tipuri de recurente.

Exponentiere pe matrice pentru recurente liniare

Recurente liniare

O recurenta liniara in contextul laboratorului de DP este de forma:

  • $dp[i] = \sum_{k=1}^{KMAX} c_k * dp[i - k]$
    • pentru KMAX o constanta
    • de obicei, KMAX este foarte mica comparativ cu dimensinea n a problemei
    • $c_k$ constante reale (unele pot fi nule)

O astfel de recurenta ar insemna ca pentru a calcula costul problemei i , imbinam costurile problemelor $i - 1, i-2, ...., i-k$, fiecare contribuind cu un anumit coeficient $c_{1}, c_{2}, ..., c_{k}$.

Complexitate recurente liniara

Complexitate recurente liniara

Presupunand ca nu mai exista alte specificatii ale problemei si ca avand cele KMAX cazuri de baza (primele KMAX valori ar trebui stiute/deduse prin alte reguli), atunci un algoritm poate implementa recurenta de mai sus folosind 2 cicluri de tip for (for i = 1 : n, for k = 1 : KMAX …).

  • complexitatea temporala : $ T =O(n * KMAX) = O(n) $
    • reamintim ca acea valoarea KMAX este o constanta foarte mica in compartie cu n (ex. KMAX < 100)
  • complexitatea spatiala : $ S = O(n) $
  • am presupus ca avem nevoie sa retinem doar tabloul dp

Exponentiere pe matrice

Facem urmatoarele notatii:

  • $S_i$ = starea la pasul i
    • $S_i = (dp[i - k + 1], dp[i - k + 2], ..., dp[i - 1], dp[i])$
  • $S_k$ = starea initiala (in care cunoaste cele k cazuri de baza)
    • $S_k = (dp[1], dp[2], ..., dp[k-1], dp[k])$
  • $C$ = matrice ce coeficienti constanti
    • are dimensiune $KMAX * KMAX$
    • putem pune constante in clar
    • putem pune constantele $c_k$ care tin de problema curenta
Algoritm naiv

Putem formula problema astfel:

  • $S_k$ = este starea initiala
  • pentru a obtine starea urmatoare, aplicam algoritmul urmator
    • $S_i = S_{i-1}C$
Determinare C

Pentru a determina elementele matricei C, trebuie sa ne uitam la inmultirea matriceala de mai sus si sa alegem elementele lui C astfel incat prin inmultirealui $S_{i-1}$ cu $C$ sa obtinem elementele din $S_i$.

\begin{gather} \begin{bmatrix} dp[i - k + 1] & ... & dp[i-1] & dp[i] \\ \end{bmatrix} = \begin{bmatrix} dp[i - k] & ... & dp[i-2] & dp[i-1] \\ \end{bmatrix} \begin{bmatrix} 0 & 0 &... & 0 & 0 & c_{k}\\ 1 & 0 &... & 0 & 0 & c_{k-1}\\ 0 & 1 &... & 0 & 0 & c_{k-2}\\ ... & ... & ... & ... & ...\\ 0 & 0 &... & 1 & 0 & c_{2}\\ 0 & 0 &... & 0 & 1 & c_{1}\\ \end{bmatrix} \end{gather}

Explicatie determinare C

Explicatie determinare C

  • ultima coloana contine toti coeficientii $c_k$ intrucat $dp[i] = \sum_{k=1}^{KMAX} c_k * dp[i - k]$
  • celelalte coloane contin doar cate o valoare nenula
    • pe coloana j vom avea valoarea 1 pe linia $j+1$ ($j = 1 : KMAX - 1$)
      • cum obtinem, de exemplu, $dp[i - 1]$?
      • pai avem $dp[i-1]$ chiar si in starea $S_{i-1}$, deci trebuie sa il copiam in starea $S_i$
        • copierea se realizeaza prin inmultirea cu 1
        • daca $dp[i-1]$ era pe ultima pozitiei (pozitia k) in starea $S_{i-1}$, in noua stare $S_i$ este pe penultima pozitie (pozitia $k-1$)
          • deci s-a deplasat la stanga cu o pozitie!
    • in noua stare, noua pozitie este deplasata cu o unitate la stanga fata de starea precedenta
      • de aceea pe coloana $j$, vrem sa avem elementul 1 pe linia $j + 1$ ($j = 1 : KMAX - 1$)
      • cand inmultim $S_{i-1}$ cu coloana $C_j$ dorim sa
        • ce copiam?
          • valoarea $dp[i - KMAX + j]$ din $S_{i-1}$ in $S_{i}$
          • adica sa copiam a j-a valoare de pe linie
        • unde copiam?
          • de pe pozitia $j + 1$ pe pozitia $j$
Exponentiere logaritmica pe matrice

Algoritmul naiv de mai sus are dezavantajul ca are tot o complexitate temporala $O(n)$.

Sa executam cativa pasi de inductie pentru a vedea cum este determinat $S_i$. $$S_i = S_{i-1}C$$ $$S_i = S_{i-2}C^2$$ $$S_i = S_{i-3}C^3$$ $$...$$ $$S_i = S_{k}C^{i -k}$$

In laboratorul 2 (Divide et Impera) am invatat ca putem calcula $ x ^ n $ in timp logaritmic. Deoarece si inmultirea matricilor este asociativa, putem calcula $C ^ n$ in timp logaritmic.

Obtinem astfel o solutie cu urmatoarele complexitati:

  • complexitate temporala : $T = O(KMAX^3 * log(n))$
    • explicatie
      • facem doar $O(log n)$ pasi, dar un pas implica inmultire de matrice
      • o inmultire de matrice patratica de dimensiune KMAX are $KMAX^3$ operatii
    • aceasta metoda este eficienta cand $KMAX << n$ (KMAX este mult mai mic decat n)
  • complexitatea spatiala : $S = O(KMAX^3)$
    • explicatie
      • este nevoie sa stocam cateva matrici

Observatie! In ultimele calcule nu am sters contanta KMAX, intrucat apare la puterea a 3-a! $KMAX = 100$ implica $KMAX^3 = 10^6$, valoare care nu mai poate fi ignorata in practica ($KMAX^3$ poate fi comparabil cu n).

Gardurile lui Gigel (optimizare)

Dupa cum am vazut mai sus, in problema cu garduri data de Gigel solutia este o recurenta liniara:

  • $dp[1] = dp[2] = dp[3] = 1$; $d[4]=2$;
  • $dp[i] = dp[i - 1] + dp[i - 4]$, pentru $i > 4$
Exponentiere rapida
  • $ k = 4 $
  • $S_4 = (dp[1], dp[2], dp[3], dp[4]) = (1, 1, 1, 4)$
  • $S_i = (dp[i-3], dp[i-2], dp[i-1], dp[i])$
  • Raspunsul se afla efectuand operatia $S_n = S_4 * C^{n - 4}$, unde C are urmatorul continut:

\begin{gather} C = \begin{bmatrix} 0 & 0 & 0 & 1\\ 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 1 & 1\\ \end{bmatrix} \end{gather}

Implementare in C++

Implementare in C++

Mai jos se afla o implementare simplista in C++ care cuprinde toate etapele pe care trebuie sa le realizati in cod, dupa ce stiti cum arata recurenta sub forma matriceala.

#define MOD  1009
#define KMAX 4
 
// C = A * B
void multiply_matrix(int A[KMAX][KMAX], int B[KMAX][KMAX], int C[KMAX][KMAX]) {
    int tmp[KMAX][KMAX];
 
    // tmp = A * B
    for (int i = 0; i < KMAX; ++i) {
        for (int j = 0; j < KMAX; ++j) {
            unsigned long long sum = 0; // presupun ca suma  incape pe 64 de biti
 
            for (int k = 0; k < KMAX; ++k) {
                sum += 1LL * A[i][k] * B[k][j];
            }
 
            tmp[i][j] = sum % MOD;
        }
    }
 
    //  C = tmp
    memcpy(C, tmp, sizeof(tmp));
}
 
// R = C^p
void power_matrix(int C[KMAX][KMAX], int p, int R[KMAX][KMAX]) {
    // tmp = I (matricea identitate)
    int tmp[KMAX][KMAX];
    for (int i = 0; i < KMAX; ++i) {
        for (int j = 0; j < KMAX; ++j) {
            tmp[i][j] = (i == j) ? 1 : 0;
        }
    }
 
    while (p != 1) {
        if  (p % 2 == 0) {
            multiply_matrix(C, C, C);     // C = C*C
            p /= 2;                       // ramane de calculat C^(p/2)
        } else {
            // reduc la cazul anterior:
            multiply_matrix(tmp, C, tmp); // tmp = tmp*C
            --p;                          // ramane de calculat C^(p-1)
        }
    }
 
    // avem o parte din rezultat in C si o parte in tmp
    multiply_matrix(C, tmp, R);           // rezultat = tmp * C
}
 
int garduri_rapide(int n) {
    // cazurile de baza
    if (n <= 3) return 1;
    if (n == 4) return 2;
 
    // construiesc matricea C
    int C[KMAX][KMAX] = { {0, 0, 0, 1},
                          {1, 0, 0, 0},
                          {0, 1, 0, 0},
                          {0, 0, 1, 1}};
   // vreau sa aplic formula S_n = S_4 * C^(n-4)
 
   // C = C^(n-4)
   power_matrix(C, n - 4, C);
 
   // sol = S_4 * C = dp[n] (se afla pe ultima pozitie din S_n,
   // deci voi folosi ultima coloana din C)
   int sol = 1 * C[0][3] + 1 * C[1][3] + 1 * C[2][3] + 2 * C[3][3];
   return sol % MOD;
}

Remarcati faptul ca in functia de inmultire se foloseste o matrice temporara $tmp$. Motivul este ca vrem sa apelam functia $multiply(C, C, C)$, unde C joaca atat rol de intrare cat si de iesire. Daca am pune rezultatele direct in C, atunci am strica inputul inainte sa obtinem rezultatul.

Putem spune ca acea functie este matrix_multiply_safe, in sensul ca pentru orice A,B,C care respecta dimensiunile impuse, functia va calcula corect produsul.

Comparatie solutii (studiu de caz pentru curiosi)

Comparatie solutii (studiu de caz pentru curiosi)

In arhiva demo-lab04.zip gasiti o sursa completa in care se realizeaza:

  • o verificare a faptului ca cele 2 implementari ( gardurile_lui_Gigel si garduri_rapide) produc aceleasi rezultate
  • un benchmark in care cele 2 implementari sunt comparate
    • pe sistem uzual (laptop) s-au obtinut urmatoarele rezulate:
test case: varianta simpla
n = 100000000 sol = 119; time = 0.984545 s
test case: varianta rapida
n = 100000000 sol = 119; time = 0.000021 s
 
 
test case: varianta simpla
n = 1000000000 sol = 812; time = 9.662377 s
test case: varianta rapida
n = 1000000000 sol = 812; time = 0.000022 s
  • se observa clar diferenta intre cele 2 solutii (am confirmat ceea ce spunea si teoria: $O(n) $ vs $O(log(n))$); aceasta tehnica imbunatateste drastic o solutie gasita relativ usor.

Exercitii

Scheletul de laborator se găsește pe pagina pa-lab::skel/lab04.

DP or math?

Fie un sir de numere naturale strict pozitive. Cate subsiruri (submultimi nevide) au suma numerelor para?

subsir (subsequence in engleza) pentru un vector v inseamna un alt vector $u = [v[i_1], v[i_2],..., v[i_k]]]$ unde $i_1 < i_2 < ... < i_k$.

Task-uri:

  • Se cere o solutie folosind DP.
  • Inspectand recurenta gasita la punctul precedent, incercati sa o inlocuiti cu o formula matematica.
  • Care este complexitatea pentru fiecare solutie (timp + spatiu)? Care este mai buna? De ce? :D

Deoarece rezultatul poate fi prea mare, se cere restul impartirii lui la $1000000007$ ($10^9 + 7$).

Pentru punctaj maxim pentru aceasta problema, este necesar sa rezolvati toate subpunctele (ex. nu folositi direct formula, gasiti mai intai recurenta DP). Trebuie sa implementati cel putin solutia cu DP.

Exemplu 1

Exemplu 1

$n = 3$

i123
v264

Raspuns: $7$

Explicatie: Toate subsirurile posibile sunt

  • $[2]$
  • $[2, 6]$
  • $[2, 6, 4]$
  • $[2, 4]$
  • $[6]$
  • $[6, 4]$
  • $[4]$

Toate subsirurile de mai sus au suma para.

Exemplu 2

Exemplu 2

$n = 3$

i123
v213

Raspuns: $3$

Explicatie: Toate subsirurile posibile sunt

  • $[2]$
  • $[2, 1]$
  • $[2, 1, 3]$
  • $[2, 3]$
  • $[1]$
  • $[1, 3]$
  • $[3]$

Subsirurile cu suma para sunt: $[2]$, $[2, 1, 3]$, $[1, 3]$.

Exemplu 3

Exemplu 3

$n = 3$

i123
v321

Raspuns: $3$

Explicatie: Toate subsirurile posibile sunt

  • $[3]$
  • $[3, 2]$
  • $[3, 2, 1]$
  • $[3, 1]$
  • $[2]$
  • $[2, 1]$
  • $[1]$

Subsirurile cu suma para sunt: $[3, 2, 1]$, $[3, 1]$, $[2]$.

Morala: 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).

In problemele de numarat, exista o sansa buna sa putem gasi (si) o formula matematica, care poate fi implementata intr-un mod mai eficient decat o recurenta DP.

Hint

Hint

Cate subsiruri au suma impara?

Expresie booleana

Se da o expresie booleana corecta cu n termeni. Fiecare din termeni poate fi unul din stringurile true, false, and, or, xor.

Numarati modurile in care se pot aseza paranteze astfel incat rezultatul sa fie true. Se respecta regulile de la logica (tabelele de adevar pentru operatiile and, or, xor).

Deoarece rezultatul poate fi prea mare, se cere restul impartirii lui la $1000000007$ ($10^9 + 7$).

In schelet vom codifica cu valori de tip char cele 5 stringuri:

  • false: 'F'
  • true: 'T'
  • and: '&'
  • or: '|'
  • xor: '^'

Functia pe care va trebui sa o implementati voi va folosi variabilele n (numarul de termeni) si expr (vectorul cu termenii expresiei).

Exemplu 1

Exemplu 1

$n = 5$ si $expr = ['T', '&', 'F', '^', 'T']$ (expr = [ true and false xor true])

Raspuns: $2$

Explicatie: Exista 2 moduri corecte de a paranteza expresia astfel incat sa obtinem rezultatul true (1).

  • $ T&(F^T) $
  • $ (T&F)^T $

Hint

Hint

  Complexitate temporală dorita este $O(n ^ 3)$.
  
  Optional, se pot defini functii ajutatoare precum **is_operand**, **is_operator**, **evaluate**.

Pentru rezolvarea celor doua probleme ganditi-va la ce scrie in sectiunea Sfaturi / Reguli. Pentru fiecare dintre cele doua probleme facem o partitionare dupa un anumit criteriu.

Pentru problema DP or math? partitionam toate subsirurile dupa critieriul paritatii sumei subsirului (cate sunt pare/impare).
Pentru problema expresie booleana partitionam toate parantezarile posibile dupa rezultatul lor (cate dau true/false).

Bonus

Asistentul va alege una dintre problemele din sectiunea Extra.

Hint

Hint

Recomandam sa NU fie una din cele 3 probleme de la Test PA 2017. Recomandam sa le incercati dupa ce recapitulati acasa DP1 si DP2, pentru a verifica daca cunostintele acumulate sunt la nivelul asteptat.

Extra

Extraterestrii

Extraterestrii

Rezolvati problema extraterestrii de la Test PA 2017.

Secvente

Secvente

Rezolvati problema Secvente de la Test PA 2017.

PA Country

PA Country

Rezolvati problema PA Country de la Test PA 2017.

iepuri

iepuri

Rezolvati pe infoarena problema iepuri.

Hint: Exponentiere logaritmica pe matrice

Solutie:

  • $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 foloseste ecuatia matriceala atasata. Complexitate: $O(T * log(n))$.

Minimum Path Sum

Minimum Path Sum

Rezolvati pe leetcode problema Minimum Path Sum.

Lacusta

Lacusta

Rezolvati pe infoarena problema Lacusta.

Suma4

Suma4

Rezolvati pe infoarena problema Suma4.

Subsir

Subsir

Rezolvati pe infoarena problema subsir.

2sah

2sah

Rezolvati pe infoarena problema 2sah.

Hint: Exponentiere logaritmica pe matrice

O descrie detaliata se afla in arhiva OJI 2015.

DP problems

DP problems

Articolul de pe leetcode conține o listă cu diverse tipuri de probleme de programare dinamică, din toate categoriile discutate la PA.

Referințe

[0] Capitolul Dynamic Programming din Introductions to Algorithms de către T. H. Cormen, C. E. Leiserson, R. L. Rivest, C. Stein

[1] http://infoarena.ro/problema/podm

[2] http://infoarena.ro/problema/kfib

pa/laboratoare/laborator-04.1615277065.txt.gz · Last modified: 2021/03/09 10:04 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