Exemplele de cod apar încorporate și în textul laboratorului pentru a facilita parcurgerea cursivă a acestuia. ATENȚIE! Varianta actualizată a acestor exemple se găsește întotdeauna pe GitHub.
Backtracking este un algoritm care caută una sau mai multe soluții pentru o problema, printr-o căutare exhaustiva, mai eficientă însă în general decât o abordare „generează si testează”, de tip „forță brută”, deoarece un candidat parțial care nu duce la o soluție este abandonat. Poate fi folosit pentru orice problemă care presupune o căutare în spațiul stărilor. În general, în timp ce cautăm o soluție e posibil să dăm de un deadend în urma unei alegeri greșite sau să găsim o soluție, dar să dorim să căutăm în continuare alte soluții. În acel moment trebuie să ne întoarcem pe pașii făcuți (backtrack) și la un moment dat să luăm altă decizie. Este relativ simplu din punct de vedere conceptual, dar complexitatea algoritmului este exponentială.
Există foarte multe probleme (de exemplu, problemele NP-complete sau NP-dificile) care pot fi rezolvate prin algoritmi de tip backtracking mai eficient decât prin „forta bruta” (adică generarea tuturor alternativelor și selectarea soluțiilor). Atenție însă, complexitatea computațională este de cele mai multe ori exponențială. O eficientizare se poate face prin combinarea cu tehnici de propagare a restricțiilor. Orice problemă care are nevoie de parcurgerea spațiului de stări se poate rezolva cu backtracking.
Pornind de la strategiile clasice de parcurgere a spațiului de stări, algoritmii de tip backtracking practic enumeră un set de candidați parțiali, care, după completarea definitivă, pot deveni soluții potențiale ale problemei inițiale. Exact ca strategiile de parcurgere în lățime/adâncime și backtracking-ul are la bază expandarea unui nod curent, iar determinarea soluției se face într-o manieră incrementală. Prin natura sa, backtracking-ul este recursiv, iar în arborele expandat top-down se aplică operații de tipul pruning (tăiere) dacă soluția parțială nu este validă.
/* Domain - domeniul curent (cu un cardinal mai mic decat la pasul trecut) Solution - solutia curenta pe care o extindem catre cea finala */ back(Domain, Solution): if check(Solution): print(Solution) return for value in Domain: NextSolution = Solution.push(value) NextDomain = Domain.erase(value) back(NextDomain, NextSolution)
/* Domain - domeniul curent (cu un cardinal mai mic decat la pasul trecut) Solution - solutia curenta pe care o extindem catre cea finala */ back(Domain, Solution): if check(Solution): print(Solution) return for value in Domain: /* DO */ Solution = Solution.push(value) Domain = Domain.erase(value) /* RECURSION */ back(Domain, Solution) /* UNDO */ Solution = Solution.pop() Domain = Domain.insert(value)
Ne vom ocupa în continuare de următoarele probleme:
Se dă un număr N. Să se genereze toate permutările mulțimii formate din toate numerele de la 1 la N.
Soluția va avea următoarele complexitati:
Soluția va avea următoarele complexități:
Această abordare este mai eficientă decât cea generală, deoarece se evită folosirea memoriei auxiliare.
Soluția va avea următoarele complexitați:
Această soluție este optimă și are complexitatea temporală $T(n) = O(n!)$. Nu putem să obținem o soluție mai bună, întrucât trebuie să generăm n! permutări.
De asemenea, este optimă și din punct de vedere spatial, întrucât trebuie să avem $S(n) = O(n)$, din cauza stocării permutării generate.
Se dau numerele N si K. Să se genereze toate combinările mulțimii formate din toate numerele de la 1 la N, luate câte K.
Soluția va avea următoarele complexități:
Se dă un număr N și o matrice pătratică de dimensiuni N x N în care elementele egale cu 1 reprezintă ziduri (locuri prin care nu se poate trece), iar cele egale cu 0 reprezintă spații goale. Această matrice are un șoricel în celula (0, 0) și o bucată de brânză în celula (N - 1, N - 1). Scopul șoricelului e să ajungă la bucata de brânză. Afișați toate modurile în care poate face asta știind că acesta poate merge doar în dreapta sau în jos cu câte o celulă la fiecare pas.
Soluția va avea următoarele complexități:
Soluția va avea urmatoarele complexitati:
Enunt: Se dă o matrice de dimensiuni m × n formată din litere și un cuvânt word. Determinați dacă acest cuvânt poate fi format în matrice.
Cuvântul se construiește unind litere din celule adiacente (pe orizontală sau verticală). Nu aveți voie să folosiți aceeași celulă de două ori în formarea aceluiași cuvânt.
Date de intrare: O matrice de caractere de dimensiuni m × n și un șir de caractere word.
Date de ieșire: Se afișează true dacă cuvântul există în matrice, altfel false.
Problema se poate testa la: https://leetcode.com/problems/word-search/
Enunt: Se dă un șir de numere (care poate conține duplicate) și un număr țintă target. Găsiți toate combinațiile unice de elemente din șir a căror sumă este exact target.
Fiecare element de pe o anumită poziție din șir poate fi folosit cel mult o dată într-o combinație. Setul final de soluții nu trebuie să conțină combinații duplicate.
Date de intrare: Un vector de numere întregi candidates și un număr întreg target.
Date de ieșire: O listă de liste de numere întregi, reprezentând combinațiile unice valide.
Problema se poate testa la: https://leetcode.com/problems/combination-sum-ii/
Enunt: Codul Gray de ordin n este o secvență ce conține toate cele $2^n$ șiruri binare de lungime n, cu proprietatea că oricare două șiruri consecutive diferă prin exact un singur bit.
Cerința este să generați o astfel de secvență validă pentru un n dat.
Date de intrare: Un număr întreg n — lungimea șirurilor de biți.
Date de ieșire: Se afișează secvența de $2^n$ numere (în format zecimal sau binar), respectând regula codului Gray.
Problema se poate testa la: https://cses.fi/problemset/task/2205
Enunt: Vi se cere să scrieți un program care rezolvă un puzzle Sudoku clasic (9 × 9) prin completarea celulelor goale. Pentru ca soluția să fie validă, trebuie respectate regulile clasice: fiecare cifră de la 1 la 9 trebuie să apară o singură dată pe fiecare rând, pe fiecare coloană și în fiecare dintre cele nouă careuri 3 × 3.
Date de intrare: O matrice 9 × 9 de caractere reprezentând tabla de Sudoku inițială (celulele goale sunt marcate cu caracterul .).
Date de ieșire: Matricea 9 × 9 completată cu soluția corectă.
Problema se poate testa la: https://leetcode.com/problems/sudoku-solver/
Enunt: Se dă un șir de caractere s. Se cere să împărțiți șirul în fragmente, astfel încât fiecare fragment (subșir) rezultat să fie un palindrom.
Returnați toate aceste partiționări posibile.
Date de intrare: Un șir de caractere s.
Date de ieșire: O listă de liste de șiruri de caractere, unde fiecare listă interioară reprezintă o partiționare validă.
Problema se poate testa la: https://leetcode.com/problems/palindrome-partitioning/
Enunt: Se cere să găsiți o parcurgere validă a unei table de șah de dimensiuni 8 × 8 folosind un cal, astfel încât acesta să viziteze fiecare celulă a tablei exact o singură dată. Mutările trebuie să respecte regulile clasice de șah pentru cal (în formă de “L”).
Date de intrare: Două numere întregi x și y, care indică poziția inițială a calului pe tablă (coloana și rândul).
Date de ieșire: O matrice 8 × 8 în care fiecare celulă conține numărul pasului (de la 1 la 64) la care a fost vizitată respectiva poziție.
Problema se poate testa la:
[0] Chapter Backtracking, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein