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:
Fie N și K două numere naturale strict pozitive. Se cere afișarea tuturor aranjamentelor de N elemente luate cate K din mulțimea {1, 2, …, N}.
Checkerul așteaptă să le stocați în această ordine.
Fie N un număr natural strict pozitiv. Se cere afișarea tuturor submulțimilor mulțimii {1, 2, …, N}.
Checkerul așteaptă să le stocați în această ordine.
Problema damelor (sau problema reginelor) tratează plasarea a 8 regine de sah pe o tablă de șah de dimensiuni 8 x 8 astfel încat să nu existe două regine care se amenință reciproc. Astfel, se caută o soluție astfel încât nicio pereche de doua regine să nu fie pe același rând, pe aceeași coloană, sau pe aceeași diagonală. Problema cu opt regine este doar un caz particular pentru problema generală, care presupune plasarea a N regine pe o tablă de șah N x N în aceleasi condiții. Pentru această problemă, există soluții pentru toate numerele naturale N cu excepția lui N = 2 si N = 3.
Soluția este $sol[0], sol[1], ..., sol[n]$, unde $sol[i]$ = coloana unde vom plasa regina de pe linia i.
Elementul 0 este nefolosit, dorim să păstrăm convenția cu indexare de la 1.
Vi se dă o listă de caractere și o lista de frecvențe (pentru caracterul de pe poziția i, frecvența de pe poziția i). Vi se cere să generați toate șirurile care se pot forma cu aceste caractere și aceste frecvențe știind că nu pot fi mai mult de K apariții consecutive ale aceluiași caracter.
Checkerul așteaptă să le stocați în această ordine.
Aplicați AC3 pe problema damelor.
Algoritmul AC-3 (Arc Consistency Algorithm) este de obicei folosit în probleme de satisfacere a constrângerilor (CSP). Acesta șterge arcele din arborele de stări care sigur nu se vor folosi niciodata.
AC-3 lucrează cu:
O variabilă poate lua orice valoare din domeniul său la orice pas. O constrângere este o relație sau o limitare a unor variabile.
Considerăm A, o variabilă ce are domeniul D(A) = {0, 1, 2, 3, 4, 5, 6} și B o variabila ce are domeniul D(B) = {0, 1, 2, 3, 4}. Cunoaștem constrângerile: C1 = “A trebuie să fie impar” și C2 = “A + B trebuie să fie egal cu 5”.
Algoritmul AC-3 va elimina în primul rând toate valorile pare ale lui A pentru a respecta C1 ⇒ D(A) = {1, 3, 5}. Apoi, va încerca să satisfacă C2, așa că va păstra în domeniul lui B toate valorile care adunate cu valori din D(A) pot da 5 ⇒ D(B) = {0, 2, 4}.
AC-3 a redus astfel domeniile lui A si B, reducând semnificativ timpul folosit de algoritmul backtracking.
[0] Chapter Backtracking, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein