This shows you the differences between two versions of the page.
|
pa:laboratoare:laborator-06 [2023/03/15 16:47] radu.nichita |
pa:laboratoare:laborator-06 [2026/05/06 19:39] (current) radu.nichita [Implementarea algoritmilor standard] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Laborator 05: Minimax ====== | + | ====== Laborator 06: Parcurgerea grafurilor. Aplicații (1/2) ====== |
| ===== Obiective laborator ===== | ===== Obiective laborator ===== | ||
| - | * Însușirea unor cunoștințe de bază despre **teoria jocurilor** precum și despre **jocurile de tip joc de sumă zero (suma nulă, zero-sum games)** | ||
| - | * Însușirea unor cunostințe elementare despre **algoritmii necesari** rezolvării unor probleme de joc de sumă zero (zero-sum game). | ||
| - | * Găsirea și compararea diverselor euristici pentru jocurile cunoscute, precum **șah**. | ||
| - | ===== Precizări inițiale ===== | + | * Î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. Vă sfătuim să vizionați integral [[https://youtu.be/STjW3eH0Cik| Search: Games, Minimax, and Alpha-Beta]] înainte să parcurgeți materialul de pe ocw. | + | |
| - | </note> | + | |
| - | ===== Introducere ===== | + | ===== Importanţă – aplicaţii practice ===== |
| - | **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). | + | Grafurile sunt utile pentru a modela diverse probleme și au numeroase aplicații practice: |
| - | 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-o mutare următoare a jucătorului advers. Concret, pentru o alegere într-un joc oarecare se preferă cea care aduce un risc minim dintre viitoarele mutări bune ale adversarului. | + | * 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) | ||
| - | 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ă. | + | ===== 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. | ||
| + | ==== Definiții ==== | ||
| - | ===== Descrierea algoritmului ===== | + | >> 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}**. |
| - | ==== Minimax ==== | + | >> 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**. |
| - | Ideea pe care se bazează algoritmul este că jucătorii adoptă următoarele strategii: | + | |
| - | *Jucătorul 1 (**Maxi**) va încerca mereu să-și **maximizeze** propriul câștig prin mutarea pe care o are de făcut; | + | >> 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**. |
| - | *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: | + | >> 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)**. |
| - | $$ Câștig_{Jucător_1} = | Pierdere_{Jucător_2} |$$ | + | >> Un graf **aciclic** este un graf (orientat/neorientat) care nu conține cicluri. |
| - | $$ Câștig_{Jucător_2} = | Pierdere_{Jucător_1} | $$ | + | |
| - | === Reprezentarea spațiului soluțiilor === | + | ==== Reprezentare ==== |
| + | 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. | ||
| + | Un graf poate fi modelat în mai multe moduri (folosind mai multe notații): | ||
| - | Î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). | + | * printr-o pereche de mulțimi $G = (V, E)$ |
| - | 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: | + | * $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** variază în funcție de paradigma de programare aleasă, de limbaj, precum și de gradul de optimizare avut în 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) | ||
| - | ==== Implementare ==== | + | 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. |
| - | Având noțiunile de bază asupra strategiei celor doi jucători, precum și a reprezentării spațiului soluțiilor problemei, o 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. Intuitiv, cele 2 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ăm, este varianta Negamax: | + | 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). |
| - | <spoiler Pseudocod Negamax> | + | ==== Colorare ==== |
| - | <code cpp> | + | Algoritmii de parcugere se pot folosi de o colorare a nodurilor: |
| - | // compute the state score for current player | + | |
| - | int evaluate(state, player); | + | |
| - | // apply the move on the current state: old_state -> new_state | + | * **white** (alb) = nod care nu a fost încă vizitat (nu este în coadă) |
| - | void apply_move(state, move); | + | |
| - | // undo the move and restore previous state: new_state -> old_state | + | |
| - | void undo_move(state, move); | + | |
| - | // check if any player won the game | + | * **gray** (gri) = nod care este în curs de vizitare (a fost adăugat în coadă) |
| - | bool game_over(state); | + | |
| - | // return the opponent for current player | + | * **black** (negru) = nod care a fost complet vizitat (node scos din coadă și pentru care s-a vizitat tot subarborele) |
| - | Player get_opponent(player); | + | |
| - | // compute the best score that player can get, | + | ==== Algoritmi de parcurgere ==== |
| - | // considering that the opponent also has an optimal strategy | + | Problemă: Să se parcurgă un graf dat. Fiecare nod se parcuge (exact) o singură dată. |
| - | int negamax(State& state, int depth, Player player) { | + | Algoritmi: |
| - | // STEP 1: game over or maximum recursion depth was reached | + | |
| - | if (game_over() || depth == 0) { | + | * **BFS** |
| - | return evaluate(state, player); | + | |
| + | * **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> | ||
| + | // do a BFS traversal from source | ||
| + | // | ||
| + | // source = the source for the BFS traversal | ||
| + | // nodes = list of all nodes from G | ||
| + | // adj[node] = the adjacency list of node | ||
| + | // example: adj[node] = {..., neigh, ...} => edge (node, neigh) | ||
| + | BFS(source, G=(nodes, adj)) { | ||
| + | // STEP 0: initialize results | ||
| + | // d[node] = distance from source to node | ||
| + | // p[node] = parent of node in the BFS traversal started from source | ||
| + | // [optional] color[node] = white/gray/black | ||
| + | // * white = not yet visited | ||
| + | // * 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; | ||
| } | } | ||
| - | // STEP 2: generate all possible moves for player | + | // STEP 1: initialize a queue |
| - | all_moves = get_all_moves(state, player); | + | q = {} |
| + | // STEP 2: add the source(s) into q | ||
| + | d[source] = 0; // distance from source to source | ||
| + | p[source] = null; // the source never has a parent (because it's the root of the traversal) | ||
| + | q.push(source); | ||
| + | // [optional] color[source] = gray; | ||
| - | // STEP 3: try to apply each move - compute best score | + | // STEP 3: start traversal using the node(s) from q |
| - | int best_score = -oo; | + | while (!q.empty()) { // while still have nodes to explore |
| - | for (move : all_moves) { | + | // STEP 3.1: extract the next node from queue |
| - | // STEP 3.1: do move | + | node = q.pop(); |
| - | apply_move(state, move); | + | |
| - | // STEP 3.2: play for the opponent | + | // [optional] STEP 3.2: print/use the node |
| - | int score = -negamax(state, depth - 1, get_opponent(player)); | + | |
| - | // 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 | + | // STEP 3.3: expand/visit the node |
| - | // best_move = move; | + | foreach (neigh in adj[node]) { // for each neighbour |
| + | if (d[node] + 1 < d[neigh]) { // a smaller distance <=> color[neigh] == white | ||
| + | d[neigh] = d[node] + 1; // update distance | ||
| + | p[neigh] = node; // save parent | ||
| + | q.push(neigh); // add neigh to the queue of nodes to be visited | ||
| + | // [optional] color[neigh] = gray; | ||
| + | } | ||
| } | } | ||
| + | | ||
| + | // [optional] color[node] = black; | ||
| + | } | ||
| + | } | ||
| + | </code> | ||
| - | // STEP 3.3: undo move | + | <note> |
| - | undo_move(state, move); | + | 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)$ | ||
| + | |||
| + | ===== DFS - Parcurgerea în adâncime ===== | ||
| + | |||
| + | 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. | ||
| + | |||
| + | Î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. | ||
| + | |||
| + | 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 | ||
| + | |||
| + | 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. | ||
| + | |||
| + | ==== 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; | ||
| } | } | ||
| - | // STEP 4: return best allowed score | + | timestamp = 0; // the first timestamp before the DFS traversal |
| - | // [optional] also return the best move | + | |
| - | return best_score; | + | foreach (node in nodes) { |
| + | if (p[node] == null) { // or [optional] color[node] == white | ||
| + | DFS_RECURSIVE(node, G, p, timestamp) | ||
| + | } | ||
| + | } | ||
| } | } | ||
| + | DFS_RECURSIVE(node, G=(node, adj), p, ref timestamp) { | ||
| + | start[node] = ++timestamp; // start visiting its subtree | ||
| + | // [optional] color[node] = gray; | ||
| + | |||
| + | for (neigh in adj[node]) { // for each neighbour | ||
| + | if (p[neigh] == null) { // or [optional] color[neigh] = white; | ||
| + | p[neigh] = node; // save parent | ||
| + | DFS_RECURSIVE(neigh, G, p, timestamp); // continue traversal | ||
| + | } | ||
| + | } | ||
| + | |||
| + | finish[node] = ++timestamp; // finish visiting its subtree | ||
| + | // [optional] color[node] = black; | ||
| + | } | ||
| </code> | </code> | ||
| - | </spoiler> | ||
| - | === De ce este nevoie de utilizarea unei adâncimi 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> | ||
| - | 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ă și devine impracticabilă din punctul de vedere al timpului consumat sau chiar a memoriei alocate (se vor discuta aceste aspecte în 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 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. | + | ==== 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ță. | ||
| - | 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). | + | 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. | ||
| - | De asemenea, se observă recursivitatea indirectă. Prin convenție acceptăm ca **începutul algoritmului** să fie cu jucătorul max. Astfel, se 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. | + | </note> |
| - | === Exemplu grafic === | + | 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. | ||
| - | {{pa:new_pa:minimax.jpg?700}} | + | >> **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). |
| - | 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}} | + | Pentru **graf orientat**, mai există încă 2 tipuri de muchii (arce): |
| - | Aceleași explicații ca la Minimax, cu câteva observații adiționale: | + | >> **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** . |
| - | * 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 === | + | <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**. | ||
| - | Până acum s-a discutat despre algoritmii Minimax / Negamax. Aceș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 necesar, mai 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ă). | + | Într-un graf **orientat** muchiile **(x, y)** și **(y, x)** sunt diferite, deci pot avea tipuri diferite! |
| + | </note> | ||
| - | 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ă.** | + | >> **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. | ||
| - | 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. | + | Într-un graf **orientat** muchiile **(x, y)** și **(y, x)** sunt diferite, deci pot avea tipuri diferite! |
| + | </note> | ||
| - | <spoiler De ce?> | + | <spoiler Analogie cu culori> |
| - | 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. | + | 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. |
| - | 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. | + | Î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> | ||
| - | 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ă. | + | ===== 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> | ||
| - | 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). | + | 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> | ||
| + | **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). | ||
| + | |||
| + | == Complexitate == | ||
| + | * $T(n) = O(n + m)$ | ||
| + | |||
| + | === 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> | ||
| - | === Implementare === | + | **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]$). |
| - | În continuare prezentăm o implementare conceptuală a Alpha-beta pentru varianta Negamax: | + | |
| + | == Complexitate == | ||
| + | * $T(n) = O(n + m)$ | ||
| - | <spoiler Pseudocod Negamax with Alpha-beta> | + | ==== Concluzie ==== |
| - | <code cpp> | + | Ambele variante au aceeasi complexitate. |
| - | // compute the state score for current player | + | * Algoritmul bazat pe DFS nu verifica daca graful este ciclic: presupune corectitudinea inputului. Este relativ mai simplu de implementat. |
| - | int evaluate(state, player); | + | * Algoritmul bazat pe BFS se poate folosi pentru a detecta daca graful este aciclic; in caz afirmativ, gaseste o sortare topologica valida. |
| - | // apply the move on the current state: old_state -> new_state | + | ===== TLDR ===== |
| - | void apply_move(state, move); | + | |
| - | // undo the move and restore previous state: new_state -> old_state | + | |
| - | void undo_move(state, move); | + | |
| - | // check if any player won the game | + | * Cele mai uzuale moduri de reprezentare a unui graf sunt: liste de adiacentă și matrice de adiacentă. |
| - | bool game_over(state); | + | |
| - | // return the opponent for current player | + | * Cele doua moduri uzuale de parcurgere a unui graf sunt: **BFS** și **DFS**. |
| - | Player get_opponent(player); | + | |
| - | // compute the best score that player can get, | + | * 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. |
| - | // considering that the opponent also has an optimal strategy | + | |
| - | int alphabeta_negamax(State& state, int depth, Player player, int alpha, int beta) { | + | |
| - | // STEP 1: game over or maximum recursion depth was reached | + | |
| - | if (game_over() || depth == 0) { | + | |
| - | return evaluate(state, player); | + | |
| - | } | + | |
| - | // STEP 2: generate all possible moves for player | + | ===== Exerciții ===== |
| - | // Note: sort moves descending by score (if possible) for maximizing the number of cut-off actions | + | |
| - | // (or generete the moves already sorted by a custom criterion) | + | |
| - | all_moves = get_all_moves(state, player); | + | |
| + | <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]]. | ||
| - | // STEP 3: try to apply each move - compute best score | + | Prin citirea acestor precizari vă asigurați ca: |
| - | int best_score = -oo; | + | * cunoasteți **convențiile** folosite |
| - | for (move : all_moves) { | + | * evitați **buguri** |
| - | // STEP 3.1: do move | + | * evitați **depunctări** la lab/teme/test |
| - | apply_move(state, move); | + | </note> |
| - | // STEP 3.2: play for the opponent | + | ===== Implementarea algoritmilor standard ===== |
| - | int score = -alphabeta_negamax(state, depth - 1, get_opponent(player), -beta, -alpha); | + | <note> |
| - | // opponent allows player to obtain this score if player will do current move. | + | [[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. |
| - | // player chooses this move only if it has a better score. | + | </note> |
| - | if (score > best_score) { | + | |
| - | best_score = score; | + | |
| - | // [optional]: the best move can be saved | + | <note warning> |
| - | // best_move = move; | + | Î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> |
| - | // STEP 3.3: update alpha (found a better move?) | + | ====== Pool probleme (pentru prezentări) ====== |
| - | if (best_score > alpha) { | + | |
| - | alpha = best_score; | + | |
| - | } | + | |
| - | // STEP 3.4: cut-off | + | ======= 1) Berării 2 ======= |
| - | // * already found the best possible score (alpha == beta) | + | |
| - | // 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.4: undo move | + | **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. |
| - | undo_move(state, move); | + | |
| - | } | + | |
| - | // STEP 4: return best allowed score | + | **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. |
| - | // [optional] also return the best move | + | |
| - | return best_score; | + | |
| - | } | + | |
| - | </code> | + | **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. |
| - | </spoiler> | + | |
| + | Problema se poate testa la: \\ | ||
| + | [[https://infoarena.ro/problema/berarii2|Infoarena - Berării 2]] | ||
| - | <note> | + | ======= 2) Alee ======= |
| - | O observație foarte importantă se poate face analizând **modul de funcționare** al acestui algoritm: este extrem de importantă **ordonarea mișcărilor după valoarea câștigului**. | + | |
| - | Î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. | + | **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). |
| - | În **cazul cel mai defavorabil** însă, în care mișcările sunt ordonate crescător după câștigul furnizat, Alpha-beta are aceeași complexitate cu Minimax/ Negamax, îmbunătățirea fiind nulă. | + | **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. |
| - | În **medie** se constată o eficiență sporită in practică pentru Alpha-beta. | + | **Date de ieșire:** Un singur număr reprezentând distanța minimă (numărul de pași) între cele două poziții. |
| + | |||
| + | Problema se poate testa la: \\ | ||
| + | [[https://infoarena.ro/problema/alee|Infoarena - Alee]] | ||
| + | |||
| + | ======= 3) Muzeu ======= | ||
| + | |||
| + | **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. | ||
| + | |||
| + | **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ă. | ||
| + | |||
| + | **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. | ||
| + | |||
| + | Problema se poate testa la: \\ | ||
| + | [[https://infoarena.ro/problema/muzeu|Infoarena - Muzeu]] | ||
| + | |||
| + | ======= 4) Course Schedule ======= | ||
| + | |||
| + | **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. | ||
| + | |||
| + | **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. | ||
| + | |||
| + | **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]] | ||
| + | |||
| + | ======= 5) Insule ======= | ||
| + | |||
| + | **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. | ||
| + | |||
| + | **Date de intrare:** Dimensiunile hărții ''N'' și ''M'', urmate de matricea de caractere formata din ''0'', ''1'', ''2'' și ''3''. | ||
| + | |||
| + | **Date de ieșire:** Patru numere: numărul de insule din grupa 1, grupa 2, grupa 3 și lungimea podului minim construit peste apă. | ||
| + | |||
| + | Problema se poate testa la: \\ | ||
| + | [[https://infoarena.ro/problema/insule|Infoarena - Insule]] | ||
| + | |||
| + | ===== Extra ===== | ||
| + | |||
| + | ==== 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> | ||
| - | === Exemplu grafic === | + | <note> |
| + | Rezultatul se va returna sub forma unui vector **d** cu **n** elemente. | ||
| - | {{pa:new_pa:minimax_alphabeta.jpg?700}} | + | 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> | ||
| - | === Iterative deepening === | + | **Exemplu 1:** |
| - | Pe scurt, iterative deepening reprezintă o strategie de optimizare a 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]] | + | $n = 5$ $m = 4$ $source = 3$ |
| + | $muchii: { (1,2); (1,3); (2,3); (2,4);} $ | ||
| - | ==== Complexitate ==== | + | Răspuns: |
| + | |node|1|2|3|4|5| | ||
| + | |d|1|1|0|2|-1| | ||
| - | Pentru a vedea complexitatea algoritmilor prezentați anterior, se vor introduce câteva noțiuni: | + | Explicație: |
| + | Graful dat este cel din figura urmatoare. | ||
| - | * **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 ** | + | {{pa:new_pa:lab07-bfs-example1.png}} |
| - | * **depth** : **d ** = **adâncimea maximă ** pană la care se face căutarea în arborele de soluții | + | |
| - | * orice nod de adâncime d va fi considerat terminal | + | |
| - | <note> | + | * ** d[3] = 0 ** pentru că 1 este sursa |
| - | Un arbore cu un branching factor **b**, care va fi examinat până la un nivel **d** va furniza $b^d$ noduri frunze ce vor trebui procesate (ex. calculăm scorul pentru acele noduri). | + | * ** d[1] = d[2] = 1 ** pentru că există muchie directă de la 2 la fiecare nod |
| + | * ** d[4] = 2 ** pentru că trebuie să parcurgem 2 muchii ($3-2-4$) | ||
| + | * ** d[5] = -1 ** pentru că nu se poate ajunge de la 3 la 5 | ||
| - | <spoiler Explicație> | + | **Exemplu 2:** |
| - | Nivelurile sunt notate cu $0, 1, 2, ..., d$ | + | |
| - | * nivel 0: $1$ nod (radacină) | + | $n = 7$ $m = 7$ $source = 1$ |
| - | * nivel 1: $b$ noduri | + | |
| - | * nivel 2: $b^2$ noduri | + | $muchii: { (1,2); (1,4); (2,3); (4,5); (5,6); (3,7); (7,6) } $ |
| - | * nivel 3: $b^3$ noduri | + | |
| - | * ... | + | Răspuns: |
| - | * nivel d: $b^d$ noduri | + | |node|1|2|3|4|5|6|7| |
| + | |d|0|1|2|1|2|3|3| | ||
| + | |||
| + | Explicație: | ||
| + | Graful dat este cel din figura urmatoare. | ||
| + | |||
| + | {{pa:new_pa:lab07-bfs-example2.png}} | ||
| + | |||
| + | * ** 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$) | ||
| + | |||
| + | **Exemplu 3:** | ||
| + | |||
| + | $n = 7$ $m = 8$ $source = 1$ | ||
| + | |||
| + | $muchii: { (1,2); (1,4); (2,3); (4,5); (5,6); (3,7); (7,6); (1, 6) } $ | ||
| + | |||
| + | Răspuns: | ||
| + | |node|1|2|3|4|5|6|7| | ||
| + | |d|0|1|2|1|2|1|2| | ||
| + | |||
| + | Explicație: | ||
| + | Graful dat este cel din figura urmatoare. | ||
| + | |||
| + | {{pa:new_pa:lab07-bfs-example3.png}} | ||
| + | |||
| + | * ** d[1] = 0 ** pentru că 1 este sursa | ||
| + | * ** d[2] = d[4] = d[6] = 1 ** pentru că există muchie directă de la 2 la fiecare nod | ||
| + | * ** d[3] = d[5] = d[7] = 2 ** pentru că trebuie să parcurgem 2 muchii ($1-2-3$, $1-4-5$, $1-6-7$) | ||
| </spoiler> | </spoiler> | ||
| + | |||
| + | <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ă. | ||
| + | |||
| + | <note warning> | ||
| + | Restricții si precizari: | ||
| + | * $n, m <= 10^5$ | ||
| + | * timp de executie | ||
| + | * C++: 1s | ||
| + | * Java: 1s | ||
| </note> | </note> | ||
| + | <note> | ||
| + | Rezultatul se va returna sub forma unui vector **topsort** cu ** n ** elemente. | ||
| - | * **minimax/negamax** | + | Vectorul **topsort** va reprezenta o permutare a multimii ${1, 2, 3,..., n}$ reprezentand sortarea topologica gasita. |
| - | * Un algoritm **Minimax / Negamax** clasic, care analizează toate stările posibile, va avea complexitatea $O(b ^ d)$ - deci exponențială. | + | </note> |
| - | * **alpha-beta** | + | **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 ===== | + | $n = 5$ $m = 4$ |
| - | 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 | + | |
| + | $arce: { (1,2); (1,3); (2,3); (2,4);} $ | ||
| - | ===== Exerciții ===== | + | Răspuns: $topsort = [1, 2, 3, 4, 5] $ |
| - | În lab06 nu există exerciții propriu-zise. | + | Explicație: |
| + | Graful dat este cel din figura următoare. | ||
| - | Task-uri: | + | {{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 | ||
| - | * Parcugeți teoria din acest laborator împreună cu asistentul. | + | Toate sortările topologice valide sunt: |
| - | * Trebuie să întelegeți și să comparați **Negamax** vs **Negamax cu Alpha-beta pruning**. | + | * cele date de ordinea relativa a primelor 4 noduri: 1,2,3,4 |
| - | * Indicație: Desenați exemplul din videoclipul de pe YouTube din sectiunea de **Precizări inițiale**. | + | * $topsort = [1, 2, 3, 4, 5] $ |
| - | * Discutați și comparați euristici pentru: | + | * $topsort = [1, 2, 3, 5, 4] $ |
| - | * șah | + | * $topsort = [1, 2, 5, 3, 4] $ |
| - | * table | + | * $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] $ | ||
| - | ===== Referințe ===== | + | **Exemplu 2:** |
| - | [0] [[http://en.wikipedia.org/wiki/Minimax | wikipedia.org/Minimax]] | + | |
| - | [1] [[http://en.wikipedia.org/wiki/Negamax | wikipedia.org/Negamax]] | + | $n = 9$ $m = 8$ |
| - | [2] [[http://en.wikipedia.org/wiki/Alpha-beta_pruning | wikipedia.org/Alpa-beta_pruning]] | + | $arce: { |
| + | (1,2); | ||
| + | (1,3); | ||
| + | (3,4); | ||
| + | (3,5); | ||
| + | (5,9); | ||
| + | (4,6); | ||
| + | (4,7); | ||
| + | (4,8); | ||
| + | } $ | ||
| - | [4] [[https://en.wikipedia.org/wiki/Monte_Carlo_tree_search | wikipedia.org/Monte_Carlo_tree_search]] | + | Răspuns: $topsort = [1, 2, 3, 4, 6, 7, 8, 5, 9] $ |
| - | [5] [[https://en.wikipedia.org/wiki/MTD-f] | wikipedia.org/MTD-f]] | + | Explicație: |
| + | Graful dat este cel din figura următoare. | ||
| - | [6] [[https://www.chessprogramming.org/Null_Window | chessprogramming.org/Null_Window]] | + | {{pa:new_pa:lab07-topsort-example2.png}} |
| - | [7] [[https://www.chessprogramming.org/Principal_Variation | chessprogramming.org/Principal_Variation]] | + | 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 ===== | ||
| - | [8] [[https://www.chessprogramming.org/Iterative_Deepening| chessprogramming.org/Iterative_deepening]] | + | [0] Chapter **Elementary Graph Algorithms**, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein |
| - | [9] [[https://www.aaai.org/Papers/AIIDE/2008/AIIDE08-036.pdf | Monte Carlo Tree Search]] | + | [1] [[https://en.wikipedia.org/wiki/Breadth-first_search]] |
| + | [2] [[https://en.wikipedia.org/wiki/Depth-first_search]] | ||
| + | [3] [[https://en.wikipedia.org/wiki/Topological_sorting]] | ||