🐍 INFORMATICĂ — CLASA A IX-A

Python de la zero

Partea 6 — Programare dinamică

De la ideea de subproblemă la construirea unor soluții eficiente.

1. De unde pornim?

Știm deja variabile, condiții, repetări, șiruri, liste, matrice, funcții, căutare, sortare, recursivitate și Backtracking.

Problema
→
Subprobleme
→
Rezultate memorate
→
Soluție eficientă
De ce avem nevoie de programare dinamică?

Uneori aceeași subproblemă este rezolvată de foarte multe ori. În loc să recalculăm același rezultat, îl calculăm o singură dată și îl păstrăm.

2. Ce este programarea dinamică?

Programarea dinamică este o tehnică prin care descompunem problema în subprobleme, memorăm rezultatele acestora și le reutilizăm.

Două idei esențiale

1. Subprobleme repetate

Aceeași situație apare de mai multe ori.

2. Memorarea rezultatului

Nu mai calculăm de fiecare dată aceeași valoare.

Programarea dinamică nu este o comandă Python. Este o tehnică algoritmică.

3. Cum definim o stare?

O stare descrie exact informația de care avem nevoie pentru a ști ce rezultat trebuie calculat.

Exemplu: Fibonacci

F(0) = 0
F(1) = 1
F(n) = F(n - 1) + F(n - 2)

Pentru F(5), apar subproblemele F(4) și F(3). Fără memorare, unele valori sunt recalculate.

Întrebarea-cheie: „Ce trebuie să știu despre problema mai mică pentru a construi răspunsul problemei curente?”

4. Memoizarea — de sus în jos

Putem păstra rezultatele deja calculate într-o listă.

def fib(n, mem):
    if n == 0:
        return 0
    if n == 1:
        return 1
    if mem[n] != -1:
        return mem[n]

    mem[n] = fib(n - 1, mem) + fib(n - 2, mem)
    return mem[n]

n = int(input())
mem = [-1] * (n + 1)
print(fib(n, mem))

Observăm legătura cu Partea 5: recursivitatea rămâne, dar rezultatele sunt memorate.

Atenție: lista folosită pentru memorare trebuie să aibă suficiente poziții. Pentru n, avem nevoie de pozițiile de la 0 la n.

5. Bottom-up — construim de la mic la mare

În loc să pornim de la problema mare, calculăm mai întâi cazurile mici și apoi construim rezultatul.

n = int(input())
dp = [0] * (n + 1)

if n >= 1:
    dp[1] = 1

for i in range(2, n + 1):
    dp[i] = dp[i - 1] + dp[i - 2]

print(dp[n])

Ce conține dp[i]?

dp[i] reprezintă valoarea problemei pentru starea i. Aceasta este o convenție pe care noi o alegem pentru problemă.

i012345
dp[i]011235

6. Probleme pe șiruri

Programarea dinamică poate fi folosită pentru probleme în care construim un răspuns pe prefixe ale unui șir.

Exemplu: cea mai lungă subsecvență crescătoare — idee simplificată

Definim dp[i] ca lungimea celei mai lungi subsecvențe crescătoare care se termină la poziția i.

a = [3, 1, 2, 5, 4]
n = len(a)
dp = [1] * n

for i in range(n):
    for j in range(i):
        if a[j] < a[i] and dp[j] + 1 > dp[i]:
            dp[i] = dp[j] + 1

print(max(dp))
Observație: acest exemplu introduce o tehnică importantă: starea se bazează pe rezultate calculate anterior.

7. Programare dinamică pe grile

O matrice poate reprezenta o grilă. Putem calcula numărul de moduri de a ajunge într-o celulă folosind celulele anterioare.

Exemplu: drumuri într-o grilă fără obstacole

n = int(input())
m = int(input())

dp = [[0] * m for i in range(n)]
dp[0][0] = 1

for i in range(n):
    for j in range(m):
        if i == 0 and j == 0:
            continue
        if i > 0:
            dp[i][j] = dp[i][j] + dp[i - 1][j]
        if j > 0:
            dp[i][j] = dp[i][j] + dp[i][j - 1]

print(dp[n - 1][m - 1])

La fiecare celulă, putem veni de sus sau din stânga. De aceea rezultatul este suma celor două rezultate precedente.

8. Problema rucsacului — introducere

Avem obiecte cu greutăți și valori. Rucsacul are o capacitate limitată. Dorim să alegem obiecte astfel încât valoarea totală să fie cât mai mare.

Pentru început, definim:

dp[i][c] = valoarea maximă obținută folosind primele i obiecte și o capacitate c.

Decizia

Pentru obiectul curent avem două posibilități:

  1. Nu îl alegem.
  2. Îl alegem, dacă încape.
if greutate[i - 1] <= c:
    dp[i][c] = max(
        dp[i - 1][c],
        valoare[i - 1] + dp[i - 1][c - greutate[i - 1]]
    )
else:
    dp[i][c] = dp[i - 1][c]

Aici max() compară cele două decizii posibile.

9. Nu vrem întotdeauna doar valoarea

Uneori problema cere nu doar „care este valoarea optimă?”, ci și ce alegeri au produs-o.

În acest caz putem porni de la starea finală și să mergem înapoi, identificând deciziile care au fost luate.

Ideea generală: calculăm răspunsul → păstrăm suficiente informații → reconstruim soluția.

10. De ce este mai eficient?

Un algoritm recursiv fără memorare poate calcula aceeași subproblemă de foarte multe ori. Prin memorare, fiecare stare poate fi calculată o singură dată.

MetodăIdeeRecalculări
Recursivitate simplăRezolvă direct subproblemelePot fi foarte multe
MemoizarePăstrează rezultateleMult reduse
Bottom-upCalculează stările în ordineFiecare stare este construită controlat

Complexitatea exactă depinde de problemă și de numărul de stări și tranziții.

11. Cum recunoaștem o problemă de programare dinamică?

Întrebarea 1

Problema poate fi împărțită în subprobleme?

Întrebarea 2

Apar aceleași subprobleme de mai multe ori?

Întrebarea 3

Putem defini o stare clară?

Întrebarea 4

Putem construi starea curentă din stări deja rezolvate?

Dacă răspunsul este „da” la aceste întrebări, merită să investigăm o soluție de programare dinamică.

12. Exerciții rezolvate

Problema 1 — Fibonacci bottom-up UȘOR

Calculați al n-lea termen.

n = int(input())
if n == 0:
    print(0)
else:
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    print(dp[n])
Problema 2 — număr de drumuri UȘOR

Calculați numărul de drumuri de la colțul stânga-sus la dreapta-jos într-o grilă.

Pentru fiecare celulă, numărul de drumuri este suma valorilor din celula de sus și cea din stânga.

Problema 3 — suma maximă pe un drum MEDIU

Într-o matrice, de la fiecare celulă putem merge în jos sau la dreapta. Determinați suma maximă.

Definim dp[i][j] ca suma maximă până la celula (i,j) și alegem maximul dintre predecesorii accesibili.

Problema 4 — cea mai lungă subsecvență crescătoare MEDIU

Pentru o listă de numere, determinați lungimea celei mai lungi subsecvențe strict crescătoare.

Pentru fiecare poziție, verificăm pozițiile anterioare și construim dp[i].

Problema 5 — rucsac 0/1 DIFICIL

Determinați valoarea maximă care poate fi transportată fără depășirea capacității.

Folosiți starea dp[i][c] și cele două decizii: alegem sau nu alegem obiectul.

Problema 6 — reconstrucția soluției DIFICIL

După calcularea unei valori optime, determinați și elementele care au fost alese.

Porniți din starea finală și comparați valoarea cu starea fără elementul curent pentru a identifica decizia.

13. Exerciții propuse

Ușor

1. Fibonacci.

2. Tribonacci.

3. Suma maximă a primelor i elemente.

4. Numărul de moduri de a urca n trepte cu pași de 1 sau 2.

5. Numărul de moduri de a ajunge la capătul unei grile.

6. Cost minim într-o grilă.

7. Suma maximă într-un șir.

8. Numărul de apariții al unei stări simple.

Mediu

9. Cea mai lungă subsecvență crescătoare.

10. Cea mai lungă subsecvență descrescătoare.

11. Suma maximă pe drum într-o matrice.

12. Cost minim pe drum cu obstacole.

13. Număr de drumuri cu celule interzise.

14. Partiționarea unui număr.

15. Monede — numărul de moduri de obținere a unei sume.

16. Monede — numărul minim de monede.

Dificil

17. Rucsac 0/1.

18. Rucsac cu reconstrucția obiectelor.

19. Subsecvență comună maximă a două șiruri.

20. Distanța de editare între două șiruri.

21. Cea mai lungă subsecvență palindromică.

22. Drum optim cu mai multe condiții.

23. Programare dinamică pe intervale — introducere.

24. Compararea unei soluții greedy cu una DP.

14. Greșeli frecvente

15. Checklist final

ÎntrebarePot?
Știu ce este o subproblemă?☐
Știu ce reprezintă o stare?☐
Pot identifica rezultate care se repetă?☐
Înțeleg memoizarea?☐
Pot construi o soluție bottom-up?☐
Pot folosi DP pe o listă?☐
Pot folosi DP pe o matrice?☐
Înțeleg ideea problemei rucsacului?☐
Știu diferența dintre calcularea valorii și reconstrucția soluției?☐
Ordinea logică: variabile → condiții → repetări → șiruri → liste și matrice → funcții → algoritmi → recursivitate → Backtracking → programare dinamică.
Pasul următor: după programarea dinamică putem studia grafurile, reprezentarea lor și algoritmii fundamentali de parcurgere și drumuri.