Invatare Atomica

Generarea permutarilor prin backtracking

Progres lectie:
0%
🎯

Obiectivul lectiei

Vei implementa generarea sistematica a tuturor permutarilor multimii {1, 2, ..., n} prin tehnica backtracking, vei intelege mecanismul de constructie si revenire, si vei aplica conditii de continuare pentru permutari cu restrictii.

Dupa aceasta lectie vei putea:

  • Sa definesti notiunea de permutare si sa calculezi numarul lor (n!)
  • Sa implementezi generarea permutarilor in Python folosind backtracking cu vector de marcaj
  • Sa trasezi arborele de cautare al backtracking-ului pentru n mic (n=2, n=3)
  • Sa adaugi conditii de continuare (pruning) pentru permutari cu restrictii
  • Sa analizezi complexitatea temporala O(n · n!) a algoritmului
  • [EXCLUSIV INTENSIV] Sa implementezi aceeasi solutie in C++ cu tablouri si functie recursiva

Incearca singur!

Provocare de gandire:

Scrie pe hartie toate modurile in care poti aranja literele A, B, C fara repetitie.
Cate ai gasit? Ai gasit toate? Cum esti sigur ca nu ai ratat niciuna?

💡 Ai nevoie de un indiciu? ▶

Incepe prin a fixa prima pozitie: daca A este primul, ramane sa aranjezi B si C in 2 moduri (B C sau C B). Daca B este primul, ai A si C in 2 moduri. Daca C este primul, tot 2 moduri. In total: 3 x 2 x 1 = 6 aranjamente.

Backtracking-ul face exact asta — sistematic, fara sa sara si fara sa repete.

1

1. Ce este o permutare?

O permutare a multimii {1, 2, ..., n} este o aranjare a tuturor celor n elemente intr-o ordine anume. Fiecare element apare exact o data.
Exemplu: Permutarile lui {1, 2, 3}
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

Total: 3! = 3 × 2 × 1 = 6 permutari

Formula: Numarul de permutari ale lui n elemente este n! (n factorial).
Cresterea lui n! (output real, Python):
n=1:    1 permutari
n=2:    2 permutari
n=3:    6 permutari
n=4:   24 permutari
n=5:  120 permutari
n=6:  720 permutari
n=7: 5040 permutari
n=8: 40320 permutari

Atentie: n! creste exploziv. Algoritmul devine lent pentru n > 12 in aplicatii de timp real.

2

2. Ideea algoritmului: constructie pozitie cu pozitie

Construim permutarea pas cu pas: la pasul k decidem ce element punem pe pozitia k. Incercam pe rand fiecare element nefolosit inca. Daca ajungem la o solutie completa (toate n pozitiile ocupate), o afisam, apoi revenim (backtrack) si incercam urmatoarea optiune.
Arborele de cautare pentru n=2 (output real, Python):
Pun 1 pe pozitia 0
  Pun 2 pe pozitia 1
    => Solutie: [1, 2]
  Revin: scot 2 de pe pozitia 1
Revin: scot 1 de pe pozitia 0
Pun 2 pe pozitia 0
  Pun 1 pe pozitia 1
    => Solutie: [2, 1]
  Revin: scot 1 de pe pozitia 1
Revin: scot 2 de pe pozitia 0
Ingredientele esentiale ale algoritmului:
  • perm[k] — elementul ales pentru pozitia k
  • folosit[i] — True daca elementul i este deja pe o pozitie
  • back(pas) — functia recursiva: genereaza pozitia pas si toate cele urmatoare
  • Caz de baza: daca pas == n, avem o solutie completa
  • Revenire: dupa exploatarea ramurii, dezactivam marcajul si incercam urmatorul element
3

3. Implementarea completa in Python

# Generarea permutarilor prin backtracking
# Limbaj: Python | Nivel: mat-info cls XI

def permutari(n):
    perm = [0] * n          # vectorul solutie, indexat 0..n-1
    folosit = [False] * (n + 1)  # folosit[i]=True daca i e pe o pozitie
    rezultate = []

    def back(pas):
        if pas == n:
            rezultate.append(perm[:])  # copie a solutiei curente
            return
        for i in range(1, n + 1):
            if not folosit[i]:
                perm[pas] = i
                folosit[i] = True
                back(pas + 1)
                perm[pas] = 0          # revenire: eliberam pozitia
                folosit[i] = False    # revenire: eliberam marcajul

    back(0)
    return rezultate

# Generam permutarile lui {1,2,3}
n = 3
sol = permutari(n)
print(f"Permutarile lui {1,...,{n}} ({len(sol)} bucati):")
for p in sol:
    print(' '.join(map(str, p)))
Output real (rulat cu python):
Permutarile lui {1,...,3} (6 bucati):
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
Observatie: Permutarile sunt generate in ordine lexicografica deoarece iteram candidatii i de la 1 la n in ordine crescatoare.
4

4. Permutari cu restrictii: taiere de ramuri (pruning)

Puterea backtracking-ului sta in taierea ramurilor (pruning): daca un candidat nu poate duce la o solutie valida, il sarim inainte de a-l explora recursiv. Adaugam o verificare suplimentara inainte de apelul back(pas+1).
Exemplu: Permutari fara doua numere pare consecutive
# Permutari fara doua pare consecutive
def permutari_restrictie(n):
    perm = [0] * n
    folosit = [False] * (n + 1)
    rezultate = []

    def back(pas):
        if pas == n:
            rezultate.append(perm[:])
            return
        for i in range(1, n + 1):
            if not folosit[i]:
                # Conditie de taiere: nu doua pare consecutive
                if pas > 0 and perm[pas-1] % 2 == 0 and i % 2 == 0:
                    continue  # taiem aceasta ramura
                perm[pas] = i
                folosit[i] = True
                back(pas + 1)
                perm[pas] = 0
                folosit[i] = False

    back(0)
    return rezultate

n = 4
sol = permutari_restrictie(n)
print(f"Permutari de 4 fara doua pare consecutive ({len(sol)} bucati):")
for p in sol:
    print(' '.join(map(str, p)))
Output real (rulat cu python):
Permutari de 4 fara doua pare consecutive (12 bucati):
1 2 3 4
1 4 3 2
2 1 3 4
2 1 4 3
2 3 1 4
2 3 4 1
3 2 1 4
3 4 1 2
4 1 2 3
4 1 3 2
4 3 1 2
4 3 2 1
Din cele 24 permutari posibile ale lui {1,2,3,4}, doar 12 respecta conditia. Backtracking-ul a evitat sa mai exploreze ramurile invalide, reducand efortul la jumatate in acest caz.
5

5. Complexitate si limite practice

Complexitate temporala: O(n · n!)
Exista n! permutari distincte. Fiecare permutare are n elemente, deci afisarea/copierea ei costa O(n). Suma totala: n × n! operatii.
Numar de permutari pentru diverse valori ale lui n:
n=1:       1 permutari
n=2:       2 permutari
n=3:       6 permutari
n=4:      24 permutari
n=5:     120 permutari
n=6:     720 permutari
n=7:    5040 permutari
n=8:   40320 permutari
Limite practice:
  • n ≤ 8: instant (sub 1 secunda)
  • n = 10: 3.628.800 permutari — cateva secunde
  • n = 12: 479.001.600 permutari — minute sau ore
  • n > 12: practic imposibil de enumerat toate
La subiecte BAC si concursuri, n este de obicei mic (n ≤ 8). Cand n este mare, se cer doar unele solutii (primele k, cele cu o proprietate speciala) sau se aplica pruning agresiv.
6

6. Implementare C++ EXCLUSIV INTENSIV

⚡ Sectiune doar pentru intensiv informatica

La profilul mat-info intensiv, acelasi algoritm se implementeaza in C++ cu tablouri statice si functie recursiva globala. Diferente fata de Python: tipuri explicite, tablouri de dimensiune fixa, bool explicit.

#include <iostream>
using namespace std;

int n;
int perm[10];        // solutia curenta, pozitii 0..n-1
bool folosit[10];    // marcaj elemente 1..n

void back(int pas) {
    if (pas == n) {
        for (int i = 0; i < n; i++) {
            cout << perm[i];
            if (i < n - 1) cout << " ";
        }
        cout << "\n";
        return;
    }
    for (int i = 1; i <= n; i++) {
        if (!folosit[i]) {
            perm[pas] = i;
            folosit[i] = true;
            back(pas + 1);
            perm[pas] = 0;          // revenire
            folosit[i] = false;    // revenire
        }
    }
}

int main() {
    n = 3;
    cout << "Permutarile lui {1,2,3}:\n";
    back(0);
    return 0;
}
Output real (compilat g++ -std=c++17, apoi rulat):
Permutarile lui {1,2,3}:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

Acelasi output ca versiunea Python — algoritmul este identic, doar sintaxa difera. In C++, variabilele globale int perm[10] si bool folosit[10] sunt initializate automat cu 0 / false.

Exercitii practice

Exercitiul 1 (Nivel minim) — Urmarire manuala

Ruleaza mental algoritmul permutari(2). Deseneaza pe hartie arborele de cautare: ce valori ia perm la fiecare pas? Ce solutii se genereaza? Verifica raspunsul cu output-ul din Atomul 2.

Vezi rezolvarea

Urmarirea manuala a lui permutari(2), cu elementele {1, 2}:

  1. back(0): incearca i=1, deci perm=[1,0], folosit=[F,T,F], apel back(1).
  2. back(1): incearca i=2 (i=1 e deja folosit), deci perm=[1,2]. Pas==n=2, deci solutie: [1, 2].
  3. Revenire: perm[1]=0, folosit[2]=F; iesim din back(1). Apoi perm[0]=0, folosit[1]=F.
  4. back(0): incearca i=2, deci perm=[2,0], apel back(1): incearca i=1, deci perm=[2,1]. Pas==n, deci solutie: [2, 1].
  5. Revenire completa pana la radacina. Total 2 solutii: [1,2] si [2,1], corespunde cu 2! = 2 si cu output-ul din Atomul 2.

Exercitiul 2 (Nivel standard) — Modificare cu conditie

Modifica algoritmul din Atomul 3 astfel incat sa genereze doar permutarile lui {1, 2, 3, 4, 5} in care 1 apare inaintea lui 2 (adica pozitia lui 1 este mai mica decat pozitia lui 2). Cate astfel de permutari exista? (Indiciu: exact jumatate din 5! = 60.)

Vezi rezolvarea

Pornim de la algoritmul din Atomul 3 si adaugam o conditie de taiere (pruning), ca in Atomul 4: nu punem elementul 2 pe o pozitie daca elementul 1 nu a fost inca folosit.

def permutari_ordonate(n):
  perm = [0]*n; folosit = [False]*(n+1); rezultate = []
  def back(pas):
    if pas == n: rezultate.append(perm[:]); return
    for i in range(1, n+1):
      if not folosit[i]:
        if i == 2 and not folosit[1]: continue
        perm[pas]=i; folosit[i]=True; back(pas+1)
        perm[pas]=0; folosit[i]=False
  back(0); return rezultate

  1. Taierea if i == 2 and not folosit[1]: continue impiedica plasarea lui 2 inainte ca 1 sa fi fost deja asezat pe o pozitie anterioara.
  2. Pentru n=5, numarul de solutii este exact 60 (jumatate din 5! = 120), asa cum arata si indiciul din cerinta.

Exercitiul 3 (Nivel performanta) — Permutari cu suma diagonalei

[BAC III / Olimpiada] Genereaza toate permutarile lui {1, 2, 3, 4} pentru care suma elementelor de pe pozitiile pare (perm[0] + perm[2]) este egala cu suma celor de pe pozitiile impare (perm[1] + perm[3]). Afiseaza-le si numara-le. Implementeaza in Python; optional si in C++ (la intensiv).

Vezi rezolvarea

Schita de rezolvare (nu solutia completa — completeaz-o singur):

  1. Porneste de la algoritmul din Atomul 3, cu n=4 si multimea {1,2,3,4}.
  2. La pas == n, inainte de a adauga solutia in rezultate, verifica perm[0]+perm[2] == perm[1]+perm[3]. Daca da, afiseaza si incrementeaza un contor.
  3. Capcana: nu incerca sa verifici suma inainte ca toate cele 4 pozitii sa fie completate — sumele partiale nu au sens pana nu exista perm[2] si perm[3].
  4. Optional, pentru eficienta (nu obligatoriu la n=4): poti taia o ramura mai devreme daca observi ca dupa plasarea a 3 elemente diferenta ramasa nu mai poate fi echilibrata de al 4-lea — dar la n=4 (doar 24 de permutari), backtracking-ul simplu fara pruning e suficient de rapid.
  5. Implementarea in C++ (optionala, la intensiv) urmeaza exact structura din Atomul 6, cu verificarea sumei adaugata inainte de afisare, in blocul if (pas == n).

Criterii de evaluare: algoritmul genereaza corect toate cele 24 de permutari de baza; verificarea sumei e facuta doar pe solutii complete; numaratoarea finala e corecta; (la performanta maxima) se discuta si o varianta cu taiere partiala.

Ce ai invatat astazi

  • Definitia permutarii si formula n! (factorialul)
  • Structura algoritmului backtracking: back(pas) cu vector perm[] si vector folosit[]
  • Mecanismul de revenire: reset perm[pas]=0 si folosit[i]=False dupa apelul recursiv
  • Taierea de ramuri (pruning): conditie suplimentara inaintea apelului recursiv
  • Complexitatea O(n · n!) si limitele practice ale algoritmului
  • [Intensiv] Implementarea in C++ cu tablouri globale si functie recursiva

Vrei mai mult?

Provocare: Modifica functia din Atomul 3 ca sa NUMERE (fara sa afiseze) cate permutari ale lui {1,...,6} au exact un punct fix, adica exact o pozitie i pentru care perm[i] == i+1. Adauga o verificare la finalul lui back(pas), cand pas==n, si un contor global.

De gandit: Algoritmul din Atomul 3 genereaza permutarile exact in ordine lexicografica (1 2 3, 1 3 2, 2 1 3, ...), fara nicio instructiune de sortare. De ce apare ordinea asta automat, doar din structura buclei for i in range(1, n+1)?

Deschidere: Generarea completa a permutarilor e metoda bruta pentru problema comisului voiajor (TSP) pe distante mici: fiecare permutare a oraselor e un traseu posibil, iar cel mai scurt se gaseste comparandu-le pe toate. Practic doar pana la n~10-11, dupa care n! devine prea mare.

Urmatoarea lectie

Continua cu Lectia 4 pentru a invata generarea aranjamentelor si combinarilor prin backtracking.

Continua →