Fibonacci — de la O(2ⁿ) la O(n)
Programarea dinamică (DP) rezolvă problemele complexe spărgându-le în subprobleme mai mici și stocând rezultatele pentru a nu le recalcula. Se aplică când există subprobleme suprapuse — adică aceeași subproblemă apare de mai multe ori în recursivitate.
Problema: recursivitate naivă — O(2ⁿ)
// Fibonacci recursiv NaIV — calculează de mii de ori aceleași valori! int fib(int n) { if (n <= 1) return n; // caz de bază return fib(n-1) + fib(n-2); // O(2^n) apeluri! } // fib(40) = ~2 miliarde operații → prea lent!
fib(5) calculează fib(3) de 2 ori, fib(2) de 3 ori. La n=40, se fac ~2 miliarde de apeluri pentru valori deja cunoscute.
Soluția DP: tabel — O(n)
int dp[100]; // dp[i] = al i-lea număr Fibonacci void calcFibDP(int n) { dp[0] = 0; dp[1] = 1; // cazuri de bază for (int i = 2; i <= n; i++) dp[i] = dp[i-1] + dp[i-2]; // fiecare valoare calculată o singură dată } // fib(40) = 8 operații, nu 2 miliarde!
Complexitate: O(2ⁿ)
n=40 → ~2 miliarde ops
Lent, inacceptabil la BAC
Complexitate: O(n)
n=40 → 40 operații
Rapid, eficient
Memoizare (DP top-down)
Memoizarea adaugă un cache funcției recursive existente. La primul apel, calculează și salvează. La apelurile ulterioare, returnează direct valoarea salvată.
int memo[10001]; // inițializat cu -1 = "necalculat" int fib(int n) { if (n <= 1) return n; if (memo[n] != -1) return memo[n]; // deja calculat → returnează direct memo[n] = fib(n-1) + fib(n-2); // calculează și salvează return memo[n]; } int main() { // Inițializare obligatorie! fill(memo, memo + 10001, -1); cout << fib(100) << endl; // O(n) apeluri în total }
int cache[MAXN]; memset(cache, -1, sizeof(cache)); // sau fill() int solve(int n) { if (/* caz de baza */) return /* valoare_baza */; if (cache[n] != -1) return cache[n]; cache[n] = /* calcul bazat pe apeluri recursive */; return cache[n]; }
Cod simplu, se pornește de la soluția recursivă existentă
Stiva de recursivitate — stack overflow pentru n mare
Timp: O(n) · Spațiu: O(n) pentru cache + O(n) stack
DP tabelar (bottom-up)
DP bottom-up construiește tabelul de la cazuri mici la mari, fără recursivitate. Este mai eficient ca memorie și evită stack overflow.
// Fibonacci bottom-up cu tabel complet — O(n) timp, O(n) spațiu int dp[100001]; dp[0] = 0; dp[1] = 1; for (int i = 2; i <= n; i++) dp[i] = dp[i-1] + dp[i-2]; cout << dp[n] << endl; // Optimizat cu 2 variabile — O(n) timp, O(1) spațiu! int a = 0, b = 1; for (int i = 2; i <= n; i++) { int c = a + b; a = b; b = c; } cout << b << endl; // Fibonacci(n)
// dp[i] = nr. moduri de a ajunge la treapta i int dp[1001]; dp[0] = 1; // 0 trepte: 1 mod (nu faci nimic) dp[1] = 1; // 1 treaptă: 1 mod (un pas de 1) for (int i = 2; i <= n; i++) dp[i] = dp[i-1] + dp[i-2]; // vin din treapta i-1 (1 pas) sau i-2 (2 pași) cout << dp[n] << endl;
1. Definește
dp[i] (ce reprezintă)2. Cazuri de bază (dp[0], dp[1], ...)
3. Relație de recurență (cum dp[i] depinde de dp[j], j<i)
4. Răspunsul (dp[n] sau max din dp[])
Cel mai lung subșir strict crescător (LIS)
Dat un vector v[1..n], găsim cel mai lung subșir de indici i₁ < i₂ < ... < iₖ cu v[i₁] < v[i₂] < ... < v[iₖ]. Elementele selectate nu trebuie să fie consecutive în vector.
Exemplu: v = [3, 1, 4, 1, 5, 9, 2, 6] → LIS = [1, 4, 5, 9] sau [1, 4, 5, 6], lungime = 4
Soluție DP — O(n²)
// dp[i] = lungimea celui mai lung subșir crescător care se termină la v[i] int v[1001], dp[1001], n; int lisLength() { for (int i = 1; i <= n; i++) { dp[i] = 1; // minim: subșir cu un singur element for (int j = 1; j < i; j++) if (v[j] < v[i]) // v[j] poate precede v[i] dp[i] = max(dp[i], dp[j] + 1); } int lis = 0; for (int i = 1; i <= n; i++) lis = max(lis, dp[i]); return lis; // O(n²) }
dp[1]=1 (v[1]=3), dp[2]=1 (v[2]=1), dp[3]=2 (1<4: dp[2]+1),
dp[4]=1 (v[4]=1, nimic mai mic), dp[5]=3 (4<5: dp[3]+1)
→ LIS = max(1,1,2,1,3) = 3
Două bucle imbricate. Suficient pentru n≤5000 la BAC.
Folosește Binary Search pentru n ≤ 10⁵. Nu e cerut la BAC M-I standard.
Triunghi de numere — suma maximă
Dat un triunghi de numere cu n rânduri, se coboară de la vârf (rândul 1) la baza (rândul n), la fiecare pas mișcând-ne pe același indice sau indicele+1. Găsim drumul cu suma maximă.
// Exemplu triunghi (n=4): // 7 // 3 8 // 8 1 0 // 2 7 4 4 // Drum optim: 7→8→1→7 = 23 (sau 7→3→8→7=25) int t[101][101], dp[101][101], n; // DP bottom-up (de la baza spre vârf) void triunghi() { // Copiem baza ca stare inițială for (int j = 1; j <= n; j++) dp[n][j] = t[n][j]; // Urcăm de la rândul n-1 spre rândul 1 for (int i = n-1; i >= 1; i--) for (int j = 1; j <= i; j++) dp[i][j] = t[i][j] + max(dp[i+1][j], dp[i+1][j+1]); cout << "Suma maxima: " << dp[1][1] << endl; // O(n²) }
dp[i][j] = t[i][j] + max(dp[i-1][j-1], dp[i-1][j]), pornind de la vârf. Ambele variante sunt echivalente.
Demo interactiv — Fibonacci DP tabelar
Urmărește cum se construiește tabelul DP pentru Fibonacci pas cu pas. Fiecare celulă se calculează o singură dată — O(n) în loc de O(2ⁿ).
Probleme rezolvate — programare dinamică
5 probleme DP clasice, fiecare cu starea, recurența și cazurile de bază explicate.
5
8
💡 Vezi soluția
long long dp[100]; int main() { int n; cin >> n; dp[0] = 1; dp[1] = 1; for (int i = 2; i <= n; i++) dp[i] = dp[i-1] + dp[i-2]; cout << dp[n]; }
Recurența: ca să ajungi la treapta i poți veni de la i−1 (pas de 1) sau i−2 (pas de 2). E exact Fibonacci! Stare: dp[i] = nr. modurilor de a ajunge la treapta i.
9 -2 1 -3 4 -1 2 1 -5 4
6
💡 Vezi soluția
int n, a[100005]; int main() { cin >> n; for (int i = 1; i <= n; i++) cin >> a[i]; int best = a[1], cur = a[1]; for (int i = 2; i <= n; i++) { cur = max(a[i], cur + a[i]); // extind sau restart best = max(best, cur); } cout << best; }
Algoritmul lui Kadane (O(n)): la fiecare poziție alegem fie să continuăm subvectorul curent, fie să pornim unul nou de la a[i]. Pe exemplu: 4 + (−1) + 2 + 1 = 6.
3 4 1 2 3
4
💡 Vezi soluția
int n, S, c[50]; long long dp[10005]; int main() { cin >> n >> S; for (int i = 1; i <= n; i++) cin >> c[i]; dp[0] = 1; // 1 mod să faci 0: nu pui nimic for (int i = 1; i <= n; i++) for (int s = c[i]; s <= S; s++) dp[s] += dp[s - c[i]]; cout << dp[S]; }
Cheia: bucla pe monede e exterioară — astfel evităm să numărăm aceeași combinație în ordini diferite. Pentru {1,2,3} și S=4: {1+1+1+1}, {1+1+2}, {2+2}, {1+3} = 4 moduri.
3 4
10
💡 Vezi soluția
long long dp[25][25]; int main() { int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) dp[i][1] = 1; for (int j = 1; j <= m; j++) dp[1][j] = 1; for (int i = 2; i <= n; i++) for (int j = 2; j <= m; j++) dp[i][j] = dp[i-1][j] + dp[i][j-1]; cout << dp[n][m]; }
Recurența: la celula (i,j) poți ajunge din (i−1,j) — venind de sus — sau din (i,j−1) — venind din stânga. Cazurile de bază: prima linie și prima coloană au câte un singur drum (mereu jos sau mereu dreapta).
w[i] și valoare v[i], și un rucsac de capacitate W. Maximizează valoarea totală selectând obiecte (fiecare 0 sau 1 dată).4 7 2 3 3 4 4 5 5 6
9
💡 Vezi soluția
int n, W, w[105], v[105]; int dp[105][10005]; int main() { cin >> n >> W; for (int i = 1; i <= n; i++) cin >> w[i] >> v[i]; for (int i = 1; i <= n; i++) for (int c = 0; c <= W; c++) { dp[i][c] = dp[i-1][c]; // nu luăm obiectul if (c >= w[i]) dp[i][c] = max(dp[i][c], dp[i-1][c-w[i]] + v[i]); } cout << dp[n][W]; }
Stare: dp[i][c] = valoarea maximă folosind primele i obiecte și capacitate c. Decizie: îl luăm pe i (dacă încape) sau nu. Pe exemplu: obiectele 3 (val 3+4=7) sau 4 (val 4+5=9) — răspuns 9. Complexitate O(n·W).
Probleme tip BAC — Programare Dinamică
Problema 1: Cel mai lung subșir de valori egale consecutive
// Cerință: găsește lungimea maximă a unui bloc de valori egale în șir int v[1001], n; int maxLen = 1, cur = 1; for (int i = 2; i <= n; i++) { if (v[i] == v[i-1]) cur++; else cur = 1; maxLen = max(maxLen, cur); } cout << maxLen;
Problema 2: Fibonacci(n) modulo 10⁹+7
const int MOD = 1000000007; long long dp[1000001]; dp[0] = 0; dp[1] = 1; for (int i = 2; i <= n; i++) dp[i] = (dp[i-1] + dp[i-2]) % MOD; cout << dp[n];
Problema 3: LIS — lungime și reconstituire
int v[1001], dp[1001], prev_idx[1001], n; void lis_reconstituire() { int best_end = 1; for (int i = 1; i <= n; i++) { dp[i] = 1; prev_idx[i] = -1; for (int j = 1; j < i; j++) if (v[j] < v[i] && dp[j] + 1 > dp[i]) { dp[i] = dp[j] + 1; prev_idx[i] = j; } if (dp[i] > dp[best_end]) best_end = i; } // Reconstituire (de la coadă spre cap) int idx = best_end; while (idx != -1) { cout << v[idx] << " "; idx = prev_idx[idx]; } }
Fibonacci, nr. modalități de dispunere, LIS, suma maximă pe drum în triunghi, probleme de numărare cu restricții
Dacă problema cere „numărul de moduri" sau „cea mai bună/lungă cale" — gândește-te la DP!