This shows you the differences between two versions of the page.
|
pa:laboratoare:laborator-06 [2018/04/09 22:00] radu.iacob [Referinte] |
pa:laboratoare:laborator-06 [2026/05/06 19:39] (current) radu.nichita [Implementarea algoritmilor standard] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Laborator 06: Minimax ====== | + | ====== Laborator 06: Parcurgerea grafurilor. Aplicații (1/2) ====== |
| - | + | ||
| - | Responsabili: | + | |
| - | * [[neatudarius@gmail.com|Darius Neațu]] | + | |
| - | * [[visanr95@gmail.com|Radu Vișan]] | + | |
| - | * [[cristb@gmail.com|Cristian Banu]] | + | |
| ===== Obiective laborator ===== | ===== Obiective laborator ===== | ||
| - | * Insusirea unor cunostinte de baza despre **teoria jocurilor** precum si despre **jocurile de tip joc de suma zero (suma nula, zero-sum games)** | ||
| - | * Insusirea abilitatii de rezolvare a problemelor ce presupun cunoasterea si exploatarea conceptului de joc de suma zero (zero-sum game); | ||
| - | * Insusirea unor cunostinte elementare despre algoritmii necesari rezolvarii unor probleme de joc de suma zero (zero-sum game). | ||
| - | ===== Precizari initiale ===== | + | * Înțelegerea conceptele de graf, reprezentare și parcugere |
| - | <note warning> | + | * Studierea unor aplicații pentru parcurgeri |
| - | Un curs foarte bine explicat este pe canalul de YouTube de la MIT. Va sfatuim sa vizionati integral [[https://www.youtube.com/watch?v=STjW3eH0Cik&t=2480s&list=PLqUJoA5RTkI7UbJXJaHX80FotR1QJWHJt&index=1 | Search: Games, Minimax, and Alpha-Beta]] inainte sa parcurgeti materialul de pe ocw. | + | |
| - | </note> | + | |
| ===== Importanţă – aplicaţii practice ===== | ===== Importanţă – aplicaţii practice ===== | ||
| - | Algoritmul **Minimax** si variantele sale imbunatatite (**Negamax**, **Alpha-Beta** etc.) sunt folosite in diverse domenii precum teoria jocurilor (Game Theory), teoria jocurilor combinatorice (Combinatorial Game Theory – CGT), teoria deciziei (Decision Theory) si statistica. | + | Grafurile sunt utile pentru a modela diverse probleme și au numeroase aplicații practice: |
| - | Astfel, diferite variante ale algoritmului sunt necesare in proiectarea si implementarea de aplicatii legate de inteligenta artificiala, economie, dar si in domenii precum stiinte politice sau biologie. | + | * Rețele de calculatoare (ex: stabilirea unei topologii fără bucle / arbore de acoperire) |
| + | * Pagini Web (ex. algoritmi de căutare - Google PageRank ) | ||
| + | * Rețele sociale (ex. sugestii de prietenie pe Facebook) | ||
| + | * Hărți cu drumuri (ex. drum minim între două localități) | ||
| + | * Modelare grafică (ex. arbori de parționare) | ||
| + | * Rețele de transport (ex. flux) | ||
| - | ===== Descrierea problemei și a rezolvărilor===== | + | ===== Grafuri ===== |
| + | Puteți consulta capitolul "Elementary Graph Algorithms" din "Introduction to Algorithms" [0] pentru mai multe definiții formale. Această secțiune sumarizează principalele notații folosite în laboratoarele de PA. | ||
| - | Algoritmii Minimax permit abordarea unor probleme ce tin de teoria jocurilor combinatorice. CGT este o ramura a matematicii ce se ocupa cu studierea jocurilor in doi (two-player games), in care participantii isi modifica rand pe rand pozitiile in diferite moduri, prestabilite de regulile jocului, pentru a indeplini una sau mai multe conditii de castig. | + | ==== Definiții ==== |
| - | Exemple de astfel de jocuri sunt: sah, go, dame (checkers), X si O (tic-tac-toe) etc. | + | >> Un **graf** G se definește ca fiind o pereche (V, E), unde **V = {node / un nod oarecare din graf}**, iar **E = {(x, y) / (x, y) muchie in graf}**. |
| - | CGT nu studiaza jocuri ce presupun implicarea unui element aleator (sansa) in derularea jocului precum poker, blackjack, zaruri etc. Astfel decizia abordarii unor probleme rezolvabile prin metode de tip Minimax se datoreaza in principal simplitatii atat conceptuale, cat si raportat la implementarea propriu-zisa. | + | >> Un graf este **neorientat** dacă relațiile dintre noduri sunt **bidirecționale**: oricare ar fi $(x, y)$ în $E$, există și $(y, x)$ în $E$. Relațiile se numesc **muchii**. |
| - | ==== Minimax ==== | + | >> Un graf este **orientat** dacă relațiile dintre noduri sunt **unidirecționale**: $(x, y)$ este în $E$ nu implică neapărat $(y, x)$ în $E$. Relațiile se numesc **arce**. |
| - | Strategia pe care se bazeaza ideea algoritmului este ca jucatorii implicati adopta urmatoarele strategii: | + | |
| - | *Jucatorul 1 (**maxi**) va incerca mereu sa-si **maximizeze** propriul castig prin mutarea pe care o are de facut; | + | >> O **componentă conexă (CC)** este o submulțime maximală de noduri, cu proprietatea că oricare ar fi două noduri x și y din aceasta, există drum de la x la y. Pentru grafuri orientate, o componentă conexă se numește ** componentă tare conexă (CTC)**. |
| - | *Jucatorul 2 (**mini**) va incerca mereu sa **minimizeze** castigul jucatorului 1 la fiecare mutare. | + | |
| - | De ce merge o astfel de abordare? Dupa cum se preciza la inceput, discutia se axeaza pe jocuri de suma zero (zero-sum game). Acest lucru garanteaza, printre altele, ca orice castig al Jucatorului 1 este egal cu modulul sumei pierdute de Jucatorul 2. Cu alte cuvinte cat pierde Jucator 2, atat castiga Jucator 1. Invers, cat pierde Jucator 1, atat castiga Jucator 2. Sau | + | >> Un graf **aciclic** este un graf (orientat/neorientat) care nu conține cicluri. |
| - | $$Win_{Player_1} = | Loss_{Player_2} |$$ | + | ==== Reprezentare ==== |
| - | $$| Loss_{Player_1} | = Win_{Player_2}$$ | + | Problemele care se modelează folosind grafuri, de obicei, presupun explorarea spațiului. O parcurgere explorează fiecare nod al grafului, exact o singură dată, pornind de la un nod ales, numit în continuare nod sursă (EN: **source**). Modul de reprezentare al grafului poate influența performanța unei parcurgeri/unui algoritm. |
| - | === Reprezentarea spatiului solutiilor === | + | Un graf poate fi modelat în mai multe moduri (folosind mai multe notații): |
| - | In general spatiul solutiilor pentru un joc in doi de tip zero-sum se reprezinta ca un **arbore**, fiecarui nod fiindu-i asociata o stare a jocului in desfasurare (game state). Pentru exemplul nostru de X si O putem considera urmatorul arbore ( partial ) de solutii, ce corespunde primelor mutari ale lui X, respectiv O: | + | * printr-o pereche de mulțimi $G = (V, E)$ |
| + | * $V$ = {v / v este un nod în graf} = mulțimea nodurile grafului (EN: nodes / vertices) | ||
| + | * $E$ = {e / $e=(x, y)$ este o muchie în graf între nodurile x și y} = mulțimea muchiile/arcelor (EN: edges), fiecare muchie stabilind o relație de vecinătate între doua noduri. | ||
| - | {{ :pa:laboratoare:5.1_cr.png |}} | + | * printr-o pereche $G = (nodes, a)$ |
| + | * $nodes$ = {node / node este un nod în graf} | ||
| + | * $a[x][y] = 0/1$ | ||
| + | * **1** = există muchia/arcul (x, y) | ||
| + | * **0** = **NU** există muchia/arcul (x, y) | ||
| - | **Metodele de reprezentare a arborelui** variaza in functie de paradigma de programare aleasa, de limbaj, precum si de gradul de optimizare avut in vedere. | + | * printr-o pereche de mulțimi $G = (nodes, adj)$ |
| + | * $nodes$ = {node / node este un nod în graf} | ||
| + | * $adj$ = {$adj[node]$ / unde $adj[node]$ este lista de adiacență a lui node} = reprezentarea grafului ca liste de adiacențe | ||
| + | * $adj[node] = {..., neigh, ...}$ => există muchie/arc (node, neigh) | ||
| - | Avand notiunile de baza asupra strategiei celor doi jucatori, precum si a reprezentarii spatiului solutiilor problemei, putem formula o prima varianta a algoritmului Minimax: | + | Reprezentarea în memorie a grafurilor se face, de obicei, cu **liste de adiacență**. Se pot folosi însă și alte structuri de date, care vor fi introduse pe parcurs. |
| - | <spoiler Pseudocod Minimax> | + | Cele mai uzuale notații din laboratoarele de grafuri sunt descrise în [[https://ocw.cs.pub.ro/courses/pa/skel_graph | Precizări laboratoare 06-11]] (ex. $n$, $m$, $adj$, $adj\_trans$, $(x, y)$, etc). |
| + | |||
| + | ==== Colorare ==== | ||
| + | Algoritmii de parcugere se pot folosi de o colorare a nodurilor: | ||
| + | |||
| + | * **white** (alb) = nod care nu a fost încă vizitat (nu este în coadă) | ||
| + | |||
| + | * **gray** (gri) = nod care este în curs de vizitare (a fost adăugat în coadă) | ||
| + | |||
| + | * **black** (negru) = nod care a fost complet vizitat (node scos din coadă și pentru care s-a vizitat tot subarborele) | ||
| + | |||
| + | ==== Algoritmi de parcurgere ==== | ||
| + | Problemă: Să se parcurgă un graf dat. Fiecare nod se parcuge (exact) o singură dată. | ||
| + | Algoritmi: | ||
| + | |||
| + | * **BFS** | ||
| + | |||
| + | * **DFS** | ||
| + | |||
| + | ===== BFS - Parcurgerea în lățime ===== | ||
| + | |||
| + | Parcurgerea în lățime **(Breadth-first Search - BFS)** este un algoritm de căutare în graf, în care, atunci când se ajunge într-un nod oarecare **node**, nevizitat, se vizitează toate nodurile nevizitate adiacente lui (notate pe rand cu **neigh**), apoi toate vârfurile nevizitate adiacente vârfurilor adiacente lui node, etc. | ||
| + | |||
| + | Atenție! BFS depinde de nodul de start **source**. Plecând din acest nod, se vor vizita toate nodurile accesibile. De exemplu, într-un graf neorientat, aceste noduri accesibile formează o componentă conexă; în urma aplicării algoritmului BFS asupra fiecărei componente conexe a grafului, se obține un arbore de acoperire a întregului graf (prin eliminarea muchiilor pe care nu le folosim la parcurgere). Pentru a putea reconstitui acest arbore, se păstrează pentru fiecare nod dat identitatea părintelui său. În cazul în care nu exista o funcție de cost asociată muchiilor, BFS va determina și drumurile minime de la rădăcină la oricare nod. | ||
| + | |||
| + | Pentru implementarea BFS se folosește o coadă. | ||
| + | |||
| + | ==== Algoritm ==== | ||
| <code cpp> | <code cpp> | ||
| - | int evaluate(stare); // functia returneaza un scor asociat cu starea | + | // do a BFS traversal from source |
| - | // functia returneaza mereu scorul din perspectiva lui maxi | + | // |
| - | + | // source = the source for the BFS traversal | |
| - | + | // nodes = list of all nodes from G | |
| - | void apply_move(move); // functia modifica starea curenta: executa miscarea move | + | // adj[node] = the adjacency list of node |
| - | void undo_move(move); // functia restaureaza starea anterioara (de dinainte de executa lui move) | + | // example: adj[node] = {..., neigh, ...} => edge (node, neigh) |
| - | + | BFS(source, G=(nodes, adj)) { | |
| - | // alege cea mai buna mutare pentru jucatorul maxi | + | // STEP 0: initialize results |
| - | // functia returneaza best_score pentru maxi | + | // d[node] = distance from source to node |
| - | int maxi(int depth) { | + | // p[node] = parent of node in the BFS traversal started from source |
| - | // daca jocul s-a terminat sau am atins nivelul maxim de recursivitate ales | + | // [optional] color[node] = white/gray/black |
| - | if (gameOver() || depth == 0 ) { | + | // * white = not yet visited |
| - | return evaluate(); // ne oprim si evaluam starea curenta | + | // * gray = visit in progress |
| + | // * black = visited | ||
| + | foreach (node in nodes) { | ||
| + | d[node] = +oo; // distance not yet computed | ||
| + | p(node) = null; // parent not yet found | ||
| + | // [optional] color[node] = white; | ||
| } | } | ||
| - | | ||
| - | int max = -oo; // jucatorul maxi doreste sa-si maximizeze | ||
| - | |||
| - | // incercam pe rand fiecare care miscare posibila move | ||
| - | for (move : all_moves) { | ||
| - | apply_move(move); // executa move | ||
| - | // incercam sa simulam jocul mai departe: | + | // STEP 1: initialize a queue |
| - | // daca maxi face move, ce ar face mini? | + | q = {} |
| - | int score = mini(depth - 1); | + | |
| - | + | ||
| - | // dintre toate variantele pe care mini le permite (!!!), | + | |
| - | // maxi o va alege pe cea cu scor maxim | + | |
| - | if (score > max) { | + | |
| - | max = score; | + | |
| - | } | + | |
| - | + | ||
| - | undo_move(move); // restaureaza starea de dinainte de move | + | |
| - | } | + | |
| - | // cel mai bun scor pe care il poate obtine maxi este max | + | // STEP 2: add the source(s) into q |
| - | // (s-a tinut cont si de ce i-ar permite mini, avand in vedere ca si el joaca optim) | + | d[source] = 0; // distance from source to source |
| - | return max; | + | p[source] = null; // the source never has a parent (because it's the root of the traversal) |
| - | } | + | q.push(source); |
| + | // [optional] color[source] = gray; | ||
| - | // alege cea mai buna mutare pentru jucatorul mini | + | // STEP 3: start traversal using the node(s) from q |
| - | // functia returneaza best_score pentru mini | + | while (!q.empty()) { // while still have nodes to explore |
| - | int mini(int depth) { | + | // STEP 3.1: extract the next node from queue |
| - | // daca jocul s-a terminat sau am atins nivelul maxim de recursivitate ales | + | node = q.pop(); |
| - | if (gameOver() || depth == 0 ) { | + | |
| - | return -evaluate(); // ne oprim si evaluam starea curenta | + | |
| - | } | + | |
| - | + | ||
| - | int min = +oo; // jucatorul mini doreste sa minimizeze scorul lui maxi | + | |
| - | + | ||
| - | // incercam pe rand fiecare care miscare posibila move | + | |
| - | for (move : all_moves) { | + | |
| - | apply_move(move); // executa move | + | |
| - | // incercam sa simulam jocul mai departe: | + | // [optional] STEP 3.2: print/use the node |
| - | // daca mini face move, ce ar face maxi? | + | |
| - | int score = maxi(depth - 1); | + | // STEP 3.3: expand/visit the node |
| - | + | foreach (neigh in adj[node]) { // for each neighbour | |
| - | // dintre toate variantele pe care maxi le permite (!!!), | + | if (d[node] + 1 < d[neigh]) { // a smaller distance <=> color[neigh] == white |
| - | // mini o va alege pe cea cu scor minim | + | d[neigh] = d[node] + 1; // update distance |
| - | if (score < min) { | + | p[neigh] = node; // save parent |
| - | min = score; | + | q.push(neigh); // add neigh to the queue of nodes to be visited |
| + | // [optional] color[neigh] = gray; | ||
| + | } | ||
| } | } | ||
| - | + | ||
| - | undo_move(move); // este o functie care incearca sa refaca | + | // [optional] color[node] = black; |
| - | // starea de dinainte de aplicarea lui move | + | |
| } | } | ||
| - | |||
| - | // cel mai bun scor pe care il poate obtine mini este min | ||
| - | // (s-a tinut cont si de ce i-ar permite maxi, avand in vedere ca si el joaca optim) | ||
| - | return min; | ||
| } | } | ||
| - | |||
| </code> | </code> | ||
| - | </spoiler> | ||
| - | === Argumentarea utilizarii unei adancimi maxime === | + | <note> |
| + | Liniile cu **[optional]** se referă la logica de colorare menționată anterior, care se poate omite (dacă nu se dorește acest rezultat). | ||
| + | </note> | ||
| - | Datorita **spatiului de solutii mare**, de multe ori coplesitor ca volum, o inspectare completa a acestuia nu este fezabila si devine impracticabila din punctul de vedere al timpului consumat sau chiar a memoriei alocate (se vor discuta aceste aspecte in paragraful legat de complexitate). | + | ==== Complexitate ==== |
| + | * cu liste de adiacență: $O(n + m)$ sau $O(|V| + |E|)$ | ||
| + | * cu matrice de adiacență: $O(n^2)$ sau $O(|V|^2)$ | ||
| - | Astfel, de cele mai multe ori este preferata o abordare care parcurge arborele numai pana la o anumita **adancime maxima („depth”)**. Aceasta abordare permite examinarea arborelui destul de mult pentru a putea lua decizii minimalist coerente in desfasurarea jocului. | + | ===== DFS - Parcurgerea în adâncime ===== |
| - | Totusi, **dezavantajul major** este ca pe termen lung se poate dovedi ca decizia luata la adancimea depth nu este global favorabila jucatorului in cauza. | + | Parcurgerea în adâncime **(Depth-First Search - DFS)** pornește de la un nod dat (**node**), care este marcat ca fiind în curs de procesare. Se alege primul vecin nevizitat al acestui nod (**neigh**), se marchează și acesta ca fiind în curs de procesare, apoi și pentru acest vecin se caută primul vecin nevizitat, și așa mai departe. În momentul în care nodul curent nu mai are vecini nevizitati, se marchează că fiind deja procesat și se revine la nodul anterior. Pentru acest nod se caută primul vecin nevizitat. Algoritmul se repetă până când toate nodurile grafului au fost procesate. |
| - | De asemenea, se observa recursivitatea indirecta. Prin conventie acceptam ca **inceputul algoritmului** sa fie cu functia maxi. Astfel, se analizeaza succesiv diferite stari ale jocului din punctul de vedere al celor doi jucatori pana la adancimea depth. Rezultatul intors este scorul final al miscarii celei mai bune. | + | În urma aplicării algoritmului DFS asupra fiecărei componente conexe a grafului, se obține pentru fiecare dintre acestea câte un arbore de acoperire (prin eliminarea muchiilor pe care nu le folosim la parcurgere). Pentru a putea reconstitui acest arbore, păstram pentru fiecare nod dat identitatea părintelui sau. |
| - | ==== Negamax ==== | + | Pentru fiecare nod se vor reține: |
| + | * $start[node]$ = timestamp-ul / timpul descoperirii | ||
| + | * $finish[node]$ = timestamp-ul / timpul finalizării | ||
| + | * $p[node]$ = părintele din parcugerea DFS a lui node | ||
| - | Negamax este o varianta a minimax, ce se bazeaza pe urmatoarea observatie: intr-un joc cu suma zero castigul unui jucator este egal cu modulul sumei pierdute de celalalt jucator si invers. | + | Spre deosebire de BFS, pentru implementarea DFS se folosește o stivă (abordare **LIFO** în loc de **FIFO**). În practică, stiva nu va fi reținută explicit - ci ne vom baza pe recursivitate. |
| - | Intr-adevar putem spune ca jucatorul mini incearca de fapt sa maximizeze in modul suma pierduta de maxi. Astfel putem formula urmatoarea implementare ce profita de observatia de mai sus. | + | ==== Algoritm ==== |
| + | <code cpp> | ||
| + | // do a DFS traversal from all nodes | ||
| + | // | ||
| + | // nodes = list of all nodes from G | ||
| + | // adj[node] = the adjacency list of node | ||
| + | // example: adj[node] = {..., neigh, ...} => edge (node, neigh) | ||
| + | // | ||
| + | DFS(G=(nodes, adj)) { | ||
| + | // STEP 0: initialize results | ||
| + | // p[node] = parent of node in the BFS traversal started from source | ||
| + | // start[node] = the timestamp (the order) when we started visiting the node subtree | ||
| + | // finish[node] = the timestamp (the order) when we finished visiting the node subtree | ||
| + | // [optional] color[node] = white/gray/black | ||
| + | // * white = not yet visited | ||
| + | // * gray = visit in progress | ||
| + | // * black = visited | ||
| + | foreach (node in nodes) { | ||
| + | p[node] = null; // parent not yet found | ||
| + | // [optional] color[node] = white; | ||
| + | } | ||
| - | Nota: putem exprima aceasta observatie si pe baza formulei **max(a, b) = -min(-a, -b)**. | + | timestamp = 0; // the first timestamp before the DFS traversal |
| - | <spoiler Pseudocod Negamax> | + | foreach (node in nodes) { |
| - | <code cpp> | + | if (p[node] == null) { // or [optional] color[node] == white |
| - | int evaluate(stare); // functia returneaza un scor asociat cu starea | + | DFS_RECURSIVE(node, G, p, timestamp) |
| - | // functia returneaza mereu scorul din perspectiva jucatorului CURENT! | + | } |
| - | + | ||
| - | + | ||
| - | void apply_move(move); // functia modifica starea curenta: executa miscarea move | + | |
| - | void undo_move(move); // functia restaureaza starea anterioara (de dinainte de executa lui move) | + | |
| - | + | ||
| - | // alege cea mai buna mutare pentru jucatorul CURENT (EU) | + | |
| - | // functia returneaza best_score | + | |
| - | int negamax(int depth) { | + | |
| - | // daca jocul s-a terminat sau am atins nivelul maxim de recursivitate ales | + | |
| - | if (gameOver() || depth == 0 ) { | + | |
| - | return evaluate(); // ne oprim si evaluam starea curenta | + | |
| } | } | ||
| - | + | } | |
| - | int max = -oo; // jucatorul CURENT doreste sa-si maximizeze scorul | + | |
| - | + | ||
| - | // incercam pe rand fiecare care miscare posibila move | + | |
| - | for (move : all_moves) { | + | |
| - | apply_move(move); // executa move | + | |
| - | // incercam sa simulam jocul mai departe: | + | DFS_RECURSIVE(node, G=(node, adj), p, ref timestamp) { |
| - | // daca jucatorul CURENT face face move, ce ar face ADVERSARUL? | + | start[node] = ++timestamp; // start visiting its subtree |
| - | int score = -negamax(depth - 1); // cel mai bine pentru el, | + | // [optional] color[node] = gray; |
| - | // este cel mai rau pentru mine si invers | + | |
| - | | + | for (neigh in adj[node]) { // for each neighbour |
| - | // dintre toate variantele pe care ADVERSARUL le permite (!!!), | + | if (p[neigh] == null) { // or [optional] color[neigh] = white; |
| - | // EU (jucatorul CURENT) o voi alege pe cea cu scor maxim | + | p[neigh] = node; // save parent |
| - | if (score > max) { | + | DFS_RECURSIVE(neigh, G, p, timestamp); // continue traversal |
| - | max = score; | + | |
| } | } | ||
| - | | ||
| - | undo_move(move); // restaureaza starea de dinainte de move | ||
| } | } | ||
| - | // cel mai bun scor pe care il pot obtine EU este max | + | finish[node] = ++timestamp; // finish visiting its subtree |
| - | // (s-a tinut cont si de ce mi-ar permite ADVERSARUL, avand in vedere ca si el joaca optim) | + | // [optional] color[node] = black; |
| - | return max; | + | |
| } | } | ||
| </code> | </code> | ||
| - | </spoiler> | ||
| <note> | <note> | ||
| - | Se observa direct avantajele acestei formulari fata de Minimax-ul standard prezentat anterior: | + | Liniile cu **[optional]** se referă la logica de colorare menționată anterior, care se poate omite (dacă nu se dorește acest rezultat). |
| + | </note> | ||
| + | |||
| + | ==== Complexitate ==== | ||
| + | * cu liste de adiacență: $O(n + m)$ sau $O(|V| + |E|)$ | ||
| + | * cu matrice de adiacență: $O(n^2)$ sau $O(|V|^2)$ | ||
| + | |||
| + | ==== Tipuri de muchii/arce în parcurgerea DFS ==== | ||
| + | >> **Arborele de parcurgere DFS** cuprinde toate nodurile din graf împreună cu muchiile/arcele pe vizitate de DFS. Dacă graful nu este conex / tare conex, se obțin mai mulți arbori care formează o **pădure de arbori DFS**. | ||
| + | <note> | ||
| + | Arborele DFS nu este unic - depinde de ordinea în care nodurile sunt stocate în listele de adiacență. | ||
| + | |||
| + | Exemplu: | ||
| + | * dacă în lista lui 1 avem nodurile 2 și 3, atunci când se va vizita 1, prima oară se încearcă vizitarea lui 2, apoi a lui 3. | ||
| + | * dacă în lista lui 1 avem nodurile 3 și 2, atunci când se va vizita 1, prima oară se încearcă vizitarea lui 3, apoi a lui 2. | ||
| - | * **claritatea** si **eleganta** sporita a codului | ||
| - | * usurinta in **intretinerea** si **extinderea** functionalitatii | ||
| </note> | </note> | ||
| - | Din punctul de vedere al complexitatii temporale, Negamax nu difera absolut deloc de Minimax (ambele examineaza acelasi numar de stari in arborele de solutii). | + | Putem folosi o parcurgere DFS pentru a clasifica tipurile de muchii (toate muchiile din graf) relativ la arborele DFS curent. |
| + | >> **tree-edge** (**T**) / muchie de arbore = muchie **(x, y)** care conectează un nod x de copilul său y din arbore. | ||
| - | Putem concluziona ca este de **preferat** o implementare ce foloseste **negamax** fata de una bazata pe minimax in rezolvarea unor probleme ce tin de aceasta tehnica. | + | >> **back-edge** (**B**) / muchie înapoi = muchie **(x, y)** care conectează un nod x de un strămoș y (ambele noduri sunt în curs de vizitare). |
| - | ==== Alpha-beta pruning ==== | + | Pentru **graf orientat**, mai există încă 2 tipuri de muchii (arce): |
| + | >> **forward-edge** (**F**) / muchie înainte /muchie de înaintare = muchie **(x, y)** care nu este **tree-edge** și care conecteză un nod **x** de un descendent **y** . | ||
| - | Pana acum s-a discutat despre algoritmii Minimax / Negamax. Acestia sunt algoritmi exhaustivi (**exhausting search algorithms**). Cu alte cuvinte, ei gasesc solutia optima examinand intreg spatiul de solutii al problemei. Acest mod de abordare este extrem de ineficient in ceea ce priveste efortul de calcul necesar, mai ales considerand ca extrem de multe stari de joc inutile sunt explorate (este vorba de acele stari care nu pot fi atinse datorita incalcarii principiului de maximizare a castigului la fiecare runda). | + | <note> |
| + | În graful neorientat aceasta nu are sens. Într-un graf neorientat, **(x, y)** și **(y, x)** reprezintă același lucru. Dacă x ar fi strămoș a lui y (x nu este părintele lui y, căci altfel **(x, y)** ar fi **tree-edge**), mai întâi se va încerca din y să se ajungă în x (moment în care spunem că muchia **(y, x)** este **back-edge**), ulterior la revenirea din recursivitate se va încerca din x să se ajungă în y (moment în care ar trebui să spunem că muchia **(x, y)** este **forward-edge**). Deoarece există o singură muchie **(x, y)** sau **(y, x)**, nu putem pune 2 categorii, așa că rămâne prima categorie găsită: **back-edge**. | ||
| - | O imbunatatire substantiala a minimax/negamax este **Alpha-beta pruning** (**taiere alfa-beta**). Acest algoritm incearca sa optimizeze mini/negamax profitand de o observatie importanta: **pe parcursul examinarii arborelui de solutii se pot elimina intregi subarbori, corespunzatori unei miscari m, daca pe parcursul analizei gasim ca miscarea m este mai slaba calitativ decat cea mai buna miscare curenta.** | + | Într-un graf **orientat** muchiile **(x, y)** și **(y, x)** sunt diferite, deci pot avea tipuri diferite! |
| + | </note> | ||
| - | Astfel, consideram ca pornim cu o prima miscare M1. Dupa ce analizam aceasta miscare in totalitate si ii atribuim un scor, continuam sa analizam miscarea M2. Daca in analiza ulterioara gasim ca adversarul are cel putin o miscare care transforma M2 intr-o miscare mai slaba decat M1 atunci orice alte variante ce corespund miscarii M2 (subarbori) nu mai trebuie analizate. | + | >> **cross-edge** (**C**) / muchie de traversare = muchie **(x, y)** care conectează un nod x de un nod y din alt subarbore. |
| + | <note> | ||
| + | În graful neorientat aceasta nu are sens. Într-un graf neorientat, dacă **(x, y)** ar fi **cross-edge**, înseamnă că aceasta conectează pe x de un nod y din alt subarbore (care a fost deja vizitat!). Dacă nodul y a fost deja vizitat, iar graful este neorientat, atunci s-ar fi **înaintat** pe muchia **(y, x)** din momentul vizitării nodului y. Acest lucru ar face muchia **(y, x)** un **tree-edge**. Prin urmare obținem o contradicție, deci nu putem avea cross-edge într-un graf neorientat. | ||
| - | <spoiler De ce?> | + | Într-un graf **orientat** muchiile **(x, y)** și **(y, x)** sunt diferite, deci pot avea tipuri diferite! |
| - | De ce? Pentru ca stim ca exista **cel putin** o varianta in care adversarul obtine un castig mai bun decat daca am fi jucat miscarea M1. | + | </note> |
| - | Nu conteaza exact cat de slaba poate fi miscarea M2 fata de M1. O analiza amanuntita ar putea releva ca poate fi si mai slaba decat am constatat initial, insa acest lucru este irelevant. | + | <spoiler Analogie cu culori> |
| + | Mai sus s-a catalogat o muchie **(x, y)** atunci când dintr-un nod în curs de vizitare x (culoare **GRAY**) se încearcă trecerea într-un nod y. | ||
| - | De ce insa ignoram intregi subarbori si miscari potential bune numai pentru o miscare slaba gasita? Pentru ca, in conformitate cu **principiul de maximizare al castigului** folosit de fiecare jucator, adversarul va alege exact acea miscare ce ii va da un castig maximal. Daca exista o varianta si mai buna pentru el este irelevant, deoarece noi suntem interesati daca cea mai slaba miscare buna a lui este mai buna decat miscarea noastra curent analizata. | + | În funcție de culoare lui y, observăm care este tipul muchiei: |
| + | |||
| + | * **(x, y)** -> **(GRAY, WHITE)** => **tree-edge** | ||
| + | * **(x, y)** -> **(GRAY, GRAY)** => **back-edge** | ||
| + | * **(x, y)** -> **(GRAY, BLACK)** => **forward-edge** sau **cross-edge** | ||
| + | </spoiler> | ||
| + | |||
| + | ===== Aplicații parcurgeri ===== | ||
| + | |||
| + | * Componente Conexe | ||
| + | * Sortarea Topologică | ||
| + | * Componente Tare-Conexe | ||
| + | * Componente Biconexe | ||
| + | |||
| + | În acest laborator vom studia doar problema sortare topologică. | ||
| + | |||
| + | ===== TopSort - Sortarea Topologică ===== | ||
| + | |||
| + | ==== Problemă ==== | ||
| + | |||
| + | >> O **sortare topologică** într-un **graf orientat aciclic** reprezintă o aranjare/permutare a nodurilor din graf care ține cont de arce. | ||
| + | |||
| + | Orientarea muchiilor corespunde unei relatii de ordine de la nodul sursa catre cel destinație: dacă $(x,y)$ este un arc, $x$ trebuie să apară înaintea lui $y$ în inșiruire. | ||
| + | |||
| + | <note> | ||
| + | Daca graful ar fi ciclic, nu ar putea exista o astfel de insiruire (nu se poate stabili o ordine intre nodurile care alcatuiesc un ciclu). | ||
| + | </note> | ||
| + | |||
| + | <spoiler Exemplu TopSort> | ||
| + | {{pa:new_pa:lab07-topsort-example1.png}} | ||
| + | |||
| + | În figura anterioară avem un graf cu: | ||
| + | * $n = 5$ $m = 4$ | ||
| + | * $arce: { (1,2); (1,3); (2,3); (2,4);} $ | ||
| + | |||
| + | Toate sortările topologice valide sunt: | ||
| + | * cele date de ordinea relativa a primelor 4 noduri: (1,2,3,4) | ||
| + | * $topsort = [1, 2, 3, 4, 5] $ | ||
| + | * $topsort = [1, 2, 3, 5, 4] $ | ||
| + | * $topsort = [1, 2, 5, 3, 4] $ | ||
| + | * $topsort = [1, 5, 2, 3, 4] $ | ||
| + | * $topsort = [5, 1, 2, 3, 4] $ | ||
| + | * cele date de ordinea relativa a primelor 4 noduri: (1,2,4,3) | ||
| + | * $topsort = [1, 2, 4, 3, 5] $ | ||
| + | * $topsort = [1, 2, 4, 5, 3] $ | ||
| + | * $topsort = [1, 2, 5, 4, 3] $ | ||
| + | * $topsort = [1, 5, 2, 4, 3] $ | ||
| + | * $topsort = [5, 1, 2, 4, 3] $ | ||
| + | Explicație pentru $topsort = [1, 2, 3, 4, 5] $: | ||
| + | * deoarece avem arcele $1 \rightarrow 3$ si $1 \rightarrow 2$, 1 trebuie să apara înainte lui 2 și 3 | ||
| + | * deoarece avem arcul $2 \rightarrow 3$ si $2 \rightarrow 4$, 2 trebuie să apara înainte lui 3 și 4 | ||
| + | * 5 nu depinde de nimeni, poate să apară oriunde | ||
| </spoiler> | </spoiler> | ||
| + | ==== Algoritmi ==== | ||
| + | Sunt doi algoritmi cunoscuti pentru sortarea topologică. | ||
| + | |||
| + | === TopSort - DFS: sortare descrescătoare după timpul de finalizare === | ||
| <note> | <note> | ||
| - | O observatie foarte importanta se poate face analizand **modul de functionare** al acestui algoritm: este extrem de importanta **ordonarea miscarilor dupa valoarea castigului**. | + | Algoritm TopSort cu DFS: |
| + | * se face o parcurgere DFS pentru determinarea timpilor de finalizare | ||
| + | * se sortează descrescător in functie de timpul de finalizare | ||
| + | * permutarea de noduri obținută este o sortare topologică | ||
| + | </note> | ||
| - | In **cazul ideal** in care cea mai buna miscare a jucatorului curent este analizata prima, toate celelalte miscari, fiind mai slabe, vor fi eliminate din cautare timpuriu. | + | **Optimizare**: Pentru a evita sortarea nodurilor in functie de timpul de finalizare, se poate folosi o stiva ce retine aceste noduri in ordinea terminarii parcurgerii (sau un vector care la final este inversat). |
| - | In **cazul cel mai defavorabil** insa, in care miscarile sunt ordonate crescator dupa castigul furnizat, Alpha-beta are aceeasi compelxitate cu Mini/Nega-max, neobtinandu-se nicio imbunatatire. | + | == Complexitate == |
| + | * $T(n) = O(n + m)$ | ||
| - | In **medie** se constata o imbunatatire vizibila a algoritmului Alpha-beta fata de Mini/Nega-max. | + | === TopSort - BFS: algoritmul lui Kahn === |
| + | <note> | ||
| + | Algoritm TopSort cu BFS: | ||
| + | * se initializeaza coada de la BFS cu toate nodurile din graf care au grad inten **0** | ||
| + | * se porneste parcurgerea BFS | ||
| + | * la fiecare pas se vizitează un nod **node** | ||
| + | * se șterg toate muchiile care pleacă din **node**: $(node, neigh)$ | ||
| + | * $neigh$ este adaugat in coada doar daca devine un nod cu grad intern **0** | ||
| + | * se verifica la finalul parcugerii daca mai sunt muchii ramase in graf | ||
| + | * **daca** inca mai exista muchii neșterse, atunci graful conține cel puțin un ciclu - nu se poate determina o sortare topologică | ||
| + | * **altfel**, ordinea in care s-au scos nodurile din coada reprezinta o sortare topologica | ||
| </note> | </note> | ||
| - | <spoiler Exemplu Grafic> | + | **Optimizare**: Pentru a evita ștergerea propriu-zisă a muchiilor din graf, se poate modifica gradul intern al fiecărui nod (care poate fi reținut într-un vector $in\_degree[node]$). |
| - | Rolul miscarilor analizate la inceput presupune stabilirea unor **plafoane de minim si maxim** legate de cat de bune/slabe pot fi miscarile. | + | == Complexitate == |
| + | * $T(n) = O(n + m)$ | ||
| - | Astfel, plafonul de minim (Lower Bound), numit **alpha** stabileste ca o miscare nu poate fi mai slaba decat valoarea acestui plafon. | + | ==== Concluzie ==== |
| - | Plafonul de maxim (Upper Bound), numit **beta**, este important doarece el foloseste la a stabili daca o miscare este prea buna pentru a fi luata in considerare. Depasirea plafonului de maxim inseamna ca o miscare este atat de buna incat adversarul nu ar fi permis-o, adica mai sus in arbore exista o miscare pe care ar fi putut s-o joace pentru a nu ajunge in situatia curent analizata. | + | Ambele variante au aceeasi complexitate. |
| + | * Algoritmul bazat pe DFS nu verifica daca graful este ciclic: presupune corectitudinea inputului. Este relativ mai simplu de implementat. | ||
| + | * Algoritmul bazat pe BFS se poate folosi pentru a detecta daca graful este aciclic; in caz afirmativ, gaseste o sortare topologica valida. | ||
| - | Astfel alpha si beta furnizeaza o fereastra folosita pentru a filtra miscarile posibile pentru cei doi jucatori. Evident aceasta fereastra se poate actualiza pe masura ce se analizeaza mai multe miscari. De exemplul plafonul minim alpha se mareste pe masura ce gasim anumite tipuri de miscari mai bune (**better worst best moves**). Asadar, in implementare tinem seama si de aceste doua plafoane. In conformitate cu principiul Minimax, plafonul de minim al unui jocator (alpha-ul) este plafonul de maxim al celuilalt (beta-ul) si invers. Prezentam in continuare o descriere grafica a algoritmului Alpha-beta: | + | ===== TLDR ===== |
| - | + | ||
| - | {{ :pa:laboratoare:5.2_cr.png}} | + | |
| - | </spoiler> | + | * Cele mai uzuale moduri de reprezentare a unui graf sunt: liste de adiacentă și matrice de adiacentă. |
| + | * Cele doua moduri uzuale de parcurgere a unui graf sunt: **BFS** și **DFS**. | ||
| + | |||
| + | * O aplicație importantă a parcurgerilor este **Sortarea topologică** - o modalitate de aranjare a nodurilor în funcție de muchiile dintre ele. În functie de nodul de start al DFS, se pot obține sortări diferite, păstrând însă proprietatile generale ale sortarii topologice. | ||
| + | |||
| + | ===== Exerciții ===== | ||
| + | |||
| + | <note warning> | ||
| + | Înainte de a rezolva exercițiile, asigurați-vă ca ați citit și înțeles toate precizările din secțiunea | ||
| + | [[https://ocw.cs.pub.ro/courses/pa/skel_graph | Precizări laboratoare 06-11]]. | ||
| + | |||
| + | Prin citirea acestor precizari vă asigurați ca: | ||
| + | * cunoasteți **convențiile** folosite | ||
| + | * evitați **buguri** | ||
| + | * evitați **depunctări** la lab/teme/test | ||
| + | </note> | ||
| + | |||
| + | ===== Implementarea algoritmilor standard ===== | ||
| <note> | <note> | ||
| - | Un video cu un exemplu detaliat si foarte bine explicat se gaseste in tutorialul recomandat de pe YouTube (de la minutul 21:30 la 30:30). | + | [[https://github.com/acs-pa/pa-lab/tree/main/algorithms/lab06|pa-lab/algorithms/lab06]] conține implementarea algoritmilor standard / tehnicilor / tiparelor de algoritmi pentru acest laborator. |
| + | </note> | ||
| + | <note warning> | ||
| + | Începând cu acest laborator, fiecare problemă are restricții concrete: dimensiuni pentru input și timp maxim de execuție. Pentru a vedea dacă o soluție (idee) intră în timp înainte de a o implementa, va trebui să îi calculați complexitatea și să aproximați timpul de execuție folosind tutorialul [[https://github.com/acs-pa/pa-lab/tree/main/docs/complexity.md|pa-lab::docs/Complexity]]. | ||
| </note> | </note> | ||
| - | === Implementare === | + | ====== Pool probleme (pentru prezentări) ====== |
| - | In continuare prezentam o implementare conceptuala a Alpha-beta, atat pentru Minimax, cat si pentru Negamax: | + | |
| + | ======= 1) Berării 2 ======= | ||
| - | <spoiler Pseudocod Minimax with Alpha-beta> | + | **Enunț:** Se dă un graf orientat reprezentând o rețea de străzi și intersecții ale unui oraș, precum și locațiile în care se află berării. Se cere găsirea tuturor intersecțiilor din care **nu** se poate ajunge la nicio berărie, indiferent de traseul ales, respectând sensul unic al străzilor. |
| - | <code cpp> | + | |
| - | int evaluate(stare); // functia returneaza un scor asociat cu starea | + | |
| - | // functia returneaza mereu scorul din perspectiva lui maxi | + | |
| - | + | ||
| - | + | ||
| - | void apply_move(move); // functia modifica starea curenta: executa miscarea move | + | |
| - | void undo_move(move); // functia restaureaza starea anterioara (de dinainte de executa lui move) | + | |
| - | // alege cea mai buna mutare pentru jucatorul maxi | + | **Date de intrare:** Numărul de intersecții ''N'', numărul de străzi ''M'', numărul de berării ''P'', urmate de ''M'' perechi de noduri (reprezentând străzile orientate) și de ''P'' numere reprezentând intersecțiile unde se află berării. |
| - | // functia returneaza best_score pentru maxi, dar tine cont si de ce ii perminte mini | + | |
| - | int aplhabeta_maxi(int aplha, int beta, int depth) { | + | |
| - | // daca jocul s-a terminat sau am atins nivelul maxim de recursivitate ales | + | |
| - | if (gameOver() || depth == 0 ) { | + | |
| - | return evaluate(); // ne oprim si evaluam starea curenta | + | |
| - | } | + | |
| - | // incercam pe rand fiecare miscare posibila move | + | **Date de ieșire:** Numărul de intersecții din care nu se poate ajunge la o berărie, urmat de lista acestor intersecții, afișate în ordine crescătoare. |
| - | for (move : all_moves) { | + | |
| - | apply_move(move); // executa move | + | |
| - | // incercam sa simulam jocul mai departe: | + | Problema se poate testa la: \\ |
| - | // daca maxi face move, ce ar face mini? | + | [[https://infoarena.ro/problema/berarii2|Infoarena - Berării 2]] |
| - | int score = alphabeta_mini(alpha, beta, depth - 1); | + | |
| - | if (score >= beta) { | + | ======= 2) Alee ======= |
| - | return beta; // beta cut-off | + | |
| - | } | + | |
| - | // dintre toate variantele pe care mini le permite (!!!), | + | **Enunț:** O parcare este formată din ''N × N'' pătrate de latură 1. Anumite pătrate sunt ocupate de copaci și nu pot fi traversate. Se cere determinarea numărului minim de pași necesari pentru a ajunge de la un pătrat de start la un pătrat de destinație, ocolind copacii. Se pot face pași doar pe orizontală și verticală (Nord, Sud, Est, Vest). |
| - | // maxi o va alege pe cea cu scor maxim | + | |
| - | if (score > alpha) { | + | |
| - | alpha = score; | + | |
| - | } | + | |
| - | undo_move(move); // restaureaza starea de dinainte de move | + | **Date de intrare:** Dimensiunea parcării ''N'', numărul de copaci ''M'', urmate de coordonatele celor ''M'' copaci și, la final, coordonatele punctului de plecare și cele ale punctului de sosire. |
| - | } | + | |
| - | // cel mai bun scor pe care il poate obtine maxi este alpha | + | **Date de ieșire:** Un singur număr reprezentând distanța minimă (numărul de pași) între cele două poziții. |
| - | // (s-a tinut cont si de ce i-ar permite mini, avand in vedere ca si el joaca optim) | + | |
| - | return alpha; | + | |
| - | } | + | |
| + | Problema se poate testa la: \\ | ||
| + | [[https://infoarena.ro/problema/alee|Infoarena - Alee]] | ||
| - | // alege cea mai buna mutare pentru jucatorul mini | + | ======= 3) Muzeu ======= |
| - | // functia returneaza best_score pentru mini, dar tine cont si de ce ii perminte maxi | + | |
| - | int aplhabeta_mini(int aplha, int beta, int depth) { | + | |
| - | // daca jocul s-a terminat sau am atins nivelul maxim de recursivitate ales | + | |
| - | if (gameOver() || depth == 0 ) { | + | |
| - | return -evaluate(); // ne oprim si evaluam starea curenta | + | |
| - | } | + | |
| - | // incercam pe rand fiecare miscare posibila move | + | **Enunț:** Un muzeu este reprezentat printr-o matrice de ''N × N'' zone. În muzeu există spații libere, pereți (obstacole) și mai mulți paznici. Trebuie să determinați, pentru fiecare zonă liberă, distanța minimă până la cel mai apropiat paznic, folosind doar deplasări sus/jos/stânga/dreapta. |
| - | for (move : all_moves) { | + | |
| - | apply_move(move); // executa move | + | |
| - | // incercam sa simulam jocul mai departe: | + | **Date de intrare:** Un număr întreg ''N'' reprezentând dimensiunea muzeului, urmat de o matrice de ''N × N'' caractere, unde ''P'' este paznic, ''#'' este perete, iar ''.'' este zonă liberă. |
| - | // daca mini face move, ce ar face maxi? | + | |
| - | int score = alphabeta_maxi(alpha, beta, depth - 1); | + | |
| - | if (score <= alpha) { | + | **Date de ieșire:** O matrice ''N × N'' de numere întregi. Celulele conțin 0 dacă acolo este un paznic, -1 dacă este un perete, -2 dacă este o zonă în care nu poate ajunge niciun paznic, respectiv distanța minimă pentru zonele libere accesibile. |
| - | return alpha; // alpha cut-off | + | |
| - | } | + | |
| - | // dintre toate variantele pe care maxi le permite (!!!), | + | Problema se poate testa la: \\ |
| - | // mini o va alege pe cea cu scor minim | + | [[https://infoarena.ro/problema/muzeu|Infoarena - Muzeu]] |
| - | if (score < beta) { | + | |
| - | beta = score; | + | |
| - | } | + | |
| - | undo_move(move); // restaureaza starea de dinainte de move | + | ======= 4) Course Schedule ======= |
| - | } | + | |
| - | // cel mai bun scor pe care il poate obtine mini este beta | + | **Enunț:** Se dau ''N'' cursuri (numerotate de la 1 la ''N'') și ''M'' cerințe. Fiecare cerință are forma: "cursul ''A'' trebuie finalizat înainte de a putea începe cursul ''B''". Se cere găsirea unei ordini valide în care un student ar putea să finalizeze toate cele ''N'' cursuri respectând aceste condiții. |
| - | // (s-a tinut cont si de ce i-ar permite maxi, avand in vedere ca si el joaca optim) | + | |
| - | return beta; | + | |
| - | } | + | |
| - | </code> | + | **Date de intrare:** Numărul de cursuri ''N'' și numărul de cerințe ''M'', urmate de ''M'' perechi de numere ''(A, B)'' reprezentând dependențele. |
| - | </spoiler> | + | |
| + | **Date de ieșire:** O permutare a celor ''N'' cursuri care respectă cerințele date. Dacă este imposibil să se finalizeze toate cursurile, se va afișa textul "IMPOSSIBLE". | ||
| + | Problema se poate testa la: \\ | ||
| + | [[https://cses.fi/problemset/task/1679|CSES - Course Schedule]] | ||
| - | <spoiler Pseudocod Negamax with Alpha-beta> | + | ======= 5) Insule ======= |
| - | <code cpp> | + | |
| - | int evaluate(stare); // functia returneaza un scor asociat cu starea | + | |
| - | // functia returneaza mereu scorul din perspectiva jucatorului CURENT! | + | |
| - | + | ||
| - | + | ||
| - | void apply_move(move); // functia modifica starea curenta: executa miscarea move | + | |
| - | void undo_move(move); // functia restaureaza starea anterioara (de dinainte de executa lui move) | + | |
| - | + | ||
| - | // alege cea mai buna mutare pentru jucatorul CURENT (EU) | + | |
| - | // functia returneaza best_score | + | |
| - | int alphabeta_negamax(int aplha, int beta, int depth) { | + | |
| - | // daca jocul s-a terminat sau am atins nivelul maxim de recursivitate ales | + | |
| - | if (gameOver() || depth == 0 ) { | + | |
| - | return evaluate(); // ne oprim si evaluam starea curenta | + | |
| - | } | + | |
| - | + | ||
| - | // incercam pe rand fiecare care miscare posibila move | + | |
| - | for (move : all_moves) { | + | |
| - | apply_move(move); // executa move | + | |
| - | // incercam sa simulam jocul mai departe: | + | **Enunț:** O hartă este descrisă de o matrice cu ''N'' linii și ''M'' coloane. Elementele hărții pot fi 0 (apă) sau 1, 2, 3 (insule aparținând a trei țări diferite). Se cere aflarea numărului de insule din fiecare țară, precum și lungimea minimă a unui pod (format exclusiv din celule cu apă) care să unească o insulă a țării 1 de o insulă a țării 2. |
| - | // daca jucatorul CURENT face face move, ce ar face ADVERSARUL? | + | |
| - | int score = -alphabeta_negamax(-beta, -aplha depth - 1); // cel mai bine pentru el, | + | |
| - | // este cel mai rau pentru mine | + | |
| - | // si invers | + | |
| - | // dintre toate variantele pe care ADVERSARUL le permite (!!!), | + | **Date de intrare:** Dimensiunile hărții ''N'' și ''M'', urmate de matricea de caractere formata din ''0'', ''1'', ''2'' și ''3''. |
| - | // EU (jucatorul CURENT) o voi alege pe cea cu scor maxim | + | |
| - | if (score >= alpha) { | + | |
| - | alpha = score; | + | |
| - | } | + | |
| - | if (alpha >= beta) { | + | **Date de ieșire:** Patru numere: numărul de insule din grupa 1, grupa 2, grupa 3 și lungimea podului minim construit peste apă. |
| - | break; // cut-off | + | |
| - | } | + | |
| - | + | ||
| - | undo_move(move); // restaureaza starea de dinainte de move | + | |
| - | } | + | |
| - | // cel mai bun scor pe care il pot obtine EU este alpha | + | Problema se poate testa la: \\ |
| - | // (s-a tinut cont si de ce mi-ar permite ADVERSARUL, avand in vedere ca si el joaca optim) | + | [[https://infoarena.ro/problema/insule|Infoarena - Insule]] |
| - | return alpha; | + | |
| - | } | + | ===== Extra ===== |
| - | </code> | + | |
| - | </spoiler> | + | ==== Exerciții ==== |
| + | |||
| + | <spoiler BFS> | ||
| + | Se dă un graf **neorientat** cu **n** noduri și **m** muchii. Se mai dă un nod special **source**, pe care îl vom numi sursa. | ||
| + | |||
| + | Se cere să se găsească **numărul minim de muchii** ce trebuie parcurse de la **source** la **toate ** celelalte noduri. | ||
| + | |||
| + | <note warning> | ||
| + | Restricții și precizări: | ||
| + | * $n, m <= 10^5$ | ||
| + | * timp de execuție | ||
| + | * C++: 1s | ||
| + | * Java: 1s | ||
| + | </note> | ||
| <note> | <note> | ||
| - | Din nou remarcam claritatea si coerenta sporita a variantei negamax! | + | Rezultatul se va returna sub forma unui vector **d** cu **n** elemente. |
| + | |||
| + | Convenție: | ||
| + | * ** d[node] ** = numărul minim de muchii ce trebuie parcurse de la **source** la nodul **node** | ||
| + | * ** d[source] = 0 ** | ||
| + | * ** d[node] = -1**, dacă nu se poate ajunge de la **source** la **node** | ||
| </note> | </note> | ||
| - | ==== Complexitate ==== | ||
| - | In continuare prezentam complexitatile asociate algoritmilor prezentati anterior. | + | **Exemplu 1:** |
| - | Pentru aceasta vom introduce cateva notiuni: | + | |
| - | * **branch factor** : ** b ** = **numarul mediu de ramificari** pe care le are un nod neterminal (care nu e frunza) din ** arborele de solutii ** | + | $n = 5$ $m = 4$ $source = 3$ |
| - | * **depth** : **d ** = **adancimea maxima ** pana la care se face cautarea in arborele de solutii | + | |
| - | * orice nod de adancime d va fi considerat terminal | + | |
| - | <note> | + | $muchii: { (1,2); (1,3); (2,3); (2,4);} $ |
| - | Un arbore cu un branching factor **b**, care va fi examinat pana la un nivel **d** va furniza $b^d$ noduri frunze ce vor trebui procesate (ex. calculam scorul pentru acele noduri). | + | |
| - | <spoiler Explicatie> | + | Răspuns: |
| - | Nivelurile sunt notate cu $0, 1, 2, ..., d$ | + | |node|1|2|3|4|5| |
| - | * nivel 0: $1$ nod (radacina) | + | |d|1|1|0|2|-1| |
| - | * nivel 1: $b$ noduri | + | |
| - | * nivel 2: $b^2$ noduri | + | |
| - | * nivel 3: $b^3$ noduri | + | |
| - | * ... | + | |
| - | * nivel d: $b^d$ noduri | + | |
| - | </spoiler> | + | |
| - | </note> | + | |
| + | Explicație: | ||
| + | Graful dat este cel din figura urmatoare. | ||
| - | * **minimax/negamax** | + | {{pa:new_pa:lab07-bfs-example1.png}} |
| - | * Un algoritm **mini/negamax** clasic, care analizeaza toate starile posibile, va avea complexitatea $O(b ^ d)$ - deci exponentiala. | + | |
| - | * **alpha-beta** | + | * ** d[3] = 0 ** pentru că 1 este sursa |
| - | * Cat de bun este insa alpha-beta fata de un mini/nega-max naiv? Dupa cum s-a mentionat anterior, in functie de ordonarea miscarilor ce vor fi evaluate putem avea un caz cel mai favorabil si un caz cel mai defavorabil. | + | * ** d[1] = d[2] = 1 ** pentru că există muchie directă de la 2 la fiecare nod |
| - | * ** best case **: miscarile sunt ordonate descrescator dupa castig (deci ordonate optim), rezulta o complexitate | + | * ** d[4] = 2 ** pentru că trebuie să parcurgem 2 muchii ($3-2-4$) |
| - | * $O(b*1*b*1*b*1...de\ d \ ori...b*1)$ pentru d par | + | * ** d[5] = -1 ** pentru că nu se poate ajunge de la 3 la 5 |
| - | * $O(b*1*b*1*b*1...de \ d \ ori...b)$ pentru d impar | + | |
| - | * restrangand ambele expresii rezulta o complexitate $O(b ^ {\frac{d}{2}}) = O(\sqrt{b^d})$ | + | |
| - | * prin urmare, intr-un caz ideal, algoritmul alpha-beta poate explora de 2 ori mai putine nivele in arborele de solutii fata de un algoritm mini/nega-max naiv. | + | |
| - | * ** worst case**: miscarile sunt ordonate crescator dupa castigul furnizat unui jucator, astfel fiind necesara o examinare a tuturor nodurilor pentru gasirea celei mai bune miscari. | + | |
| - | * in consecinta complexitatea devine egala cu cea a unui algoritm mini/negamax naiv. | + | |
| - | + | ||
| - | + | ||
| - | ===== Concluzii si observatii ===== | + | |
| - | Alpha-beta NU ofera o alta solutie fata de Minimax! Este doar o optimizare pusa deasupra algoritmului Minimax care ne permite sa exploram mai multe stari in acelasi timp sau pentru acelasi numar de stari sa optinem un timp de doua ori mai mic. | + | |
| - | ===== Exemple ===== | + | **Exemplu 2:** |
| - | Dintre cele mai importante jocuri in care putem aplica direct strategia minimax, mentionam: | + | |
| - | * [[https://en.wikipedia.org/wiki/Tic-tac-toe | X și 0]] | + | |
| - | * joc foarte simplu/usor (spatiul starilor este mic). | + | |
| - | * Prin urmare tot arborele de solutii poate fi generat si explorat intr-un timp foarte scurt. | + | |
| - | * [[https://en.wikipedia.org/wiki/Chess | sah ]] | + | |
| - | * joc foarte greu (spatiul starilor este foarte mare) | + | |
| - | * minimax/negamax simplu poate merge pana la $ d = 7$ (nu reusea da bata campionul mondial la sah - campion uman) | + | |
| - | * alpha-beta poate merge pana la $d = 14$ | + | |
| - | * ** Deep Blue** a fost implementarea unui bot cu minimax si alpha-beta care a batut in 1997 campionul mondial la sah (Gary Kasparov). | + | |
| - | * [[https://en.wikipedia.org/wiki/Ultimate_tic-tac-toe | Ultimate tic-tac-toe]] | + | |
| - | * varianta mult mai grea de X si 0 (spatiul starilor foarte mare) | + | |
| - | * s-a dat la proiect PA 2016 :D | + | |
| - | * implemantarile se pot testa [[http://theaigames.com/competitions/ultimate-tic-tac-toe/rules | aici]] | + | |
| - | * [[https://en.wikipedia.org/wiki/Nim | Nim]] | + | |
| - | * [[https://en.wikipedia.org/wiki/Reversi| Reversi]] | + | |
| + | $n = 7$ $m = 7$ $source = 1$ | ||
| - | Alte exempe de jocuri interesante: | + | $muchii: { (1,2); (1,4); (2,3); (4,5); (5,6); (3,7); (7,6) } $ |
| - | * [[https://en.wikipedia.org/wiki/Go_(game) | Go]] | + | |
| - | * solutiile se bazeaza pe Monte Carlo Tree Search (nu pe minimax) | + | |
| - | * [[https://en.wikipedia.org/wiki/AlphaGo|AlphaGo]] este botul cel mai bun pe tabla de 19x19 | + | |
| - | + | ||
| - | ===Nim=== | + | |
| - | Fiind date 3 multimi de bile, fiecare jucator trebuie sa extraga la fiecare mutare 1, 2 sau 3 bile din oricare multime. | + | Răspuns: |
| + | |node|1|2|3|4|5|6|7| | ||
| + | |d|0|1|2|1|2|3|3| | ||
| - | **Cel care este fortat sa aleaga ultima bila, pierde.** | + | Explicație: |
| + | Graful dat este cel din figura urmatoare. | ||
| + | {{pa:new_pa:lab07-bfs-example2.png}} | ||
| - | ===Reversi game=== | + | * ** d[1] = 0 ** pentru că 1 este sursa |
| + | * ** d[2] = d[4] = 1 ** pentru că există muchie directă de la 2 la fiecare nod | ||
| + | * ** d[3] = d[5] = 2 ** pentru că trebuie să parcurgem 2 muchii ($1-2-3$, $1-4-5$) | ||
| + | * ** d[6] = d[7] = 3 ** pentru ca trebuie să parcurgem 3 muchii ($1-2-3-7$ sau $1-4-5-6$) | ||
| - | Tabla de joc consista dintr-un grid 6x6. Piesele pot fi reprezentate de monede, fiecarui jucator fiindu-i asociata o fata diferita a monezii. Jucatorii muta alternativ, dupa regula urmatoare: | + | **Exemplu 3:** |
| - | * Pozitia (x, y) in care este plasata piesa trebuie sa fie libera | + | $n = 7$ $m = 8$ $source = 1$ |
| - | * Trebuie sa existe o alta pozitie (x', y'), pe aceeasi linie, coloana sau diagonala a jucatorului aflat la mutare si toate pozitiile dintre (x, y) si (x', y') trebuie sa fie ocupate de piese ale adversarului | + | |
| - | * Piesele adversarului dintre (x, y) si (x', y') vor fi capturate, intorcandu-se monedele pe cealalta fata. | + | |
| - | Mai jos, de la stanga spre dreapta: pozitia initiala, posibilitatile de mutare ale primului jucator, tabla dupa prima mutare, posibilitatile de mutare ale celuilalt jucator, tabla dupa a doua mutare. | + | $muchii: { (1,2); (1,4); (2,3); (4,5); (5,6); (3,7); (7,6); (1, 6) } $ |
| - | {{:pa:laboratoare:1.png?200|}} | + | Răspuns: |
| - | {{:pa:laboratoare:2.png?200|}} | + | |node|1|2|3|4|5|6|7| |
| - | {{:pa:laboratoare:3.png?200|}} | + | |d|0|1|2|1|2|1|2| |
| - | {{:pa:laboratoare:4.png?200|}} | + | |
| - | {{:pa:laboratoare:5.png?200|}} | + | |
| + | Explicație: | ||
| + | Graful dat este cel din figura urmatoare. | ||
| - | Observatii: | + | {{pa:new_pa:lab07-bfs-example3.png}} |
| - | * Jucatorul poate acapara piese ale adversarului in mai multe directii simultan | + | |
| - | * Daca un jucator nu are unde muta, acesta cedeaza randul, adversarul efectuand a doua mutare la rand | + | |
| - | * Jocul se incheie cand nimeni nu mai poate muta, invingatorul fiind acela care detine cele mai multe piese proprii | + | |
| - | ===== Exercitii ===== | + | * ** d[1] = 0 ** pentru că 1 este sursa |
| - | <note> | + | * ** d[2] = d[4] = d[6] = 1 ** pentru că există muchie directă de la 2 la fiecare nod |
| - | In acest laborator vom folosi scheletul de laborator din arhiva {{pa:new_pa:skel-lab06.zip}}. | + | * ** d[3] = d[5] = d[7] = 2 ** pentru că trebuie să parcurgem 2 muchii ($1-2-3$, $1-4-5$, $1-6-7$) |
| - | </note> | + | </spoiler> |
| - | Vom implementa algoritmi pentru jocurile **Nim** si **Reversi**. | + | <spoiler Topological Sort> |
| + | Se dă un graf **orientat** aciclic cu **n** noduri și **m** arce. Se cere să se găsească **o sortare topologica** validă. | ||
| - | === Minimax Nim === | + | <note warning> |
| - | Se doreste implementarea algoritmulului minimax sau negamax pentru Nim. | + | Restricții si precizari: |
| - | <note> | + | * $n, m <= 10^5$ |
| - | Recomandam implementarea variantei Negamax. | + | * timp de executie |
| + | * C++: 1s | ||
| + | * Java: 1s | ||
| </note> | </note> | ||
| - | === Minimax Reversi === | ||
| - | Se doreste implementarea algoritmulului minimax sau negamax pentru Reversi. | ||
| <note> | <note> | ||
| - | Recomandam implementarea variantei Negamax. | + | Rezultatul se va returna sub forma unui vector **topsort** cu ** n ** elemente. |
| + | |||
| + | Vectorul **topsort** va reprezenta o permutare a multimii ${1, 2, 3,..., n}$ reprezentand sortarea topologica gasita. | ||
| </note> | </note> | ||
| + | **Exemplu 1:** | ||
| + | |||
| + | $n = 5$ $m = 4$ | ||
| + | |||
| + | $arce: { (1,2); (1,3); (2,3); (2,4);} $ | ||
| + | |||
| + | Răspuns: $topsort = [1, 2, 3, 4, 5] $ | ||
| + | |||
| + | Explicație: | ||
| + | Graful dat este cel din figura următoare. | ||
| + | |||
| + | {{pa:new_pa:lab07-topsort-example1.png}} | ||
| + | * deoarece avem arcele $1 \rightarrow 3$ si $1 \rightarrow 2$, 1 trebuie să apara înainte lui 2 și 3 | ||
| + | * deoarece avem arcul $2 \rightarrow 3$ si $2 \rightarrow 4$, 2 trebuie să apara înainte lui 3 și 4 | ||
| + | * 5 nu depinde de nimeni, poate să apară oriunde | ||
| + | |||
| + | Toate sortările topologice valide sunt: | ||
| + | * cele date de ordinea relativa a primelor 4 noduri: 1,2,3,4 | ||
| + | * $topsort = [1, 2, 3, 4, 5] $ | ||
| + | * $topsort = [1, 2, 3, 5, 4] $ | ||
| + | * $topsort = [1, 2, 5, 3, 4] $ | ||
| + | * $topsort = [1, 5, 2, 3, 4] $ | ||
| + | * $topsort = [5, 1, 2, 3, 4] $ | ||
| + | * cele date de ordinea relativa a primelor 4 noduri: (1,2,4,3) | ||
| + | * $topsort = [1, 2, 4, 3, 5] $ | ||
| + | * $topsort = [1, 2, 4, 5, 3] $ | ||
| + | * $topsort = [1, 2, 5, 4, 3] $ | ||
| + | * $topsort = [1, 5, 2, 4, 3] $ | ||
| + | * $topsort = [5, 1, 2, 4, 3] $ | ||
| + | |||
| + | **Exemplu 2:** | ||
| + | |||
| + | $n = 9$ $m = 8$ | ||
| + | |||
| + | $arce: { | ||
| + | (1,2); | ||
| + | (1,3); | ||
| + | (3,4); | ||
| + | (3,5); | ||
| + | (5,9); | ||
| + | (4,6); | ||
| + | (4,7); | ||
| + | (4,8); | ||
| + | } $ | ||
| + | |||
| + | Răspuns: $topsort = [1, 2, 3, 4, 6, 7, 8, 5, 9] $ | ||
| + | |||
| + | Explicație: | ||
| + | Graful dat este cel din figura următoare. | ||
| + | |||
| + | {{pa:new_pa:lab07-topsort-example2.png}} | ||
| + | |||
| + | Se observă din desen că soluția menționată este validă. | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Max Packet Size> | ||
| + | Rezolvați problema [[ https://www.hackerrank.com/contests/test-practic-pa-2018-v1-deadbeef/challenges/test-1-2-2-max-packet-size-medie/problem | Max Packet Size ]] pe Hackerrank. | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Labyrinth (CSES)> | ||
| + | **Enunț:** Se dă o hartă a unui labirint sub forma unei matrice. Se cere să se găsească un drum de lungime minimă de la o celulă de start 'A' la o celulă de destinație 'B'. Afișați lungimea drumului minim și o secvență de mutări (L, R, U, D) care descrie pașii. | ||
| + | |||
| + | Problema se poate testa la: \\ | ||
| + | [[https://cses.fi/problemset/task/1193 | CSES - Labyrinth]] | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Building Roads (CSES)> | ||
| + | **Enunț:** Se dă o rețea de ''N'' orașe și ''M'' drumuri bidirecționale existente. Se cere determinarea numărului minim de drumuri noi ce trebuie construite astfel încât să se poată ajunge din orice oraș în oricare altul din rețea. | ||
| + | |||
| + | **Hint:** Problema se reduce la aflarea numărului de componente conexe. | ||
| + | |||
| + | Problema se poate testa la: \\ | ||
| + | [[https://cses.fi/problemset/task/1666 | CSES - Building Roads]] | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Componente Conexe> | ||
| + | **Enunț:** Se dă un graf neorientat cu ''N'' noduri și ''M'' muchii. Se cere să se determine numărul de componente conexe din graf. O componentă conexă este un subgraf maximal în care există drum între oricare două noduri. | ||
| + | |||
| + | Problema se poate testa la: \\ | ||
| + | [[https://infoarena.ro/problema/dfs | Infoarena - DFS]] | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Sortare Topologică> | ||
| + | **Enunț:** Se dă un graf orientat aciclic (DAG) cu ''N'' noduri și ''M'' arce. Să se determine o sortare topologică a nodurilor sale. Aranjamentul trebuie să garanteze că pentru orice arc de la ''X'' la ''Y'', nodul ''X'' apare înaintea nodului ''Y'' în secvența finală. | ||
| + | |||
| + | Problema se poate testa la: \\ | ||
| + | [[https://infoarena.ro/problema/sortaret | Infoarena - Sortare Topologică]] | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Arbore3> | ||
| + | Rezolvați problema [[https://infoarena.ro/problema/arbore3 | arbore3]] pe Infoarena. | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Pokemon GO AWAY> | ||
| + | Rezolvați problema [[https://www.hackerrank.com/contests/test-practic-pa-2017-v2-meeseeks/challenges/test-2-pokemon-go-away-grea | Pokemon GO AWAY]] de la Testul Practic PA 2017. | ||
| + | |||
| + | **Hint:** Cu ce problemă seamănă? | ||
| + | </spoiler> | ||
| + | |||
| + | <spoiler Tsunami> | ||
| + | Rezolvați problema [[https://infoarena.ro/problema/tsunami | tsunami]] pe Infoarena. | ||
| + | </spoiler> | ||
| + | |||
| + | ===== Referințe ===== | ||
| - | === Bonus === | + | [0] Chapter **Elementary Graph Algorithms**, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein |
| - | Extindeti algoritmul implementat anterior pentru jocul **Reversi ** intr-un algoritm de tip alpha-beta pruning. | + | |
| - | Cum puteti sa comparati cei doi algoritmi implementati pentru Reversi? | + | |
| - | ===== Referinte ===== | + | |
| - | [1] [[http://en.wikipedia.org/wiki/Minimax]] | + | |
| - | [2] [[http://en.wikipedia.org/wiki/Negamax]] | + | [1] [[https://en.wikipedia.org/wiki/Breadth-first_search]] |
| - | [3] [[http://en.wikipedia.org/wiki/Alpha-beta_pruning]] | + | [2] [[https://en.wikipedia.org/wiki/Depth-first_search]] |
| - | [4] http://inst.eecs.berkeley.edu/~cs61b/fa14/ta-materials/apps/ab_tree_practice/ | + | [3] [[https://en.wikipedia.org/wiki/Topological_sorting]] |