Differences

This shows you the differences between two versions of the page.

Link to this comparison view

pa:laboratoare:laborator-05 [2023/03/15 16:54]
radu.nichita
pa:laboratoare:laborator-05 [2024/04/01 14:12] (current)
radu.nichita
Line 1: Line 1:
-====== Laborator 05: Minimax ​====== +====== Laborator 05: Backtracking ​====== 
 + 
 ===== Obiective laborator ===== ===== Obiective laborator =====
-  * Însușirea unor cunoștințde bază despre ​**teoria jocurilor** precum și despre **jocurile de tip joc de sumă zero (suma nulă, zero-sum games)** +  * Întelegerea noțiunilor ​de bază despre ​backtracking;​ 
-  * Însușirea ​unor cunostințe elementare despre ​**algoritmii necesari** rezolvării ​unor probleme ​de joc de sumă zero (zero-sum game). +  * Însușirea ​abilităților de implementare a algoritmilor bazați pe backtracking;​ 
-  * Găsirea și compararea diverselor euristici pentru jocurile cunoscute, precum **șah**.+  ​Rezolvarea ​unor probleme ​NP-complete în timp exponențial.
  
 ===== Precizări inițiale ===== ===== Precizări inițiale =====
-<​note ​warning+<​note>​ 
-Un curs foarte bine explicat este pe canalul de YouTube ​de la MIT. Vă sfătuim să vizionați integral ​[[https://youtu.be/STjW3eH0CikSearch: Games, Minimax, and Alpha-Beta]] înainte să parcurgeți materialul ​de pe ocw.+Toate exemplele ​de cod se găsesc pe pagina ​[[https://github.com/acs-pa/​pa-lab/​tree/​main/​demo/​lab05|pa-lab::​demo/​lab05]]
 + 
 +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.
 </​note>​ </​note>​
  
-===== Introducere =====+  * Toate bucățile de cod prezentate în partea introductivă a laboratorului (înainte de exerciții) au fost testate. Cu toate acestea, este posibil ca din cauza mai multor factori (formatare, caractere invizibile puse de browser etc.) un simplu copy-paste să nu fie de ajuns pentru a compila codul. 
 +  * Vă rugam să compilați **DOAR** codul de pe GitHub. Pentru raportarea problemelor,​ contactați unul dintre maintaineri.  
 +  * Pentru orice problemă legată de conținutul acestei pagini, vă rugăm să dați e-mail unuia dintre responsabili.
  
-**Jocuri de sumă 0** ([[https://​en.wikipedia.org/​wiki/​Zero-sum_game |wikipedia.org/​zero-sum-game]]) sunt jocurile care de obicei se joacă în doi jucători și în care o mutare bună a unui jucător este în dezavantajul celuilalt jucător în mod direct. Aplicațiile din cadrul acestui laborator sunt în orice joc de sumă 0: șah, table, X și 0, dame, go etc. Orice joc în care se poate acorda un scor / punctaj pentru anumite evenimente - de exemplu la șah pentru capturarea unor piese sau la X și 0 pentru plasarea unui X pe o anumite poziție). 
  
-Algoritmul Minimax reprezintă una din cele mai cunoscute strategii ​pentru ​a juca jocurile ​de sumă 0. Minimax este un algoritm ce permite minimizarea pierderii de puncte într-mutare următoare a jucătorului adversConcretpentru ​alegere într-un joc oarecare se preferă cea care aduce un risc minim dintre viitoarele mutări bune ale adversarului.+===== Ce este Backtracking?​ ===== 
 +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 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 ​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ă.
  
-De asemenea, algoritmul Minimax este folosit în diverse domenii precum teoria jocurilor ​(Game Theory)teoria jocurilor combinatorice (Combinatorial Game Theory ​CGT), teoria deciziei ​(Decision Theory) ​și statistică.+===== Importanța – aplicaţii practice ===== 
 +Există foarte multe probleme ​(de exempluproblemele NP-complete sau NP-dificilecare 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
  
 +===== Descrierea problemei și a rezolvărilor =====
 +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ă.
  
-===== Descrierea algoritmului ​=====+===== Algoritm de baza ===== 
 +<code C> 
 +/* 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
  
-==== Minimax ==== +    for value in Domain: 
-Ideea pe care se bazează algoritmul este că jucătorii adoptă următoarele strategii:+        NextSolution ​Solution.push(value) 
 +        NextDomain ​Domain.erase(value) 
 +        ​back(NextDomain,​ NextSolution) 
 +</​code>​
  
-  *Jucătorul 1 (**Maxi**) va încerca mereu să-și **maximizeze** propriul câștig prin mutarea pe care o are de făcut; +{{ pa:​laboratoare:​perm_gen_viz.png?700|}}
-  *Jucătorul 2 (**Mini**) va încerca mereu să **minimizeze** câștigul jucătorului 1 la fiecare mutare.+
  
-De ce merge o astfel ​de abordare? După cum se preciza la început, discuția se axează pe jocuri de sumă zero (zero-sum game). Acest lucru garanteazăprintre altele, că orice câștig al Jucătorului 1 este egal cu modulul sumei pierdute de Jucătorul 2. Cu alte cuvinte, cât pierde Jucătorul 2, atât câștigă Jucător 1. Invers, cât pierde Jucător 1, atât câștigă Jucator 2. Sau:+===== Algoritm ​de baza (modificat pentru transmitere prin referinta) ===== 
 +<code C> 
 +/* Domain ​domeniul curent (cu un cardinal mai mic decat la pasul trecut) 
 +Solution - solutia curenta pe care o extindem catre cea finala */ 
 +back(DomainSolution): 
 +    if check(Solution):​ 
 +        print(Solution) 
 +        return
  
-$$ Câștig_{Jucător_1} ​| Pierdere_{Jucător_2} |$$ +    for value in Domain: 
-$$ Câștig_{Jucător_2} ​| Pierdere_{Jucător_1} | $$+        /* DO */ 
 +        Solution ​Solution.push(value) 
 +        ​Domain ​Domain.erase(value) 
 +         
 +        /* RECURSION */ 
 +        back(Domain,​ Solution) 
 +         
 +        /* UNDO */ 
 +        Solution = Solution.pop() 
 +        Domain = Domain.insert(value) 
 +</​code>​
  
-=== Reprezentarea spațiului soluțiilor ===+{{ :​pa:​laboratoare:​perm_ref_viz.png?​700 |}}
  
 +===== Exemple clasice =====
 +Ne vom ocupa în continuare de următoarele probleme:
 +  * Permutări
 +  * Combinări
 +  * Aranjamente
 +  * Submulțimi
 +  * Generare de șiruri
 +  * Problema damelor
 +  * Problema șoricelului
 +  * Tic-Tac-Toe
 +  * Sudoku
 +  * Ultimate Tic-Tac-Toe
  
-În general spațiul ​soluțiilor ​pentru un joc în doi de tip zero-sum se reprezintă ca un **arbore**, fiecărui nod fiindu-i asociată o stare a jocului în desfășurare (game state).  +<​note>​ 
-De exemplul, putem considera jocul de X și O ce are următorul arbore (parțial) de soluții. Acesta corespunde primelor mutări ale lui X, respectiv O: +Sudoku și Ultimate Tic-Tac-Toe sunt probleme foarte grele. ​În general ​nu putem explora tot spațiul ​stărilor ​pentru un input arbitrar dat
 +</​note>​
  
-{{ :​pa:​laboratoare:​5.1_cr.png |}}+==== Permutări ====
  
-**Metodele de reprezentare a arborelui** variază în funcție de paradigma de programare aleasă, de limbaj, precum și de gradul de optimizare avut în vedere.+=== Enunț ===
  
-==== Implementare ====+Se dă un număr N. Să se genereze toate permutările mulțimii formate din toate numerele de la 1 la N.
  
-Având noțiunile de bază asupra strategiei celor doi jucătoriprecum și a reprezentării spațiului soluțiilor problemeio primă implementare a algoritmului Minimax ar folosi două funcții maxi() și mini()care ambele calculează cel mai bun scor pe care îl poate obține jucătorul menționat. Intuitivcele funcții au implementare aproape identicăastfel că dorim o organizam a codului fără porțiuni duplicate. De aceea singura variantă de implementare pe care o recomandămeste varianta Negamax: ​+=== Exemple === 
 + 
 +<spoiler Exemplu 1> 
 + 
 +N = 3 => M = {123} 
 + 
 +Solutie: 
 +  * {12, 3} 
 +  * {1, 3, 2
 +  * {213} 
 +  * {2, 3, 1} 
 +  * {3, 1, 2} 
 +  * {3, 2, 1} 
 + 
 +</​spoiler>​ 
 + 
 +=== Soluții === 
 + 
 +=== Backtracking (algoritmul în cazul general) === 
 +<spoiler Implementare>​
  
-<spoiler Pseudocod Negamax> 
 <code cpp> <code cpp>
-// compute the state score for current player +/* deoarece numerele sunt sterse din domeniu odata ce sunt folosite, soluția generata este garantata 
-int evaluate(state, player);+sa nu contina duplicate. Astfel, atunci cand domeniul ajunge vid, soluția este intotdeauna corecta *
 +bool check(std::​vector<​int>​ solution
 +    return true; 
 +}
  
-// apply the move on the current stateold_state -new_state +void printSolution(std:​:vector<​intsolution) { 
-void apply_move(state, move); +    ​for ​(auto &s : solution{ 
-// undo the move and restore previous statenew_state -> old_state +        std::cout << s << " "; 
-void undo_move(state,​ move);+    } 
 +    std::cout << "​\n"​; 
 +}
  
-// check if any player won the game +void back(std::​vector<​int>​ domain, std::​vector<​int>​ solution) { 
-bool game_over(state);+    ​/* dupa ce am folosit toate elementele din domeniu putem verifica daca 
 +    am gasit o solutie */ 
 +    ​if (domain.size() == 0) { 
 +        if(check(solution)) { 
 +            printSolution(solution); 
 +        } 
 +        return; 
 +    }
  
-// return the opponent ​for current player +    ​/* incercam sa adaugam in solutie toate valorile din domeniu, pe rand */ 
-Player get_opponent(player);+    ​for (unsigned int i = 0; i < domain.size();​ ++i) { 
 +        /* cream o solutie noua si un domeniu nou care sunt identice cu cele 
 +        de la pasul curent */ 
 +        std::​vector<​int>​ newSolution(solution),​ newDomain(domain);
  
-// compute the best score that player can get, +        ​/* adaugam in noua solutie elementul ales din domeniu *
-// considering that the opponent also has an optimal strategy +        ​newSolution.push_back(domain[i]); 
-int negamax(State& state, int depth, Player player{ +        /* stergem elementul ales din noul domeniu *
-    // STEP 1: game over or maximum recursion depth was reached +        ​newDomain.erase(newDomain.begin() + i)
-    ​if ​(game_over() || depth == 0{ + 
-       return evaluate(stateplayer);+        /* apelam recursiv backtracking pe noul domeniu si noua solutie */ 
 +        back(newDomainnewSolution);
     }     }
 +}
  
-    ​// STEP 2: generate all possible moves for player +int main() { 
-    ​all_moves = get_all_moves(stateplayer);+    ​/* dupa ce am citit n initializam domeniul cu n elemente, numerele de la 1 la n, 
 +    iar solutia este vida initial *
 +    ​std::​vector<​int>​ domain(n)solution; 
 +    for (int i = 0; i < n; ++i
 +        domain[i] = i + 1; 
 +    }
  
 +    /* apelam backtracking pe domeniul nostru, cautand solutia in vectorul solution */
 +    back(domain,​ solution);
 +}
 +</​code>​
  
-    // STEP 3: try to apply each move - compute best score +Apelarea inițială ​(din "​main"​se face astfel"back(domainsolution);", unde domain reprezintă un vector cu elementele de la 1 la N, iar solution este un vector gol.
-    int best_score = -oo; +
-    for (move : all_moves+
-        // STEP 3.1do move +
-        apply_move(statemove);+
  
-        ​// STEP 3.2play for the opponent +<note important>​ 
-        int score -negamax(statedepth - 1get_opponent(player)); +Nu este indicată implementarea backtracking-ului astfel deoarece este foarte costisitor din punct de vedere al memoriei(se creează noi domenii și soluții la fiecare pas). 
-        // opponent allows player to obtain this score if player will do current move+</note> 
-        // player chooses this move only if it has a better score+ 
-        ​if ​(score best_score) { +</spoiler>​ 
-            ​best_score = score;+ 
 +== Complexitate == 
 + 
 +Soluția va avea următoarele complexitati
 + 
 +  * complexitate temporala : $T(n)=O(n * n!)$ 
 +     * explicație : Complexitatea generarii permutarilor$O(n!)$se înmultește cu complexitatea copierii vectorilor soluție si domeniu si a stergerii elementelor din domeniu, $O(n)
 +  * complexitate spatiala : $S(n)=O(n^2)$ 
 +     * explicație : Fiecare nivel de recursivitate are propria lui copie a soluției și a domeniuluiSunt n nivele de recursivitate,​ deci complexitatea spatială este $O(n * n) = O(n^2)$ 
 + 
 + 
 +=== Backtracking (date transmise prin referinta) === 
 +<spoiler Implementare>​ 
 + 
 +<code cpp> 
 + 
 +/* deoarece numerele sunt sterse din domeniu odata ce sunt folosite, soluția generata este garantata sa nu contina duplicateAstfel, atunci cand domeniul ajunge vid, soluția este intotdeauna corecta */ 
 +bool check(std::​vector<​intsolution) { 
 +    ​return true; 
 +
 + 
 +void printSolution(std::​vector<​int>​ &​solution) { 
 +    for (int s : solution) { 
 +        std::cout << s << " "; 
 +    } 
 +    std::cout << "​\n";​ 
 +}
  
-            // [optional]the best move can be saved +void back(std::​vector<​int>​ &​domain,​ std::​vector<​int>​ &​solution) { 
-            // best_move ​move;+    /* dupa ce am folosit toate elementele din domeniu putem verifica daca 
 +    am gasit o solutie */ 
 +    if (domain.size() ​== 0) { 
 +        if(check(solution)) { 
 +            printSolution(solution);
         }         }
 +        return;
 +    }
 +
 +    /* incercam sa adaugam in solutie toate valorile din domeniu, pe rand */
 +    for (unsigned int i = 0; i < domain.size();​ ++i) {
 +        /* retinem valoarea pe care o scoatem din domeniu ca sa o readaugam dupa
 +        apelarea recursiva a backtracking-ului */
 +        int tmp = domain[i];
 +
 +        /* adaug elementul curent la potentiala solutie */
 +        solution.push_back(domain[i]);​
 +        /* sterg elementul curent din domeniu ca sa il pot pasa prin referinta
 +        si sa nu fie nevoie sa creez alt domeniu */
 +        domain.erase(domain.begin() + i);
 +
 +        /* apelez recursiv backtracking pe domeniul si solutia modificate */
 +        back(domain,​ solution);
  
-        // STEP 3.3: undo move +        /* refac domeniul si solutia la modul in care aratau inainte de apelarea 
-        ​undo_move(statemove);+        recursiva a backtracking-ului,​ adica readaug elementul eliminat in 
 +        domeniu si il sterg din solutie */ 
 +        domain.insert(domain.begin() + i, tmp); 
 +        ​solution.pop_back(); 
 +    } 
 +
 + 
 +int main() { 
 +    /* dupa ce am citit n initializam domeniul cu n elementenumerele de la 1 la n, 
 +    iar solutia este vida initial */ 
 +    std::​vector<​int>​ domain(n), solution; 
 +    for (int i = 0; i < n; ++i) { 
 +        domain[i] = i + 1;
     }     }
  
-    // STEP 4: return best allowed score +    /* apelam backtracking pe domeniul nostru, cautand solutia in vectorul solution *
-    ​// [optional] also return the best move +    ​back(domain,​ solution);
-    return best_score;+
 } }
 +</​code>​
  
 +Apelarea initiala (din "int main") se face astfel: "​back(domain,​ solution);",​ unde domain reprezinta un vector cu elementele de la 1 la N, iar solution este un vector gol.
 +</​spoiler>​
 +
 +
 +== Complexitate ==
 +
 +Soluția va avea următoarele complexități:​
 +
 +  * complexitate temporală : $T(n)=O(n * n!)$
 +    * explicație : Complexitatea generării permutărilor,​ $O(n!)$, se înmulțește cu complexitatea ștergerii elementelor din domeniu, $O(n)$
 +  * complexitate spatială : $S(n)=O(n)$
 +    * explicație : Spre deosebire de solutia anterioară,​ toate nivelele de recursivitate folosesc aceeași soluție și același domeniu. Complexitatea spatială este astfel redusă la $O(n)$
 +<​note>​
 +
 +Această abordare este mai eficientă decât cea generală, deoarece se evită folosirea memoriei auxiliare.
 +
 +</​note>​
 +
 +
 +=== Backtracking (tăierea ramurilor nefolositoare) ===
 +<spoiler Implementare>​
 +
 +<code cpp>
 +
 +bool check(std::​vector<​int>​ &​solution) {
 +    return true;
 +}
 +
 +void printSolution(std::​vector<​int>​ &​solution) {
 +    for (auto s : solution) {
 +        std::cout << s << " ";
 +    }
 +    std::cout << "​\n";​
 +}
 +
 +void back(int step, int stop, std::​vector<​int>​ &​domain,​
 +        std::​vector<​int>​ &​solution,​ std::​unordered_set<​int>​ &​visited) {
 +    ​
 +    /* vom verifica o solutie atunci cand am adaugat deja N elemente in solutie,
 +    adica step == stop */
 +    if (step == stop) {
 +        /* deoarece am avut grija sa nu se adauge duplicate, "​check()"​ va returna
 +        intotdeauna "​true"​ */
 +        if(check(solution)) {
 +            printSolution(solution);​
 +        }
 +        return;
 +    }
 +
 +    /* Adaugam in solutie fiecare element din domeniu care *NU* a fost vizitat
 +    deja renuntand astfel la nevoia de a verifica duplicatele la final prin
 +    functia "​check()"​ */
 +    for (unsigned int i = 0; i < domain.size();​ ++i) {
 +        /* folosim elementul doar daca nu e vizitat inca */
 +        if (visited.find(domain[i]) == visited.end()) {
 +            /* il marcam ca vizitat si taiem eventuale expansiuni nefolositoare
 +            viitoare (ex: daca il adaug in solutie pe 3 nu voi mai avea
 +            niciodata nevoie sa il mai adaug pe 3 in continuare) */
 +            visited.insert(domain[i]);​
 +
 +            /* adaugam elementul curent in solutie pe pozitia pasului curent
 +            (step) */
 +            solution[step] = domain[i];
 +
 +            /* apelam recursiv backtracking pentru pasul urmator */
 +            back(step + 1, stop, domain, solution, visited);
 +
 +            /* stergem vizitarea elementului curent (ex: pentru N = 3, dupa ce
 +            la pasul "step = 0" l-am pus pe 1 pe prima pozitie in solutie si
 +            am continuat recursiv pana am ajuns la solutiile {1, 2, 3} si 
 +            {1, 3, 2}, ne dorim sa il punem pe 2 pe prima pozitie in solutie si
 +            sa continuam recursiv pentru a ajunge la solutiile {2, 1, 3} etc.) */
 +            visited.erase(domain[i]);​
 +        }
 +    }
 +}
 +
 +int main() {
 +    /* dupa ce am citit n initializam domeniul cu n elemente, numerele de la 1 la n,
 +    iar solutia este initializata cu un vector de n elemente (deoarece o permutare
 +    contine n elemente) */
 +    std::​vector<​int>​ domain(n), solution(n);​
 +    std::​unordered_set<​int>​ visited;
 +    for (int i = 0; i < n; ++i) {
 +        domain[i] = i + 1;
 +    }
 +
 +    /* apelam back cu step = 0 (atatea elemente avem adaugate in solutie),
 +    stop = n (stim ca vrem sa adaugam n elemente in solutie pentru ca o
 +    permutare e alcatuita din n elemente), domain este vectorul de valori
 +    posibile, solution este vectorul care simuleaza stiva pe care o vom
 +    umple, visited este un unordered_set (initial gol) in care retinem daca
 +    un element din domeniu se afla deja in solutia curenta la un anumit pas */
 +    back(0, n, domain, solution, visited);
 +}
 </​code>​ </​code>​
 +
 +Apelarea inițială (din "​main"​) se face astfel: "​back(0,​ n, domain, solution, visited);",​ unde domain reprezintă un vector cu elementele de la 1 la N, iar solution este un vector de n elemente, 0 este pasul curent, n este pasul la care dorim să ne oprim, iar visited este map-ul care ne permite să ținem cont de ce elemente au fost vizitate sau nu.
 +
 </​spoiler>​ </​spoiler>​
  
-=== De ce este nevoie de utilizarea unei adâncimi maxime? ===+== Complexitate ​==
  
-Datorită **spațiului de soluții mare**, de multe ori copleșitor ca volum de date de analizat, o inspectare completă a acestuia nu este fezabilă șdevine impracticabilă din punctul de vedere al timpului consumat sau chiar a memoriei alocate (se vor discuta aceste aspecte în paragraful legat de complexitate). ​+Soluția va avea următoarele complexitați:
  
-Astfel, de cele mai multe ori este preferată o abordare care parcurge arborele numai până la o anumită **adâncime maximă („depth”)**. Aceasta abordare permite examinarea arborelui destul de mult pentru a putea lua decizii minimalist coerente in deșfăsurarea jocului+  * complexitate temporală : $T(n)=O(n * n!)=O(n!)$ 
 +    * explicație : Complexitatea generării permutărilor, O(n!), se înmulțește cu complexitatea iterării prin domeniu, $O(n)$ 
 +  ​complexitate spatială : $S(n)=O(n)$ 
 +    ​explicație : Toate nivelele ​de recursivitate folosesc aceeași soluție și același domeniu.
  
-Totuși, **dezavantajul major** este că pe termen lung se poate dovedi ca decizia luată la adâncimea depth nu este global favorabilă jucătorului în cauză (s-a ales o valoare maxim local, iar dacă s-ar fi continuat în arborele de explorare s-ar fi constatat că este o decizie ce avantajează celălălt jucător). ​+<​note>​
  
-De asemenea, se observă recursivitatea indirectă. Prin convenție acceptăm ca **începutul algoritmului** ​să fie cu jucătorul max. Astfelse analizează succesiv diferite stări ale jocului din punctul de vedere al celor doi jucatori până la adâncimea depth. Rezultatul întors este scorul final al mișcării celei mai bune pentru un jucător din perspectiva următoarelor depth mutări în joc.+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.
  
-=== Exemplu grafic ===+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. 
 +</​note>​
  
-{{pa:​new_pa:​minimax.jpg?​700}} 
-Se observă că arborele se completează prin parcugere în adâncime (stanga-dreapta,​ sus-jos). 
-* Pe nivel Max/Maxi se alege maximul dintre valorile primite de la copii. 
-* Pe nivel Min/Mini se alege minimul dintre valorile primite de la copii. 
-Obervație: Toate nodurile sunt evaluate. 
  
-{{pa:​new_pa:​negamax.jpg?​700}} +==== Combinări ====
-Aceleași explicații ca la Minimax, cu câteva observații adiționale:​ +
-* NU mai este nevoie să numerotăm nivelele. Pe fiecare nivel, jucătorul curent alege maximul dintre scorurile negate ale adeversarului (venite din copii). +
-==== Optimizări====+
  
-=== Alpha-beta pruning ​===+=== Enunț ​===
  
-Până acum s-a discutat despre algoritmii Minimax / NegamaxAceștia sunt algoritmi exhaustivi (**exhausting search algorithms**). Cu alte cuvinte, ei găsesc soluția optima examinând întreg spațiul ​de soluții al problemei. Acest mod de abordare este extrem de ineficient în ceea ce privește efortul de calcul necesarmai ales considerând că extrem de multe stări de joc inutile sunt explorate (este vorba de acele stări care nu pot fi atinse datorită încălcării principului că fiecare jucător joacă optim la fiecare rundă).+Se dau numerele N si KSă se genereze toate combinările mulțimii formate din toate numerele ​de la 1 la Nluate câte K.
  
-O îmbunățațire substanțială a Minimax/​Negamax este **Alpha-beta pruning** (**tăiere alfa-beta**). Acest algoritm încearcă să optimizeze Minimax/​Negamax profitând de o observație importantă:​ **pe parcursul examinării arborelui de soluții se pot elimina întregi subarbori, corespunzători unei mișcări m, dacă pe parcursul analizei găsim că mișcarea m este mai slabă calitativ decât cea mai bună mișcare curentă.**+=== Exemple ===
  
-Astfel, considerăm că pornim cu o primă mișcare M1. După ce analizăm această mișcare în totalitate și îi atribuim un scor, continuăm să analizăm mișcarea M2. Dacă în analiza ulterioară găsim că adversarul are cel puțin o mișcare care transformă M2 într-o mișcare mai slabă decât M1 atunci orice alte variante ce corespund mișcării M2 (subarbori) nu mai trebuie analizate.+<spoiler Exemplu 1>
  
-<spoiler De ce?> +N = 4, K = 2 =M = {1, 2, 3, 4}
-De ce? Pentru că știm că există **cel puțin** o variantă în care adversarul obține un câștig mai bun decât dacă am fi jucat mișcarea M1. +
  
-Nu conteaza exact cât de slabă poate fi mișcarea M2 față de M1. O analiză amănunțită ar putea releva că poate fi și mai slabă decât am constatat inițialînsă acest lucru este irelevant. ​+Soluție: 
 +  * {1, 2} 
 +  * {1, 3} 
 +  * {1, 4} 
 +  * {2, 3} 
 +  * {2, 4} 
 +  * {34}
  
-De ce însă ignorăm întregi subarbori și mișcări potențial bune numai pentru o mișcare slabă găsită? Pentru că, în conformitate cu **principiul de maximizare al câștigului** folosit de fiecare jucator, adversarul va alege exact acea mișcare ce îi va da un câștig maximal. Dacă există o variantă și mai bună pentru el este irelevant, deoarece noi suntem interesați dacă cea mai slabă mișcare bună a lui este mai bună decât mișcarea noastră curent analizată. 
 </​spoiler>​ </​spoiler>​
  
 +=== Soluții ===
 +
 +===Backtracking (tăierea ramurilor nefolositoare) ===
 +<spoiler Implementare>​
 +<code cpp>
 +
 +bool check(std::​vector<​int>​ &​solution) {
 +    return true;
 +}
 +
 +void printSolution(std::​vector<​int>​ &​solution,​ std::​vector<​int>​ &​domain,​ int stop) {
 +    for (unsigned i = 0; i < stop; ++i) {
 +        std::cout << domain[solution[i]] << " ";
 +    }
 +    std::cout << "​\n";​
 +}
 +
 +void back(int step, int stop, std::​vector<​int>​ &​domain,​
 +        std::​vector<​int>​ &​solution) {
 +    /* vom verifica o solutie atunci cand am adaugat deja K elemente in solutie,
 +    adica step == stop */
 +    if (step == stop) {
 +        /* deoarece am avut grija sa se adauge elementele doar in ordine
 +        crescatoare,​ "​check()"​ va returna intotdeauna "​true"​ */
 +        if(check(solution)) {
 +            printSolution(solution,​ domain, stop);
 +        }
 +        return;
 +    }
 +
 +    /* daca este primul pas, alegem fiecare element din domeniu ca potential
 +    candidat pentru prima pozitie in solutie; altfel, pentru a elimina ramurile
 +    in care de exemplu {2, 1} se va genera dupa ce s-a generat {1, 2} (adica
 +    ar fi duplicat), vom folosi doar elementele din domeniu care sunt mai mari
 +    decat ultimul element adaugat in solutie (solution[step - 1]) */
 +    unsigned i = step > 0 ? solution[step - 1] + 1 : 0;
 +    for (; i < domain.size();​ ++i) {
 +        solution[step] = i;
 +        back(step + 1, stop, domain, solution);
 +    }
 +}
 +
 +int main() {
 +    /* dupa ce citim n si k initializam domeniul cu valorile de la 1 la n,
 +    iar solutia este initializata cu un vector de k elemente (fiindca o
 +    combinare de "n luate cate k" are k elemente) */
 +    std::​vector<​int>​ domain(n), solution(k);​
 +    for (int i = 0; i < n; ++i) {
 +        domain[i] = i + 1;
 +    }
 +
 +    back(0, k, domain, solution);
 +}
 +</​code>​
 +
 +În această soluție ne bazăm pe faptul că toate combinările pot fi generate în
 +ordine crescătoare,​ adică soluția {1, 3, 4} e echivalentă cu {4, 1, 3}.
  
 <​note>​ <​note>​
-Un video cu un exemplu detaliat și foarte bine explicat se găsește în tutorialul recomandat de pe YouTube ​(de la minutul 21:30 la 30:30).+ 
 +Această soluție este optimă întrucât toate soluțiile generate sunt corecte ​(de aceea funcția check întoarce true). Deoarece problema cere obținerea tuturor combinărilor,​ aceasta complexitate nu poate fi mai mică de Combinări(n,​ k).
  
 </​note>​ </​note>​
  
-=== Implementare === +</​spoiler>​
-În continuare prezentăm o implementare conceptuală a Alpha-beta pentru varianta Negamax:+
  
 +=== Complexitate ===
 +
 +Soluția va avea următoarele complexități:​
 +
 +  * complexitate temporală : $T(n)=O(Combinari(n,​ k))$
 +  * complexitate spatială : $S(n)=O(n+k)=O(n)$
 +    * explicație : $k <= n$, deci $O(n+k)=O(n)$
 +
 +==== Problema șoricelului ====
 +
 +=== Enunț ===
 +
 +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.
 +
 +=== Exemple ===
 +
 +<spoiler Exemplu 1>
 +
 +  * 2
 +  * 0 1
 +  * 0 0
 +
 +Există 1 drum posibil:
 +
 +  * (0, 0)->(1, 0)->(1, 1)
 +
 +</​spoiler>​
 +
 +<spoiler Exemplu 2>
 +
 +  * 3
 +  * 0 0 0
 +  * 0 1 0
 +  * 0 0 0
 +
 +Există 2 drumuri posibile:
 +
 +  * (0,​0)->​(0,​1)->​(0,​2)->​(1,​2)->​(2,​2)
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(2,​1)->​(2,​2)
 +
 +</​spoiler>​
 +
 +<spoiler Exemplu 3>
 +
 +  * 4
 +  * 0 0 0 1
 +  * 0 1 1 0
 +  * 0 0 0 0
 +  * 0 0 0 0
 +
 +Există 4 drumuri posibile:
 +
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(2,​1)->​(2,​2)->​(2,​3)->​(3,​3)
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(2,​1)->​(2,​2)->​(3,​2)->​(3,​3)
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(2,​1)->​(3,​1)->​(3,​2)->​(3,​3)
 +  * (0,​0)->​(1,​0)->​(2,​0)->​(3,​0)->​(3,​1)->​(3,​2)->​(3,​3)
 +
 +</​spoiler>​
 +
 +=== Soluții ===
 +
 +=== Backtracking (transmitere prin referință) ===
 +<spoiler Implementare>​
  
-<spoiler Pseudocod Negamax with Alpha-beta>​ 
 <code cpp> <code cpp>
-// compute the state score for current player 
-int evaluate(state,​ player); 
  
-// apply the move on the current stateold_state -new_state +bool check(std::​vector<​std::​pair<​int,​ int> &​solution,​ int walls[100][100]) { 
-void apply_move(state, move); +    for (unsigned i = 0; i < solution.size() - 1++i) { 
-// undo the move and restore previous state: new_state -> old_state +        /* line_prev si col_prev reprezinta celula in care se afla soricelul la 
-void undo_move(state,​ move);+        pasul i; line_next si col_next reprezinta celula in care se afla 
 +        la pasul i + 1; trebuie sa fim siguri ca soricelul nu a ajuns pe zid 
 +        si ca urmatoarea celula este sub sau in dreapta celulei curente *
 +        int line_prev = solution[i].first;​ 
 +        int line_next = solution[i + 1].first; 
 +        int col_prev = solution[i].second;​ 
 +        int col_next = solution[i + 1].second;
  
-// check if any player won the game +        ​/* walls[x][y] == 1 inseamna ca este zid pe linia x, coloana y */ 
-bool game_over(state);+        ​if (walls[line_prev][col_prev] == 1 || 
 +                !((line_next == line_prev + 1 && col_next == col_prev|| 
 +                (line_next == line_prev && col_next == col_prev + 1))) { 
 +            return false; 
 +        } 
 +    }
  
-// return ​the opponent for current player +    ​return ​true; 
-Player get_opponent(player);+}
  
-// compute the best score that player can get, +void printSolution(std::​vector<​std::​pair<​int, int> > &​solution) { 
-// considering that the opponent also has an optimal strategy +    ​for (std::pair<​int,​ int> s : solution) { 
-int alphabeta_negamax(State& state, ​int depth, Player player, int alpha, int beta) { +        ​std::​cout << "(" << s.first << "," << s.second << ")->";
-    ​// STEP 1game over or maximum recursion depth was reached +
-    if (game_over() || depth == 0) { +
-       return evaluate(stateplayer);+
     }     }
 +    std::cout << "​\n";​
 +}
  
-    // STEP 2generate all possible moves for player +void back(std::​vector<​std::​pair<​int,​ int> > &​domain,​ int walls[100][100],​ 
-    // Notesort moves descending by score (if possiblefor maximizing the number of cut-off actions +        std::​vector<​std::​pair<​int,​ int> > &​solution,​ int max_iter{ 
-    // (or generete the moves already sorted by a custom criterion) +    /* daca am facut "​max_iter"​ pasi ma opresc si verific daca este corecta 
-    ​all_moves ​get_all_moves(stateplayer);+    solutia *
 +    ​if (solution.size() ​== max_iter) { 
 +        if(check(solutionwalls)) { 
 +            printSolution(solution); 
 +        } 
 +        return; 
 +    }
  
 +    /* avand domeniul initializat cu toate celulele din matrice, incercam sa
 +    adaugam oricare dintre aceste celule la solutie, verificand la final daca
 +    solutia este buna */
 +    for (unsigned int i = 0; i < domain.size();​ ++i) {
 +        /* pastram elementul curent pentru a-l readauga in domeniu dupa
 +        apelarea recursiva */
 +        std::​pair<​int,​ int> tmp = domain[i];
  
-    ​// STEP 3: try to apply each move - compute best score +        ​/* adaugam elementul curent la solutia candidat *
-    int best_score = -oo; +        ​solution.push_back(domain[i]); 
-    for (move : all_moves{ +        /* stergem elementul curent din domeniu *
-        // STEP 3.1: do move +        ​domain.erase(domain.begin() + i);
-        ​apply_move(state, move);+
  
-        // STEP 3.2: play for the opponent +        /* apelam recursiv backtracking *
-        ​int score = -alphabeta_negamax(statedepth - 1get_opponent(player)-beta, -alpha)+        ​back(domainwallssolutionmax_iter);
-        // opponent allows player to obtain this score if player will do current move. +
-        // player chooses this move only if it has a better score. +
-        if (score > best_score) { +
-            best_score = score;+
  
-            ​// [optional]the best move can be saved +        ​/* adaugam elementul sters din domeniu inapoi */ 
-            // best_move ​move;+        domain.insert(domain.begin() + i, tmp); 
 +        /* stergem elementul curent din solutia candidat pentru a o forma pe 
 +        urmatoarea */ 
 +        solution.pop_back();​ 
 +    } 
 +
 + 
 +int main() { 
 +    /* initializam domeniul si solutia ca vectori de perechi de int-uri; 
 +    domeniul va contine initial toate perechile de indici posibile din 
 +    matrice ((0, 0), (0, 1) ... (n - 1, n - 1)), iar solutia va fi initial 
 +    vida */ 
 +    std::​vector<​std::​pair<​int,​ int> > domain, solution; 
 + 
 +    fin >> n; 
 +    for (i = 0; i < n; ++i) { 
 +        for (j = 0; j < n; ++j) { 
 +            /* walls[i][j] ​== 1 daca pe pozitia (i, j) este zid; 0 altfel */ 
 +            fin >> walls[i][j];​ 
 +            domain.push_back({i,​ j});
         }         }
 +    }
  
-        ​// STEP 3.3update alpha (found a better move?+    ​/* apelam back cu domeniul format initial, cu matricea de ziduri, cu 
-        ​if (best_score ​alpha) { +    solutia vida si cu numarul maxim de iteratii = 2 * n - 1 pentru ca 
-            ​alpha ​best_score;+    mergand doar in dreapta si in jos, in 2 * n - 1 pasi va ajunge din 
 +    (0, 0) in (n - 1, n - 1) */ 
 +    back(domain,​ walls, solution, 2 * n - 1); 
 +
 +</​code>​ 
 + 
 +Apelarea initiala (din "int main") se face astfel: "​back(domain,​ walls, solution, 2 * n - 1);", unde domain reprezinta un vector cu perechi in care sunt toate celulele matricii, solution este un vector gol, walls este matricea care ne arata daca este zid sau nu pe o anumita pozitie, iar 2 * n - 1 e numarul e pasi in care ar trebui ca soricelul sa ajunga la branza pe oriunde ar merge. 
 + 
 +</​spoiler>​ 
 +=== Complexitate === 
 + 
 +Soluția va avea următoarele complexități:​ 
 + 
 +  * complexitate temporală ​$T(n)=O(Aranjamente(n^2,​ 2n - 1))$ 
 +    * explicație:​ Initial in domeniu avem $n^2$ valori. Noi dorim sa generam toate submultimile ordonate de cate $2n-1$ elemente. Acestea sunt tocmai aranjamentele de $n^2$ luate cate $2n-1$. 
 +  * complexitate spatială : $S(n)=O(n^2)$ 
 +    * explicație:​ Trebuie să stocăm informație despre drum, care are $2n-1$ celule; stocăm domeniul care are $n^2$ elemente 
 + 
 +=== Backtracking (tăierea ramurilor nefolositoare) === 
 +<spoiler Implementare>​ 
 + 
 +<code cpp> 
 + 
 +bool check(std::​vector<​std::​pair<​int,​ int> > &​solution) { 
 +    return true; 
 +
 + 
 +void printSolution(std::​vector<​std::​pair<​int,​ int> > &​solution) { 
 +    for (std::​pair<​int,​ int> s : solution) { 
 +        ​std::cout << "(" << s.first << ","​ << s.second << ")->"; 
 +    } 
 +    std::cout << "​\n";​ 
 +
 + 
 +void back(int step, int stop, int walls[100][100],​ 
 +        std::​vector<​std::​pair<​int,​ int> > &​solution,​ int line_moves[2],​ 
 +        int col_moves[2]) { 
 +    /* ne oprim dupa ce am ajuns la pasul "​stop"​ si verificam daca solutia este 
 +    corecta */ 
 +    if (step == stop) { 
 +        /* deoarece am eliminat ramurile nefolositoare am ajuns la o solutie care 
 +        sigur este corecta */ 
 +        if(check(solution)) { 
 +            printSolution(solution);
         }         }
 +        return;
 +    }
 +
 +    /* daca este primul pas stiu ca soricelul este in pozitia (0, 0) */
 +    if (step == 0) {
 +        /* adaugam (0, 0) la solutia candidat */
 +        solution.push_back({0,​ 0});
 +
 +        /* apelam backtracking recursiv la pasul urmator */
 +        back(step + 1, stop, walls, solution, line_moves, col_moves);
 +
 +        /* scoatem (0, 0) din solutie */
 +        solution.pop_back();​
 +        return;
 +    }
 +
 +    /* sunt doar doua mutari pe care le pot face intr-un pas: dreapta si jos;
 +    acestea sunt encodate prin vectorii de directii line_moves[2] = {0, 1} si
 +    col_moves[2] = {1, 0} care reprezinta la indicele 0 miscarea in dreapta, iar
 +    la indicele 1 miscarea in jos */
 +    for (unsigned int i = 0; i < 2; ++i) {
 +        /* cream noua linie si noua coloana cu ajutorul vectorilor de directii */
 +        int new_line = solution.back().first + line_moves[i];​
 +        int new_col = solution.back().second + col_moves[i];​
 +        int n = (stop + 1) / 2;
 +
 +        /* daca linia si coloana sunt valide (nu ies din matrice) si nu este
 +        zid pe pozitia lor, putem continua pe acea celula */
 +        if (new_line < n && new_col < n && walls[new_line][new_col] == 0) {
 +            /* adaugam noua celula in solutia candidat;
 +            NOTE: {new_line, new_col} este echivalent cu
 +            std::​pair<​int,​ int>​(new_line,​ new_col) si se numeste "​initializer
 +            list", feature in C++11 */
 +            solution.push_back({new_line,​ new_col});
 +
 +            /* apelam backtracking recursiv la pasul urmator */
 +            back(step + 1, stop, walls, solution, line_moves, col_moves);
  
-        // STEP 3.4: cut-off +            ​/* scoatem celula adaugata din solutie ​*/ 
-        // * already found the best possible score (alpha == beta) +            ​solution.pop_back();
-        // OR +
-        // on this branch we can obtain a score (alpha) better than the +
-        ​// maximum allowed score by the opponent => drop the branch because +
-        // opponent also plays optimal +
-        if (alpha >= beta+
-            break+
         }         }
 +    }
 +}
  
-        ​// STEP 3.4undo move +int main() { 
-        ​undo_move(state, move);+    ​/* initializam solutia ca vector de perechi de int-uri */ 
 +    std::​vector<​std::​pair<​int,​ int> > solution; 
 + 
 +    fin >> n; 
 +    for (i = 0; i < n; ++i) { 
 +        ​for (j = 0; j < n; ++j
 +            /* citim matricea zidurilor1 pentru zid, 0 altfel */ 
 +            fin >> walls[i][j];​ 
 +        }
     }     }
  
-    // STEP 4: return best allowed score +    /* apelam back cu step = 0, stop = 2 * n - 1 deoarece in 2 * n - 1 
-    ​// [optionalalso return the best move +    ​pasi soricelul va ajunge la branza, vectorul de ziduri, vectorul in 
-    ​return best_score;+    care vom stoca solutia, vectorii de directii line_moves[2= {0, 1} si 
 +    col_moves[2] = {1, 0}; nu avem nevoie de domeniu deoarece folosind 
 +    vectorii de directii vom sti din ultima pozitie pusa in solutie cele 
 +    doua solutii in care putem merge, astfel domeniul nostru va fi alcatuit 
 +    din doua solutii la fiecare pas (daca ultima pozitie din solutie a fost 
 +    (5, 7) => domeniul pasului curent = {(5 + 0, 7 + 1) si (5 + 1, 7 + 0)} 
 +    care este egal cu {(5, 8), (6, 7)}. */ 
 +    ​back(0, 2 * n - 1, walls, solution, line_moves, col_moves);
 } }
  
 </​code>​ </​code>​
 +
 +Pentru aceasta abordare la fiecare pas de backtracking vom merge doar in doua directii, in loc sa mergem in oricare celula din matrice, lucru care imbunatateste semnificativ complexitatea temporala.
 +
 </​spoiler>​ </​spoiler>​
  
 +=== Complexitate ===
  
 +Soluția va avea urmatoarele complexitati:​
 +
 +  * complexitate temporală : $T(n)=O(2^{2n})$
 +      * explicație:​ avem de urmat un șir de $2n-1$ mutari, iar la fiecare pas avem 2 variante posibile
 +  * complexitate spatiala : $S(n)=O(n)$
 +      * explicație:​ stocam maximum $2n-1$ căsuțe
 +
 +===== Exerciții =====
 <​note>​ <​note>​
-O observație foarte importantă se poate face analizând **modul de funcționare** al acestui algoritmeste extrem de importantă **ordonarea mișcărilor după valoarea câștigului**. +Scheletul de laborator se găsește pe pagina [[https://​github.com/​acs-pa/​pa-lab/​tree/​main/​skel/​lab05|pa-lab::​skel/​lab05]]. 
 +</​note>​ 
 +==== Aranjamente ==== 
 +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}.
  
-În **cazul ideal** în care cea mai bună mișcare a jucătorului curent este analizată prima, toate celelalte mișcări, fiind mai slabe, vor fi eliminate din căutare timpuriu.+<spoiler Exemplu 1>
  
-În **cazul cel mai defavorabil** însăîn care mișcările sunt ordonate crescător după câștigul furnizatAlpha-beta are aceeași complexitate cu Minimax/ Negamaxîmbunătățirea fiind nulă.  ​+Fie N = 3K = 2 => M = {123}
  
-În **medie** se constată ​eficiență sporită in practică pentru Alpha-beta.+Soluție: 
 +  ​{1, 2} 
 +  ​{1, 3} 
 +  ​{2, 1} 
 +  ​{2, 3} 
 +  * {3, 1} 
 +  * {3, 2} 
 + 
 +</​spoiler>​ 
 + 
 +<​note>​ 
 +Se dorește ​complexitate $T(n, k) = A(n,k)$.
 </​note>​ </​note>​
  
-=== Exemplu grafic ===+<spoiler Hint> 
 +Folosiți-vă de problema **Permutări**. 
 +</​spoiler>​
  
-{{pa:​new_pa:​minimax_alphabeta.jpg?​700}} 
  
-=== Iterative deepening ===+<​note>​ 
 +Soluțiile se vor genera în ordine lexico-grafica!
  
-Pe scurt, iterative deepening reprezintă o strategie de optimizare ​timpului prin căutarea progresivă în adâncime. Uneori, există posibilitatea pentru anumite stări ​să nu fie explorate complet, iar în acest caz rezultatul este aproximat printr-o euristică. Tehnica presupune salvarea în memorie a rezultatelor neexplorate complet (toate sau cele mai "​promițătoare"​ stări), iar la următoarea rundă se vor refolosi o parte din aceste rezultate (cele care mai sunt posibile din noul moment al jocului). Un video cu un exemplu explicat se găsește pe link-ul [[https://​www.youtube.com/​watch?​v=Y85ECk_H3h4&​ab_channel=JohnLevine | youtube.com/iterative_deepening]]+Checkerul ​așteaptă să le stocați ​în această ordine. 
 +</note>
  
 +==== Submulțimi ====
 +Fie N un **număr natural strict pozitiv**. Se cere afișarea tuturor submulțimilor mulțimii {1, 2, ..., N}.
  
-==== Complexitate ====+<spoiler Exemplu 1>
  
-Pentru a vedea complexitatea algoritmilor prezentați anteriorse vor introduce câteva noțiuni:+Fie N = 4 => M = {12, 3, 4}
  
-  * **branch factor** : ** b ** = **numărul mediu de ramificări** pe care le are un nod neterminal (care nu e frunză) din ** arborele de soluții ** +Soluție: 
-  * **depth** ​**d ** = **adâncimea maximă ** pană la care se face căutarea în arborele de soluții +{} - mulțimea vidă 
-      * orice nod de adâncime d va fi considerat terminal+{1} 
 +{1, 2} 
 +{1, 2, 3} 
 +{1, 2, 3, 4} 
 +{1, 2, 4} 
 +{1, 3} 
 +{1, 3, 4} 
 +{1, 4} 
 +{2} 
 +{2, 3} 
 +{2, 3, 4} 
 +{2, 4} 
 +{3} 
 +{3, 4} 
 +{4}
  
-<​note>​  +</​spoiler>​ 
-Un arbore cu un branching factor **b**, care va fi examinat până la un nivel **d** va furniza ​$b^dnoduri frunze ce vor trebui procesate (excalculăm scorul pentru acele noduri)+ 
 + 
 +<​note>​ 
 +Se dorește o complexitate ​$T(n) = O(2^n)$. 
 +</​note>​ 
 + 
 +<spoiler Hint> 
 + 
 +Folosiți-vă de problema **Combinari**.
  
-<spoiler Explicație>​ 
-Nivelurile sunt notate cu $0, 1, 2, ..., d$ 
-  * nivel 0: $1$   nod (radacină) 
-  * nivel 1: $b$   ​noduri 
-  * nivel 2: $b^2$ noduri 
-  * nivel 3: $b^3$ noduri ​ 
-  * ... 
-  * nivel d: $b^d$ noduri 
 </​spoiler>​ </​spoiler>​
 +
 +<​note>​
 +Soluțiile se vor genera în ordine lexico-grafica!
 +
 +Checkerul așteaptă să le stocați în această ordine.
 </​note>​ </​note>​
  
 +==== Problema damelor ====
  
-  ​* **minimax/​negamax*+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ândpe 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.
-      * Un algoritm **Minimax / Negamax** clasiccare analizează toate stările posibileva avea complexitatea $O(b ^ d)$ - deci exponențială. +
  
-  * **alpha-beta** +<spoiler Exemplu ​1>
-      * Cât de bun este însa alpha-beta fața de un Minimax / Negamax naiv? Dupa cum s-a menționat anterior, în funcție de ordonarea mișcărilor ce vor fi evaluate putem avea un caz cel mai favorabil și un caz cel mai defavorabil. +
-      * ** best case **: mișcările sunt ordonate descrescător după câștig (deci ordonate optim), rezultă o complexitate +
-          * $O(b*1*b*1*b*1...de\ ​ d \ ori...b*1)$ pentru d par +
-          * $O(b*1*b*1*b*1...de \ d \ ori...b)$ pentru d impar +
-          * restrângând ambele expresii rezultă o complexitate $O(b ^ {\frac{d}{2}}) = O(\sqrt{b^d})$ +
-          * prin urmare, într-un caz ideal, algoritmul Alpha-beta poate explora de 2 ori mai multe nivele în arborele de soluții în același timp față de un algoritm Minimax/​Negamax naiv. +
-      * ** worst case**: mișcările sunt ordonate crescător după câștigul furnizat unui jucător, astfel fiind necesară o examinare a tuturor nodurilor pentru găsirea celei mai bune mișcări. +
-          *  în acest caz complexitatea devine egală cu cea a unui algoritm Minimax / Negamax naiv. +
-  +
-   +
  
-===== Exemple ===== +Fie N 5
-Dintre cele mai importante jocuri în care putem aplica direct strategia minimax, menționăm:​ +
-  * [[https://​en.wikipedia.org/​wiki/​Tic-tac-toe | X și 0]] +
-    * joc foarte simplu/​ușor (spațiul stărilor este mic).  +
-    * Prin urmare tot arborele de soluții poate fi generat și explorat într-un timp foarte scurt. +
-  * [[https://​en.wikipedia.org/​wiki/​Chess | sah ]] +
-    * joc foarte greu (spațiul stărilor este foarte mare) +
-    * minimax/​negamax simplu poate merge până la $ d = 7$ (nu reușea să bată campionul mondial la șah - campion uman) +
-    * alpha-beta poate merge până la $d = 14$ +
-    * ** [[https://​en.wikipedia.org/​wiki/​Deep_Blue_(chess_computer)| Deep Blue]]** a fost implementarea unui bot cu minimax și alpha-beta care a bătut în 1997 campionul mondial la șah (Gary Kasparov). +
-  * [[https://​en.wikipedia.org/​wiki/​Ultimate_tic-tac-toe | Ultimate tic-tac-toe]] +
-    * varianta mult mai grea de X și 0 (spațiul stărilor foarte mare) +
-    * s-a dat la proiect PA 2016 :D +
-  * [[https://​en.wikipedia.org/​wiki/​Go_(game) | Go]] +
-    * soluțiile se bazează pe Monte Carlo Tree Search (nu pe minimax) +
-    * [[https://​en.wikipedia.org/​wiki/​AlphaGo|AlphaGo]] este botul cel mai bun pe tabla de 19x19+
  
 +Soluție:
  
-===== Exerciții =====+|X|-|-|-|-| 
 +|-|-|X|-|-| 
 +|-|-|-|-|X| 
 +|-|X|-|-|-| 
 +|-|-|-|X|-|
  
-În lab06 nu există exerciții propriu-zise.+X reprezintă o damă, ​reprezintă spațiu gol.
  
-Task-uri:+</​spoiler>​
  
-* Parcugeți teoria din acest laborator împreună cu asistentul. 
-  * Trebuie să întelegeți și să comparați **Negamax** vs **Negamax cu Alpha-beta pruning**. 
-  * Indicație: Desenați exemplul din videoclipul de pe YouTube din sectiunea de **Precizări inițiale**.  ​ 
-* Discutați și comparați euristici pentru: 
-  * șah 
-  * table 
  
-===== Referințe ===== +<spoiler Hint>
-[0] [[http://​en.wikipedia.org/​wiki/​Minimax | wikipedia.org/​Minimax]]+
  
-[1] [[http://​en.wikipedia.org/​wiki/​Negamax | wikipedia.org/​Negamax]]+E nevoie să facem backtracking pe matrice sau e suficient pe vector?
  
-[2] [[http://​en.wikipedia.org/​wiki/​Alpha-beta_pruning | wikipedia.org/​Alpa-beta_pruning]]+</spoiler>
  
-[4] [[https://​en.wikipedia.org/​wiki/​Monte_Carlo_tree_search | wikipedia.org/​Monte_Carlo_tree_search]] 
  
-[5] [[https://​en.wikipedia.org/​wiki/​MTD-f] | wikipedia.org/​MTD-f]] 
  
-[6] [[https://​www.chessprogramming.org/​Null_Window | chessprogramming.org/​Null_Window]]+<​note>​ 
 +Se va caută o singură soluție ​ (**oricare** soluție corectă), care va fi returnată sub forma unui vector cu $n + 1$ elemente.
  
-[7] [[https://​www.chessprogramming.org/​Principal_Variation | chessprogramming.org/​Principal_Variation]] 
  
-[8] [[https://​www.chessprogramming.org/​Iterative_Deepening| chessprogramming.org/​Iterative_deepening]]+Soluția este $sol[0], sol[1], ..., sol[n]$, unde $sol[i]$ = coloana unde vom plasa regina de pe linia i.
  
-[9] [[https://​www.aaai.org/​Papers/​AIIDE/​2008/​AIIDE08-036.pdf | Monte Carlo Tree Search]]+Elementul 0 este nefolosit, dorim să păstrăm convenția cu indexare de la 1.
  
 +</​note>​
 +==== Generare de șiruri ====
 +
 +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.
 +
 +<spoiler Exemplu 1>
 +
 +Fie caractere[] = {'​a',​ '​b',​ '​c'​},​ freq[] = {1, 1, 2}, K = 5
 +
 +Soluție:
 +  * abcc
 +  * acbc
 +  * accb
 +  * bacc
 +  * bcac
 +  * bcca
 +  * cabc
 +  * cacb
 +  * cbac
 +  * cbca
 +  * ccab
 +  * ccba
 +
 +</​spoiler>​
 +
 +<spoiler Exemplu 2>
 +
 +Fie caractere[] = {'​b',​ '​c'​},​ freq[] = {3, 2}, K = 2
 +
 +Solutie:
 +  * bbcbc
 +  * bbccb
 +  * bcbbc
 +  * bcbcb
 +  * bccbb
 +  * cbbcb
 +  * cbcbb
 +
 +</​spoiler>​
 +
 +
 +<​note>​
 +Soluțiile se vor genera în ordine lexico-grafica!
 +
 +Checkerul așteaptă să le stocați în această ordine.
 +</​note>​
 +===== Bonus =====
 +
 +==== Problema damelor (AC3) ====
 +
 +**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:
 +    * constrângeri
 +    * variabile
 +    * domenii de variabile
 +
 +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.
 +
 +=== Exemplu AC-3 ===
 +
 +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.
 +
 +===== Extra =====
 +
 +==== Immortal ====
 +
 +[[https://​www.infoarena.ro/​problema/​immortal | Enunt]]
 +<spoiler Solutie>
 +[[http://​olimpiada.info/​oji2010/​index.php?​cid=arhiva | OJI 2010 - clasele 11-12]]
 +</​spoiler>​
 +
 +
 +
 +<spoiler Backtracking problems>​
 +Articolul de pe [[https://​leetcode.com/​tag/​backtracking/​| leetcode]] conține o listă cu diverse tipuri de probleme de programare dinamică, din toate categoriile discutate la PA (plus multe altele).
 +</​spoiler>​
 +
 +===== Referințe =====
  
 +[0] Chapter **Backtracking**,​ “Introduction to Algorithms”,​ Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein
pa/laboratoare/laborator-05.1678892063.txt.gz · Last modified: 2023/03/15 16:54 by radu.nichita
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0