🌳 Backtracking

Capitolul 4 — Căutare exhaustivă cu revenire, permutări, combinări, problema reginelor

BacktrackingN-ReginePermutăriLabirintCombinări
📖 12 min
0
↩

Ce este Backtracking?

🌀 Analogie: Backtracking = Labirint

Imaginează-ți că ești într-un labirint și trebuie să găsești ieșirea:

  • Mergi pe un drum → dacă ajungi la un blocaj, dai înapoi (backtrack)
  • Încerci altă direcție → dacă și aceea e blocată, dai înapoi din nou
  • Continui până găsești ieșirea sau ai încercat toate drumurile

Backtracking-ul construiește soluția pas cu pas, și dacă o decizie duce la un eșec, se întoarce și încearcă altă variantă.

Backtracking este o tehnică algoritmică de căutare exhaustivă sistematică cu posibilitatea de a reveni (backtrack) atunci când o soluție parțială nu poate fi extinsă la o soluție completă validă.

Idee: Construim soluția element cu element. La fiecare pas, verificăm dacă alegerea curentă respectă restricțiile. Dacă nu, revenim și încercăm altă valoare.
Generare

Generăm toate soluțiile posibile (permutări, combinări, submulțimi)

Optimizare

Găsim soluția optimă care respectă anumite restricții

Decizie

Verificăm dacă există cel puțin o soluție (labirint, sudoku)

Complexitate: De obicei exponențială O(n!) sau O(2ⁿ). Se folosește pentru n mic (< 20-25).
📋

Șablonul general Backtracking

void backtracking(int nivel) {
    if (solutie_completa(nivel)) {
        afiseazaSolutie();
        return;
    }
    for (fiecare valoare posibila v) {
        if (esteValid(nivel, v)) {
            stiva[nivel] = v;         // alege valoarea
            backtracking(nivel + 1); // explorează mai departe
            stiva[nivel] = 0;         // BACKTRACK — dezalege
        }
    }
}
Alegeți o valoare pentru poziția curentă
Verificați dacă valoarea respectă restricțiile (funcția esteValid)
Recurgeți — apelați backtracking pentru nivelul următor
Reveniți (backtrack) — dezalegeți valoarea și încercați alta
Pn

Generarea permutărilor

Generăm toate aranjamentele posibile ale elementelor {1, 2, ..., n}. Numărul de permutări = n!

int n, st[20];
bool folosit[20];

void permutari(int nivel) {
    if (nivel == n + 1) {
        for (int i = 1; i <= n; i++)
            cout << st[i] << " ";
        cout << endl;
        return;
    }
    for (int v = 1; v <= n; v++) {
        if (!folosit[v]) {
            st[nivel] = v;
            folosit[v] = true;
            permutari(nivel + 1);
            st[nivel] = 0;         // backtrack
            folosit[v] = false;   // backtrack
        }
    }
}
Exemplu n=3: Generează 3! = 6 permutări: (1,2,3), (1,3,2), (2,1,3), (2,3,1), (3,1,2), (3,2,1)
Cnk

Generarea combinărilor

Generăm toate submulțimile de câte k elemente din {1, 2, ..., n}. Numărul de combinări = C(n,k) = n! / (k! × (n-k)!)

int n, k, st[20];

void combinari(int nivel, int startVal) {
    if (nivel == k + 1) {
        for (int i = 1; i <= k; i++)
            cout << st[i] << " ";
        cout << endl;
        return;
    }
    for (int v = startVal; v <= n; v++) {
        st[nivel] = v;
        combinari(nivel + 1, v + 1);  // elementele cresc strict
        st[nivel] = 0;
    }
}
Exemplu n=4, k=2: C(4,2)=6 combinări: (1,2),(1,3),(1,4),(2,3),(2,4),(3,4)
Diferența față de permutări: La combinări ordinea nu contează (1,2) = (2,1), iar fiecare element apare cel mult o dată. Pornind din startVal = v+1 asigurăm ordinea crescătoare și evităm duplicatele.
2ⁿ

Generarea submulțimilor

Generăm toate submulțimile (inclusiv mulțimea vidă) ale unei mulțimi cu n elemente. Numărul total = 2ⁿ.

int n;

void submultimi(int nivel) {
    // Afișăm submulțimea curentă (elementele cu st[i]=1)
    cout << "{";
    bool primul = true;
    for (int i = 1; i <= n; i++)
        if (st[i] == 1) {
            if (!primul) cout << ",";
            cout << i;
            primul = false;
        }
    cout << "} ";

    if (nivel == n + 1) return;

    // Fiecare element poate fi inclus (1) sau exclus (0)
    st[nivel] = 1; submultimi(nivel + 1);
    st[nivel] = 0; submultimi(nivel + 1);
}
Exemplu n=3: 2³=8 submulțimi: {}, {3}, {2}, {2,3}, {1}, {1,3}, {1,2}, {1,2,3}
♛

Problema celor N Regine

Problemă: Plasați N regine pe o tablă de șah N×N astfel încât nicio regină să nu amenințe alta (nu există două regine pe aceeași linie, coloană sau diagonală).
int n, col[15];  // col[i] = coloana reginei de pe linia i

bool esteValid(int linie, int coloana) {
    for (int i = 1; i < linie; i++) {
        if (col[i] == coloana) return false;  // aceeași coloană
        if (abs(col[i] - coloana) == abs(i - linie)) return false;  // diagonală
    }
    return true;
}

void regine(int linie) {
    if (linie == n + 1) {
        afiseazaSolutie();
        return;
    }
    for (int c = 1; c <= n; c++) {
        if (esteValid(linie, c)) {
            col[linie] = c;
            regine(linie + 1);
            col[linie] = 0;  // backtrack
        }
    }
}
NSoluții distincteSoluții fundamentale
421
5102
641
89212
1072492
🎬

Demo animat — 4 Regine

Vizualizează cum algoritmul backtracking plasează reginele una câte una, detectează conflictele și revine pentru a încerca alte variante.

Normal
Se încearcă Conflict Plasată ♛ = Regină
Ready
Demo 2: Permutări 1..n

Generează toate permutările și numărul total.

Demo 3: Combinări C(n,k)

Alege n și k pentru a vedea toate submulțimile de mărime k.

Demo 4: Submulțimi (2^n)

Afișează toate submulțimile pentru mulțimea {1..n}.

Demo 5: Labirint 5x5

Click pe celule pentru pereți. Caută drum de la S(0,0) la F(4,4).

🔀

Backtracking în labirint

Problemă: Găsiți o cale de la poziția de start la ieșire într-un labirint reprezentat ca matrice. 0 = liber, 1 = perete.
int lab[10][10], viz[10][10];
int n, m;  // dimensiuni
int dx[] = {-1, 0, 1, 0};  // sus, dreapta, jos, stânga
int dy[] = { 0, 1, 0, -1};

bool labirint(int x, int y, int xf, int yf) {
    if (x == xf && y == yf) return true;  // am ajuns!

    for (int d = 0; d < 4; d++) {
        int nx = x + dx[d], ny = y + dy[d];
        if (nx>=0 && nx<n && ny>=0 && ny<m && !viz[nx][ny] && lab[nx][ny]==0) {
            viz[nx][ny] = 1;         // marcăm ca vizitat
            if (labirint(nx, ny, xf, yf)) return true;
            viz[nx][ny] = 0;         // backtrack — ștergem marcajul
        }
    }
    return false;
}
🧩

Probleme rezolvate — backtracking

5 probleme clasice, fiecare cu enunț, exemplu și soluție comentată.

1 Toate șirurile binare de lungime n Ușor
Generează și afișează toate șirurile binare de lungime n (1 ≤ n ≤ 10), câte unul pe linie, în ordine lexicografică.
Intrare:
3
Ieșire:
000
001
010
011
100
101
110
111
💡 Vezi soluția
int n, x[15];
void bin(int k) {
    if (k > n) {
        for (int i = 1; i <= n; i++) cout << x[i];
        cout << '\n';
        return;
    }
    for (int v = 0; v <= 1; v++) {
        x[k] = v;
        bin(k + 1);
    }
}
// Apel: bin(1); Generează 2^n șiruri.

Idee: arborele binar de decizie — la fiecare poziție alegem 0 sau 1. Cazul de bază: când k > n, afișăm vectorul curent.

2 Aranjamente A(n, k) Mediu
Generează toate aranjamentele de k elemente alese din mulțimea {1, 2, ..., n}, în ordine lexicografică.
Intrare:
3 2
Ieșire:
1 2
1 3
2 1
2 3
3 1
3 2
💡 Vezi soluția
int n, k, x[20];
bool folosit[20];
void aranj(int p) {
    if (p > k) {
        for (int i = 1; i <= k; i++) cout << x[i] << ' ';
        cout << '\n';
        return;
    }
    for (int v = 1; v <= n; v++) {
        if (!folosit[v]) {
            x[p] = v;
            folosit[v] = true;
            aranj(p + 1);
            folosit[v] = false;   // backtrack!
        }
    }
}

Diferența față de permutări: ne oprim la k poziții (p > k), nu la n. Diferența față de combinări: ordinea contează — pornim mereu de la 1.

3 Suma s din mulțime Mediu
Date un vector cu n numere și o sumă s, afișează toate submulțimile care au suma egală cu s.
Intrare:
5 10
3 5 2 7 4
Ieșire:
3 5 2
3 7
2 4
3 5 2 (în alte ordini omise)
💡 Vezi soluția
int n, s, a[25];
bool sel[25];
void sub(int i, int sum) {
    if (i > n) {
        if (sum == s) {
            for (int j = 1; j <= n; j++)
                if (sel[j]) cout << a[j] << ' ';
            cout << '\n';
        }
        return;
    }
    // Decizie: includem a[i]
    sel[i] = true;
    sub(i + 1, sum + a[i]);
    sel[i] = false;       // backtrack
    // Decizie: NU includem a[i]
    sub(i + 1, sum);
}
// Apel: sub(1, 0);
// Optimizare (pruning): if (sum > s) return;

Două variante per element: îl includ sau nu. 2n submulțimi explorate. Cu pruning (oprim când sum > s, dacă elementele sunt pozitive) devine mult mai rapid în practică.

4 Numere strict crescătoare Mediu
Generează toate numerele de k cifre, cu cifre strict crescătoare, în ordine lexicografică. Cifrele aparțin mulțimii {1, 2, ..., 9}.
Intrare:
3
Ieșire:
123, 124, 125, …, 789  (84 numere)
💡 Vezi soluția
int k, x[10];
void cresc(int p) {
    if (p > k) {
        for (int i = 1; i <= k; i++) cout << x[i];
        cout << '\n';
        return;
    }
    int start = (p == 1) ? 1 : x[p - 1] + 1;
    for (int v = start; v <= 9; v++) {
        x[p] = v;
        cresc(p + 1);
    }
}

Restricția „strict crescător": pornim căutarea de la x[p-1] + 1. Nu mai e nevoie de tablou folosit[] — restricția se asigură automat din start.

5 Cod numeric pentru un cuvânt Dificil
Pentru un șir de cifre (ex: „123"), generează toate decodificările posibile știind că 1→A, 2→B, …, 26→Z. Cifrele se pot grupa câte 1 sau câte 2 (dacă valoarea ≤ 26).
Intrare:
123
Ieșire:
ABC
AW
LC
💡 Vezi soluția
string s, sol;
void decod(int i) {
    if (i == (int)s.size()) {
        cout << sol << '\n';
        return;
    }
    // 1) o singură cifră
    if (s[i] != '0') {
        sol += (char)('A' + (s[i] - '0') - 1);
        decod(i + 1);
        sol.pop_back();   // backtrack
    }
    // 2) două cifre (dacă valoarea ≤ 26)
    if (i + 1 < (int)s.size()) {
        int v = (s[i] - '0') * 10 + (s[i+1] - '0');
        if (v >= 10 && v <= 26) {
            sol += (char)('A' + v - 1);
            decod(i + 2);
            sol.pop_back();
        }
    }
}

Două decizii la fiecare pas: consumăm 1 sau 2 cifre. Restricții: nu permitem zero ca cifră singură; perechea de 2 cifre trebuie să fie între 10 și 26.

💪 Practică suplimentară: secțiunea BAC de mai jos și quiz-ul interactiv.
📝

Probleme tip BAC — backtracking

Problemă 1 (BAC clasic): Generați toate șirurile de n cifre (0-9) care nu conțin două cifre identice consecutive.
Soluție:
int st[20], n;
void generate(int nivel) {
    if (nivel == n + 1) { /* afișare */ return; }
    for (int c = 0; c <= 9; c++) {
        if (nivel == 1 || st[nivel-1] != c) {  // nu e identic cu precedentul
            st[nivel] = c;
            generate(nivel + 1);
        }
    }
}
Problemă 2: Generați toate partiționările unui număr n ca sumă de numere naturale distincte.
Soluție:
int st[30], n;
void partitii(int nivel, int suma, int minVal) {
    if (suma == n) { /* afișare st[1..nivel-1] */ return; }
    for (int v = minVal; v <= n - suma; v++) {
        st[nivel] = v;
        partitii(nivel + 1, suma + v, v + 1);
    }
}
// Apel: partitii(1, 0, 1)
Tip examen: Când scrieți backtracking la BAC, arătați clar:
1. Funcția de validare (condiție de continuare)
2. Cazul de bază (soluție completă)
3. Pasul de backtrack (dezalegere)
💪

Exerciții interactive — testează cunoștințele