🐍 INFORMATICĂ — CLASA A IX-A

Python de la zero

Partea 5 — Recursivitate și Backtracking

De la apelarea unei funcții cu ea însăși până la construirea sistematică a soluțiilor.

1. Ce știm deja?

În părțile anterioare am învățat variabile, tipuri de date, condiții, repetări, șiruri de caractere, liste, matrice, algoritmi de căutare și sortare și funcții. Sursa inițială subliniază că obiectivul este înțelegerea algoritmului, nu memorarea mecanică a comenzilor.

Variabile
→
Condiții
→
Repetări
→
Liste / Matrice
→
Funcții
→
Recursivitate
→
Backtracking
Ideea noii părți: până acum am construit algoritmi care avansează prin pași. Acum învățăm algoritmi care pot rezolva o problemă împărțind-o în probleme de același tip și, mai târziu, pot încerca sistematic mai multe variante.

2. Ce este recursivitatea?

Recursivitatea este tehnica prin care o funcție se apelează pe ea însăși pentru a rezolva o problemă de același tip, dar mai mică.

O funcție recursivă trebuie să aibă două elemente:

1. Caz de bază

situația în care răspunsul este cunoscut direct și nu mai facem apel recursiv.

2. Pas recursiv

funcția rezolvă o problemă mai mică și folosește rezultatul pentru problema curentă.

Exemplu: afișarea numerelor

def afiseaza(n):
    if n == 0:
        return
    afiseaza(n - 1)
    print(n)

afișeaza(5)
Atenție: dacă uităm cazul de bază sau dacă problema nu devine mai mică, apelurile pot continua până când programul nu mai poate continua.

3. Cum funcționează apelurile recursive?

La fiecare apel recursiv, Python trebuie să păstreze informațiile despre apelul curent. Putem urmări apelurile ca pe o stivă.

afiseaza(3)
→ afiseaza(2)
  → afiseaza(1)
    → afiseaza(0)
      → return
    → print(1)
  → print(2)
→ print(3)
Regulă practică: urmărește mai întâi coborârea apelurilor până la cazul de bază, apoi urmărește întoarcerea din apeluri.

4. Recursivitate cu parametri și return

În Partea 3 am învățat funcții, parametri și return. Aici le combinăm cu recursivitatea.

Factorialul

Matematic:

0! = 1
n! = n · (n-1)!
def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)

print(factorial(5))

Pentru 5: 5 · 4 · 3 · 2 · 1 · 1 = 120.

Sumă de la 1 la n

def suma(n):
    if n == 0:
        return 0
    return n + suma(n - 1)

print(suma(5))

5. Recursivitate și liste

Putem folosi un indice pentru a spune ce element al listei prelucrăm.

Suma elementelor

def suma_lista(a, i):
    if i == len(a):
        return 0
    return a[i] + suma_lista(a, i + 1)

print(suma_lista([4, 7, 2, 9], 0))
Observație: indicele i crește de la o etapă la alta, iar la i == len(a) am ajuns după ultimul element.

Cea mai mare valoare

def maxim_lista(a, i):
    if i == len(a) - 1:
        return a[i]
    m = maxim_lista(a, i + 1)
    if a[i] > m:
        return a[i]
    return m

6. Ce este Backtracking?

Backtracking construiește o soluție pas cu pas. La fiecare poziție încearcă o posibilitate; dacă acea posibilitate nu poate conduce la o soluție validă, renunță la ea și încearcă alta.
Aleg o variantă
→
Verific
→
Continui
→
Nu mai pot?
→
Mă întorc

Modelul general

def backtracking(pozitie):
    if am_completat_solutia:
        afisez_solutia()
        return

    for varianta in variante:
        if este_valida(varianta):
            aleg(varianta)
            backtracking(pozitie + 1)
            anulez(varianta)

Ultimul pas este esențial: după revenirea din apel, refacem starea pentru a putea încerca următoarea variantă.

7. Arborele soluțiilor

Backtracking-ul poate fi înțeles vizual ca o explorare a unui arbore. Fiecare nod reprezintă o alegere parțială, iar fiecare ramură reprezintă o posibilitate.

                 început
              /     |     \
             A      B      C
           / | \  / \   / | \
          ...     ...     ...
Două întrebări importante:
1. Ce reprezintă o poziție în soluție?
2. Ce variante sunt permise la acea poziție?

8. Permutări

O permutare este o așezare a tuturor elementelor într-o anumită ordine. Pentru [1,2,3] avem 6 permutări.

Ideea algoritmului

La poziția curentă încercăm fiecare element care nu a fost folosit.

a = [1, 2, 3]
sol = [0, 0, 0]
folosit = [False, False, False]

def permutari(k):
    if k == len(a):
        print(sol)
        return

    for i in range(len(a)):
        if not folosit[i]:
            sol[k] = a[i]
            folosit[i] = True
            permutari(k + 1)
            folosit[i] = False

permutari(0)
De ce avem nevoie de folosit? Pentru a nu alege același element de două ori în aceeași permutare.

9. Combinări

Într-o combinație alegem mai multe elemente, dar ordinea lor nu contează. De exemplu, {1,3} și {3,1} reprezintă aceeași combinație.

Pentru a evita repetarea aceleiași combinații, următoarea alegere începe de la un indice mai mare.

a = [1, 2, 3, 4]
sol = [0, 0]

def combinari(k, start):
    if k == 2:
        print(sol)
        return

    for i in range(start, len(a)):
        sol[k] = a[i]
        combinari(k + 1, i + 1)

combinari(0, 0)

10. Aranjamente

La aranjamente alegem mai multe elemente, iar ordinea contează. Un element nu poate fi folosit de două ori în aceeași soluție.

a = [1, 2, 3, 4]
sol = [0, 0]
folosit = [False] * len(a)

def aranjamente(k):
    if k == 2:
        print(sol)
        return

    for i in range(len(a)):
        if not folosit[i]:
            sol[k] = a[i]
            folosit[i] = True
            aranjamente(k + 1)
            folosit[i] = False

aranjamente(0)
TipToate elementele?Ordinea contează?Repetarea unui element?
PermutăriDaDaNu
AranjamenteNu neapăratDaNu
CombinăriNu neapăratNuNu

11. Backtracking pe matrice

O matrice poate reprezenta o tablă. Putem folosi backtracking pentru probleme în care trebuie să construim un drum, să plasăm obiecte sau să alegem poziții respectând reguli.

Exemplu conceptual: drum într-o matrice

La fiecare pas putem încerca direcții posibile. Dacă poziția este în afara matricei, este ocupată sau nu respectă regula problemei, alegerea este respinsă.

def drum(i, j):
    if am_ajuns_la_destinatie:
        afisez_drumul()
        return

    for directie in directii:
        ni = i + directie[0]
        nj = j + directie[1]

        if pozitia_este_valida(ni, nj):
            marchez(ni, nj)
            drum(ni, nj)
            demarchez(ni, nj)
Acesta este un model algoritmic. Într-o problemă concretă trebuie definite exact matricea, pozițiile de start și final, direcțiile permise și condiția de validitate.

12. Cum construim o soluție de Backtracking?

  1. Identificăm ce trebuie construit.
  2. Stabilim ce înseamnă o poziție din soluție.
  3. Stabilim variantele posibile la fiecare poziție.
  4. Scriem condiția de validitate.
  5. Stabilim cazul în care soluția este completă.
  6. Facem alegerea.
  7. Apelăm recursiv.
  8. Anulăm alegerea după revenire.
Întrebarea-cheie: „Dacă aleg această variantă, mai pot construi o soluție validă?” Dacă răspunsul este nu, putem renunța la acea ramură.

13. Exerciții rezolvate

Problema 1 — Factorial recursiv Ușor

Calculați n! folosind recursivitate.

def factorial(n):
    if n == 0:
        return 1
    return n * factorial(n - 1)

n = int(input())
print(factorial(n))
Problema 2 — Suma cifrelor Ușor
def suma_cifre(n):
    if n == 0:
        return 0
    return n % 10 + suma_cifre(n // 10)

n = int(input())
print(suma_cifre(n))
Problema 3 — Numărul de cifre Ușor
def numar_cifre(n):
    if n < 10:
        return 1
    return 1 + numar_cifre(n // 10)

n = int(input())
print(numar_cifre(n))
Problema 4 — Putere Ușor
def putere(a, n):
    if n == 0:
        return 1
    return a * putere(a, n - 1)

print(putere(2, 5))
Problema 5 — Palindrom recursiv Mediu
def palindrom(s, st, dr):
    if st >= dr:
        return True
    if s[st] != s[dr]:
        return False
    return palindrom(s, st + 1, dr - 1)

s = input()
print(palindrom(s, 0, len(s) - 1))
Problema 6 — Afișarea tuturor submulțimilor Mediu
a = [1, 2, 3]
sol = []

def submultimi(k):
    if k == len(a):
        print(sol)
        return

    submultimi(k + 1)
    sol.append(a[k])
    submultimi(k + 1)
    sol.pop()

submultimi(0)

Aici fiecare element are două posibilități: nu îl alegem sau îl alegem.

Problema 7 — Permutări ale numerelor 1,2,3 Mediu
a = [1, 2, 3]
sol = [0] * len(a)
folosit = [False] * len(a)

def permutari(k):
    if k == len(a):
        print(sol)
        return
    for i in range(len(a)):
        if not folosit[i]:
            sol[k] = a[i]
            folosit[i] = True
            permutari(k + 1)
            folosit[i] = False

permutari(0)
Problema 8 — Alegerea a două elemente Mediu
a = [1, 2, 3, 4]
sol = [0, 0]

def combinari(k, start):
    if k == 2:
        print(sol)
        return
    for i in range(start, len(a)):
        sol[k] = a[i]
        combinari(k + 1, i + 1)

combinari(0, 0)
Problema 9 — N regine, modelul algoritmic Dificil

Plasați N regine pe o tablă astfel încât două regine să nu se atace.

sol = [0] * n

def valid(k, col):
    for i in range(k):
        if sol[i] == col:
            return False
        if abs(sol[i] - col) == abs(i - k):
            return False
    return True

def regine(k):
    if k == n:
        print(sol)
        return
    for col in range(n):
        if valid(k, col):
            sol[k] = col
            regine(k + 1)

regine(0)
Problema 10 — Generarea numerelor binare Mediu

Generați toate șirurile de lungime n formate din 0 și 1.

sol = [0] * n

def binare(k):
    if k == n:
        print(sol)
        return
    sol[k] = 0
    binare(k + 1)
    sol[k] = 1
    binare(k + 1)

binare(0)

Exerciții propuse

Ușor

1. Suma numerelor de la 1 la n recursiv.

2. Produsul elementelor unei liste.

3. Numărul de elemente pare dintr-o listă.

4. Maximul unei liste recursiv.

5. Verificarea dacă toate elementele sunt pozitive.

6. Inversarea unui șir prin recursivitate.

7. Calculul lui an.

8. CMMDC prin algoritmul lui Euclid.

Mediu

9. Generați toate șirurile binare de lungime n.

10. Generați toate submulțimile unei liste.

11. Generați toate permutările unei liste.

12. Generați toate combinațiile de k elemente.

13. Generați toate aranjamentele de k elemente.

14. Numărați soluțiile unei probleme de tip backtracking.

15. Găsiți o sumă de elemente egală cu o valoare dată.

16. Generați toate parantezările corecte de lungime 2n.

Dificil

17. Problema celor N regine.

18. Labirint într-o matrice.

19. Drum de la colțul stânga-sus la dreapta-jos cu obstacole.

20. Generarea tuturor drumurilor valide.

21. Partiționarea unui număr în sume.

22. Colorarea unei hărți cu un număr limitat de culori.

23. Sudoku: construcția unei soluții.

24. Backtracking cu mai multe condiții de validare.

14. Recapitulare și checklist

ÎntrebarePot?
Știu ce este un caz de bază?☐
Pot scrie o funcție recursivă simplă?☐
Pot urmări apelurile recursive pas cu pas?☐
Înțeleg de ce problema trebuie să devină mai mică?☐
Știu ce înseamnă alegere și revenire în Backtracking?☐
Pot identifica variantele posibile la o poziție?☐
Pot construi permutări?☐
Pot construi combinații?☐
Pot folosi o listă pentru soluția parțială?☐
Pot explica de ce anulăm o alegere după apelul recursiv?☐
Ordinea logică a cursului: variabile → condiții → repetări → șiruri → liste și matrice → funcții → algoritmi pe structuri de date → recursivitate → Backtracking. Această ordine păstrează ideea de a introduce un concept înainte de a-l folosi în exerciții.
Pasul următor: după ce recursivitatea și Backtracking-ul sunt bine înțelese, putem trece la algoritmi mai avansați: programare dinamică, grafuri și tehnici de optimizare, fiecare într-o parte separată și introdusă progresiv.