This shows you the differences between two versions of the page.
|
pa:laboratoare:laborator-07 [2022/03/01 23:43] darius.neatu [Laborator 07: Parcurgerea grafurilor. Aplicații (1/2)] |
pa:laboratoare:laborator-07 [2026/04/21 00:56] (current) radu.nichita [6) Giant Pizza] |
||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ====== Laborator 07: Parcurgerea grafurilor. Aplicații (1/2) ====== | + | ====== Laborator 07: Parcurgerea grafurilor. Aplicații (2/2) ====== |
| ===== Obiective laborator ===== | ===== Obiective laborator ===== | ||
| - | * Înțelegerea conceptele de graf, reprezentare și parcugere | + | * Înțelegerea conceptelor de graf, reprezentare și parcugere |
| * Studierea unor aplicații pentru parcurgeri | * Studierea unor aplicații pentru parcurgeri | ||
| + | ===== Componente Conexe ===== | ||
| + | >> O **componentă conexă** (**CC**) / **Connected Component** (**CC**) într-un graf **neorientat** 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. | ||
| - | ===== Importanţă – aplicaţii practice ===== | + | <spoiler CC - exemplu 01> |
| + | $n = 6$ $m = 6$ | ||
| - | Grafurile sunt utile pentru a modela diverse probleme și au numeroase aplicații practice: | + | $muchii: { (1,2); (1,5); (2,5); (2,3); (3, 5); (4, 6);} $ |
| - | * Rețele de calculatoare (ex: stabilirea unei topologii fără bucle / arbore de acoperire) | + | {{pa:new_pa:lab08-cc-example01.png}} |
| - | * 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) | + | |
| + | Sunt **2 CC**-uri în graful dat: | ||
| + | * {1, 2, 3, 5} | ||
| + | * {4, 6} | ||
| - | ===== Grafuri ===== | + | Explicație: |
| - | 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. | + | * Cele 2 sunt mulțimi maximale pentru care se respectă proprietatea de conexitate. |
| + | * 4 și 6 nu sunt accesibile din nodurile 1, 2, 3 și 5, prin urmare, acestea trebuie să facă parte din componente diferite. | ||
| + | </spoiler> | ||
| - | ==== Definiții ==== | + | \\ |
| + | >> Un graf **neorientat** este **conex** dacă conține **o singură** componentă conexă. | ||
| - | >> 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}**. | + | <spoiler CC - exemplu 02> |
| + | $n = 6$ $m = 7$ | ||
| - | >> 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**. | + | $muchii: {(1, 2); (1, 5); (2, 5); (2, 3); (3, 5); (4, 6); (5, 4)} $ |
| - | >> 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**. | + | {{pa:new_pa:lab08-cc-example02.png}} |
| - | >> 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)**. | + | Graful dat este conex - există **1 CC**: {1, 2, 3, 4, 5, 6}. |
| - | >> Un graf **aciclic** este un graf (orientat/neorientat) care nu conține cicluri. | + | Explicație: Se poate ajunge de la oricare nod la oricare altul. |
| - | ==== Reprezentare ==== | + | </spoiler> |
| - | 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 ar grafului, poate influența performanța unei parcurgeri/unui algoritm. | + | |
| - | Un graf poate fi modelat în mai multe moduri (folosind mai multe notații): | + | <note> |
| + | O componentă conexă reprezintă o partiție a nodurilor în submulțimi! <=> Fiecare nod face parte dintr-o singură componentă conexă! | ||
| + | </note> | ||
| - | * printr-o pereche de mulțimi $G = (V, E)$ | + | ==== Algoritmi ==== |
| - | * $V$ = {v / v este un nod în graf} = mulțimea nodurile grafului (EN: nodes / vertices) | + | === DFS === |
| - | * $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. | + | <note> |
| + | CC cu DFS: | ||
| + | * În algoritmul clasic de parcurgere DFS, de fiecare dată când se găsește un nod fără părinte și se apeleză DFS_RECURSIVE, se descoperă o nouă componentă conexă. | ||
| + | * Toate nodurile vizitate în acel subarbore fac parte din aceeași componentă conexă. | ||
| + | </note> | ||
| - | * printr-o pereche $G = (nodes, a)$ | + | == Complexitate == |
| - | * $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) | + | |
| - | * printr-o pereche de mulțimi $G = (nodes, adj)$ | + | $T = O(n + m)$ |
| - | * $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) | + | |
| + | === BFS === | ||
| + | <note> | ||
| + | CC cu BFS: | ||
| + | * Se parcurge lista de noduri. | ||
| + | * Pentru fiecare nod care nu are părinte, se pornește o nouă parcurgere BFS din nodul curent. | ||
| + | * Toate nodurile vizitate într-o parcurgere BFS fac parte din aceeași componentă conexă. | ||
| + | * Observație: Se păstreză lista de părinți de la o parcurgere la alta. | ||
| + | </note> | ||
| + | == Complexitate == | ||
| - | 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. | + | $T = O(n + m)$ |
| - | Cele mai uzuale notații din laboratoarele de grafuri sunt descrise în [[https://ocw.cs.pub.ro/courses/pa/skel_graph | Precizări laboratoare 07-12]] (ex. $n$, $m$, $adj$, $adj\_trans$, $(x, y)$, etc). | + | <note warning> |
| + | Deși ambele abordări au aceeași complexitate, recomandăm abordarea cu DFS pentru simplitate. | ||
| + | </note> | ||
| + | ===== Componente Tare Conexe ===== | ||
| - | ==== Colorare ==== | + | >> O **componentă tare conexă** (**CTC**) / **Strongly Connected Component** (**SCC**) într-un graf **orientat** 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. |
| - | Algoritmii de parcugere se pot folosi de o colorare a nodurilor: | + | |
| - | * **white** (alb) = nod care nu a fost încă vizitat (nu este în coadă) | + | <spoiler SCC - exemplu 01> |
| + | $n = 6$ $m = 6$ | ||
| - | * **gray** (gri) = nod care este în curs de vizitare (a fost adăugat în coadă) | + | $arce: {(1, 2); (1, 5); (5, 2); (2, 3); (3, 5); (4, 6)} $ |
| - | * **black** (negru) = nod care a fost complet vizitat (node scos din coadă și pentru care s-a vizitat tot subarborele) | + | {{pa:new_pa:lab08-scc-example01.png}} |
| + | Sunt **4 SCC**-uri în graful dat: | ||
| + | * {1} | ||
| + | * {2, 3, 5} | ||
| + | * {4} | ||
| + | * {6} | ||
| - | ==== Algoritmi de parcurgere ==== | + | Explicație: |
| - | Problemă: Să se parcurgă un graf dat. Fiecare nod se parcuge (exact) o singură dată. | + | * În nodul 1 nu se poate ajunge, prin urmare acesta formează o componentă separată. Analog pentru 4. |
| - | Algoritmi: | + | * Similar, din nodul 6 nu se poate ajunge în alt nod, deci și acesta formeză singur o componentă. |
| + | * Nodurile 2, 3 și 5 formeză un ciclu, prin urmare se poate ajunge de la oricare la oricare. | ||
| + | </spoiler> | ||
| - | * **BFS** | + | \\ |
| + | >> Un graf **orientat** este **tare conex** dacă conține o **singură componentă** tare conexă. | ||
| - | * **DFS** | + | <spoiler SCC - exemplu 02> |
| + | $n = 6$ $m = 6$ | ||
| - | ===== BFS - Parcurgerea în lățime ===== | + | $arce: {(1, 2); (1, 5); (5, 2); (2, 3); (3, 5); (4, 6); (4, 1); (5, 4); (6, 5)} $ |
| - | 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. | + | {{pa:new_pa:lab08-scc-example02.png}} |
| + | Graful este tare conex - există **1 SCC**: {1, 2, 3, 4, 5, 6}; | ||
| - | 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. | + | Explicație: Se poate vedea că pentru fiecare nod x se poate ajunge în oricare alt nod y. |
| + | </spoiler> | ||
| + | <note> | ||
| + | O componentă tare conexă reprezintă o partiție a nodurilor în submulțimi! <=> Fiecare nod face parte dintr-o singură componentă tare conexă! | ||
| + | </note> | ||
| - | Pentru implementarea BFS se folosește o coadă. | + | ==== Algoritmi ==== |
| + | === TARJAN SCC === | ||
| + | [[https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm|Algoritmul lui Tarjan pentru SCC]] foloseşte o singură parcurgere DFS în urma căreia rezultă o pădure de arbori DFS. Componentele tare conexe vor fi subarbori în această pădure. Rădăcinile acestor subarbori se vor numi **rădăcinile componentelor tare conexe** (**SCC roots**). | ||
| - | ==== Algoritm ==== | + | Nodurile sunt puse pe o stivă, în ordinea vizitării. Când parcurgerea termină de vizitat un subarbore, se determină dacă rădăcina arborelui care s-a terminat de vizitat este și rădăcina unui SCC. Dacă un nod este rădăcina unei componente, atunci el şi toate de deasupra sa din stivă formează acea componentă tare conexă. |
| - | <code cpp| BFS> | + | |
| - | // 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 1: initialize a queue | + | Pentru a determina dacă un nod este rădăcina unei componente tare conexe, se definesc: |
| - | q = {} | + | <code cpp> |
| - | + | // the timestamp when node was found (when started to visit its subtree) | |
| - | // STEP 2: add the source(s) into q | + | found[node] = start[node]; |
| - | 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: start traversal using the node(s) from q | + | |
| - | while (!q.empty()) { // while still have nodes to explore | + | |
| - | // STEP 3.1: extract the next node from queue | + | |
| - | node = q.pop(); | + | |
| - | + | ||
| - | // [optional] STEP 3.2: print/use the node | + | |
| - | + | ||
| - | // STEP 3.3: expand/visit the node | + | |
| - | 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; | + | |
| - | } | + | |
| - | } | + | |
| + | // the minimum accessible timestamp that node can see/access | ||
| + | low_link[node] = min { found[x] | x is node OR x in ancestors(node) OR x in descendants(node) }; | ||
| </code> | </code> | ||
| - | <note> | + | >> **Tarjan SCC**: **node** is root for a SCC if **low_link[node] == found[node]**. |
| - | 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 ==== | + | <spoiler Explicații found+low_link> |
| - | *cu liste de adiacență: $O(n + m)$ sau $O(|V| + |E|)$ | + | **found[node]** reprezintă timpul de **start** din DFS, definit în laboratorul anterior. În implementare reținem o variabilă **timestamp** care se incrementează de fiecare dată când se vizitează un nod. Noua valoare a lui **timestamp** este **found[node]** (momentul la care **node** a fost găsit). |
| - | *cu matrice de adiacență: $O(n^2)$ sau $ O(|V|^2)$ | + | |
| - | ===== DFS - Parcurgerea în adâncime ===== | + | **low_link[node]** reprezintă cel mai mic timp de descoperire al unui nod **x** la care se poate ajunge pornind din **node** și mergând pe arcele/muchii nevizitate (se poate coborî sau urca). Practic, nodul cu cel mai mic timp de descoperire care se poate atinge prin traversarea a 0 sau mai multe arce. |
| - | + | * observații prelimilare: | |
| - | 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. | + | * nodurile vizitate înaintea lui **node** au valoare **found** mai mică (deci și orice strămoș a lui **node** - mulțimea **ancestors(node)**) |
| - | + | * nodurile descendente ale lui **node** au valoare **found** mai mare (mulțimea **descendants(node)**) | |
| - | Î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. | + | * întrucât și **node** face parte din subarbore, inițializăm **low_link[node] = found[node]**. Valoarea finală va fi mai mică sau egală decât aceasta (conform definiției, vom căuta un minim). |
| + | |||
| + | * după ce **toate** nodurile accesibile din **node** au fost vizitate, se cunoaște ***valoarea finală** a lui **low_link[node]** și putem avea 2 cazuri: | ||
| + | * **low_link[node] == found[node]** | ||
| + | * dacă valoarea finală a rămăs cea inițială, înseamnă că **NU** s-a urcat în arbore (altfel am fi întâlnit valori mai mici decât cea inițială) | ||
| + | * prin urmare **node** este rădăcina unui SCC (primul nod întâlnit din acest SCC) | ||
| + | * nodurile din vârful stivei de deasupra lui **node** formează SCC-ul găsit | ||
| + | * **low_link[node] < found[node] ** | ||
| + | * dacă valoarea finală pentru **low_link[node]** este mai mică decât cea inițială, înseamnă că s-a urcat în arbore | ||
| + | * în acest caz există cel puțin o muchie **(y, x)** unde **x** este strămoș și **y** este descendent pentru **node**, prin care **y** (si implicit și **node**) își actualizează minimul cu valoarea din **x** | ||
| + | * drumul **x - ... - node -... y - ... - x ** este atunci un ciclu care face parte dintr-un SCC; **node** este un nod oarecare dintr-un astfel de ciclu ("la mijloc", întrucât mai sus de el există cel puțin un nod mai aproape de "începutul ciclului", adică nodul **x**) | ||
| + | * prin urmare, suntem siguri că **node** nu este rădăcina unui SCC | ||
| + | </spoiler> | ||
| - | Pentru fiecare nod se vor reține: | + | == Algoritm == |
| - | * $start[node]$ = timestamp-ul / timpul descoperirii | + | <code cpp | TARJAN_SCC> |
| - | * $finish[node]$ = timestamp-ul / timpul finalizării | + | // Tarjan_SCC |
| - | * $p[node]$ = părintele din parcugerea DFS a lui node | + | // * visit all nodes with DFS |
| - | + | // * compute found[node] and low_link[node] | |
| - | + | // * extract SCCs | |
| - | 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 | DFS> | + | |
| - | // do a DFS traversal from all nodes | + | |
| // | // | ||
| // nodes = list of all nodes from G | // nodes = list of all nodes from G | ||
| // adj[node] = the adjacency list of node | // adj[node] = the adjacency list of node | ||
| - | // example: adj[node] = {..., neigh, ...} => edge (node, neigh) | + | // example: adj[node] = {..., neigh, ...} => edge (node, neigh) |
| - | // | + | TARJAN_SCC(G = (nodes, adj)) { |
| - | DFS(G=(nodes, adj)) { | + | // STEP 1: initialize results |
| - | // STEP 0: initialize results | + | // parent[node] = parent of node in the DFS traversal |
| - | // 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 | + | // the timestamp when node was found (when started to visit its subtree) |
| - | // finish[node] = the timestamp (the order) when we finished visiting the node subtree | + | // Note: The global timestamp is incremented everytime a node is found. |
| - | // [optional] color[node] = white/gray/black | + | // |
| - | // * white = not yet visited | + | // the minimum accessible timestamp that node can see/access |
| - | // * gray = visit in progress | + | // low_link[node] = min { found[x] | x is node OR x in ancestors(node) OR x in descendants(node) }; |
| - | // * black = visited | + | // |
| foreach (node in nodes) { | foreach (node in nodes) { | ||
| - | p[node] = null; // parent not yet found | + | parent[node] = null; // parent not yet found |
| - | // [optional] color[node] = white; | + | found[node] = +oo; // node not yet found |
| + | low_link[node] = +oo; // value not yet computed | ||
| } | } | ||
| + | nodes_stack = {}; // visiting order stack | ||
| - | timestamp = 0; // the first timestamp before the DFS traversal | + | // STEP 2: visit all nodes |
| + | timestamp = 0; // global timestamp | ||
| foreach (node in nodes) { | foreach (node in nodes) { | ||
| - | if (p[node] == null) { // or [optional] color[node] == white | + | if (parent[node] == null) { // node not visited |
| - | DFS_RECURSIVE(node, G, p, timestamp) | + | parent[node] = node; // convention: the parent of the root is actually the root |
| + | |||
| + | // STEP 3: start a new DFS traversal this subtree | ||
| + | DFS(node, adj, parent, timestamp, found, low_link, nodes_stack); | ||
| } | } | ||
| } | } | ||
| } | } | ||
| - | DFS_RECURSIVE(node, G=(node, adj), p, ref timestamp) { | + | DFS(node, adj, parent, ref timestamp, found, low_link, nodes_stack) { |
| - | start[node] = ++timestamp; // start visiting its subtree | + | // STEP 1: a new node is visited - increment the timestamp |
| - | // [optional] color[node] = gray; | + | found[node] = ++timestamp; // the timestamp when node was found |
| + | low_link[node] = found[node]; // node only knows its timestamp | ||
| + | nodes_stack.push(node); // add node to the visiting stack | ||
| - | for (neigh in adj[node]) { // for each neighbour | + | // STEP 2: visit each neighbour |
| - | if (p[neigh] == null) { // or [optional] color[neigh] = white; | + | foreach (neigh in adj[node]) { |
| - | p[neigh] = node; // save parent | + | // STEP 3: check if neigh is already visited |
| - | DFS_RECURSIVE(neigh, G, p, timestamp); // continue traversal | + | if (parent[neigh] != null) { |
| + | // STEP 3.1: update low_link[node] with information gained through neigh | ||
| + | // note: neigh is in the same SCC with node only if it's in the visiting stack; | ||
| + | // otherwise, neigh is from other SCC, so it should be ignored | ||
| + | if (neigh in nodes_stack) { | ||
| + | low_link[node] = min(low_link[node], found[neigh]); | ||
| + | } | ||
| + | |||
| + | continue; | ||
| } | } | ||
| + | |||
| + | // STEP 4: save parent | ||
| + | parent[neigh] = node; | ||
| + | |||
| + | // STEP 5: recursively visit the child subtree | ||
| + | DFS(neigh, adj, parent, timestamp, found, low_link, nodes_stack); | ||
| + | |||
| + | // STEP 6: update low_link[node] with information gained through neigh | ||
| + | low_link[node] = min(low_link[node], low_link[neigh]); | ||
| } | } | ||
| - | finish[node] = ++timestamp; // finish visiting its subtree | + | // STEP 7: node is root in a SCC if low_link[node] == found[node] |
| - | // [optional] color[node] = black; | + | // (there is no edge from a descendant to an ancestor) |
| + | if (low_link[node] == found[node]) { | ||
| + | // STEP 7.1: pop all elements above node from stack => extract the SCC where node is root | ||
| + | new_scc = {}; | ||
| + | do { | ||
| + | x = nodes_stack.pop(); | ||
| + | new_scc.push(x); | ||
| + | } while (x != node); // stop when node was popped from the stack | ||
| + | |||
| + | // STEP 7.2: save / print the new SCC | ||
| + | print(new_scc); | ||
| + | } | ||
| } | } | ||
| </code> | </code> | ||
| - | <note> | + | Observații: |
| - | Liniile cu **[optional]** se referă la logica de colorare menționată anterior, care se poate omite (dacă nu se dorește acest rezultat). | + | * La pasul **3.1** se încearcă actualizarea lui **low_link[node]** cu informația din **neigh** doar dacă **neigh** este în stivă. |
| - | </note> | + | * Nodul **neigh** are deja părinte, deci poate fi în unul din următoare 2 cazuri: |
| + | * **neigh** este în curs de vizitare (deci este în stivă) => **neigh** este strămoș a lui **node** | ||
| + | * Reactualizăm **low_link[node]** cu valoarea din **neigh**. | ||
| + | * **neigh** este deja vizitat (deci a fost scos din stivă) => **neigh** face parte din alt subarbore, terminat anterior. | ||
| + | * Prin urmare, anterior s-a stabilit că **neigh** face parte dintr-un alt SCC și trebuie ignorat (întrucât sigur are valoare **found** mai mică decât a lui **node** și ar reactualiza **low_link[node]** în mod eronat. | ||
| + | * Se face această actualizare doar dacă **neigh** este strămoș al lui | ||
| + | == Complexitate == | ||
| + | * **complexitate temporală **: $T = O(n + m)$ | ||
| + | * **complexitate spațială ** : $S = O(n)$ | ||
| + | * recursivitate + câteva structuri de date de lungime $O(n)$ | ||
| - | ==== Complexitate ==== | + | === Kosaraju === |
| - | *cu liste de adiacență: $O(n + m)$ sau $O(|V| + |E|)$ | + | Există și alt algoritm pentru determinarea componentelor tare conexe. Algoritmul lui Kosaraju se bazează pe compactarea ciclurilor. Deoarece are aceeași complexitate ca și Tarjan, nu îl vom studia la laborator la PA. Am ales algoritmul lui Tarjan întrucât îl putem modifica ușor pentru a produce și alte rezultate. |
| - | *cu matrice de adiacență: $O(n^2)$ sau $ O(|V|^2)$ | + | |
| + | Puteți consulta următoarele materiale dacă doriți să aflați mai multe: | ||
| + | * https://www.youtube.com/watch?v=RpgcYiky7uw | ||
| + | * https://iq.opengenus.org/kosarajus-algorithm-for-strongly-connected-components/ | ||
| + | ===== Puncte de articulație ===== | ||
| - | ==== Tipuri de muchii/arce în parcurgerea DFS ==== | + | >> **Punct de articulație** / **nod critic** / **Cut Vertex** (**CV**) este un nod într-un graf **neorientat** a cărui eliminare duce la creșterea numărului de componente conexe (CC) - se elimină nodul împreună cu muchiile incidente. |
| - | >> **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: | + | <spoiler CV - exemplu 01> |
| - | * 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. | + | $n = 8$ $m = 6$ |
| - | * 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. | + | |
| - | </note> | + | $muchii: {(1, 2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $ |
| - | Putem folosi o parcurgere DFS pentru a clasifica tipurile de muchii (toate muchiile din graf) relativ la arborele DFS curent. | + | {{pa:new_pa:lab08-cv-example01.png}} |
| - | >> **tree-edge** (**T**) / muchie de arbore = muchie **(x, y)** care conectează un nod x de copilul său y din arbore. | + | |
| - | >> **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). | + | Sunt **3 CV**-uri în graful dat: 1, 5 și 7. |
| - | Pentru **graf orientat**, mai există încă 2 tipuri de muchii (arce): | + | Explicație: |
| - | >> **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** . | + | * Dacă ștergem nodul 1, graful se sparge în 2 CC-uri: {2, 3, 4}, {5, 6, 7, 8}. |
| + | * Dacă ștergem nodul 5, graful se sparge în 2 CC-uri: {1, 2, 3, 4}, {6, 7, 8}. | ||
| + | * Dacă ștergem nodul 7, graful se sparge în 2 CC-uri: {1, 2, 3, 4, 5, 6}, {8}. | ||
| + | * Dacă ștergem oricare alt nod, ,graful rămâne conex. | ||
| + | </spoiler> | ||
| - | <note> | + | ==== TARJAN CV ==== |
| - | Î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**. | + | Putem modifica ușor algoritmul TARJAN SCC astfel încât să obținem [[https://en.wikipedia.org/wiki/Biconnected_component | Algoritmul lui Tarjan pentru CV]]. |
| - | Într-un graf **orientat** muchiile **(x, y)** și **(y, x)** sunt diferite, deci pot avea tipuri diferite! | + | În mod analog, pentru a determina dacă un nod este CV, se definesc și folosesc **found** și **low_link**. |
| - | </note> | + | |
| - | >> **cross-edge** (**C**) / muchie de traversare = muchie **(x, y)** care conectează un nod x de un nod y din alt subarbore. | + | >> **TARJAN CV**: **node** is **CV** if |
| - | <note> | + | >>**i)** node is NOT root and **low_link[neigh] >= found[node]** for at least one **neigh** in **adj[node]** |
| - | Î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. | + | >> OR |
| + | >> **ii)** node is root and children(node) > 1 | ||
| - | Într-un graf **orientat** muchiile **(x, y)** și **(y, x)** sunt diferite, deci pot avea tipuri diferite! | + | Dacă **node** este rădăcină într-un subarbore, acesta are valoarea **found** mai mică decât a oricărui nod. Prin urmare, condiția **low_link[neigh] >= found[node]** ar fi adevărată mereu și nu ne-ar putea furniza o informație utilă. De aceea, cazul **i)** nu este aplicabil pentru rădăcină. Putem trata foarte simplu cazul pentru rădăcină folosind **ii)**: dacă **node** este rădăcină a unui subarborele și are cel puțin 2 copii, atunci, prin eliminarea lui **node**, arborele acestuia se sparge într-un număr de subarbori egal cu numărul de copii. |
| - | </note> | + | |
| - | <spoiler Analogie cu culori> | + | <spoiler Explicații found+low_link> |
| - | 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. | + | **found[node]** are aceeași semnificație ca la SCC. |
| + | |||
| + | **low_link[node]** are aceeași semnificație ca la SCC. | ||
| + | * observații/diferențe: | ||
| + | * Nu este nevoie de folosirea stivei de vizitare. | ||
| + | * La SCC aveam nevoie de stiva de noduri pentru a nu folosi o muchie **node -> neigh** (arc) care unea 2 SCC-uri. | ||
| + | * Într-un graf neorientat nu putem avea o muchie care să unească 2 subarbori, deoarece în momentul în care un capăt este vizitat, adaugă și celălalt capăt în același subarbore. | ||
| + | * **neigh** este copil al lui **node** în parcurgerea DFS => **found[neigh] > found[node]** | ||
| + | * după ce un copil **neigh** este vizitat, se cunoaște ***valoarea finală** a lui **low_link[neigh]** și putem avea 2 cazuri: | ||
| + | * **low_link[neigh] < found[node] ** | ||
| + | * inițial **low_link[neigh] = found[neigh]**, deci **low_link[neigh] > found[node]** | ||
| + | * în acest caz există cel puțin o muchie **(y, x)** unde **x** este strămoș și **y** este descendent pentru **node** prin care **y** (și implicit și **node**) își actualizează minimul cu valoarea din **x** | ||
| + | * drumul **x - ... - node - neigh - ... y - x ** este atunci un ciclu | ||
| + | * dacă **node** este eliminat din graf, toate nodurile din subarborele lui **neigh** vor rămâne conectate de restul grafului prin muchia **(y, x)** | ||
| + | * deci nu putem trage vreo concluzie doar analizând vecinul curent **neigh**, trecem la următorul | ||
| + | * **low_link[neigh] >= found[node] ** | ||
| + | * în acest caz nu există ciclul **x - ... - node - neigh - ... y - x ** de la cazul anterior | ||
| + | * eliminarea lui **node** ar duce la separarea subarborelui lui **neigh** de restul grafului | ||
| + | * prin urmare, numărul de componente conexe crește cu cel puțin 1, deci **node** este sigur un **CV** (concluzie corectă chiar dacă ne-am uitat la un singur vecin **neigh**) | ||
| - | Î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> | </spoiler> | ||
| - | ===== Aplicații parcurgeri ===== | + | \\ |
| + | \\ | ||
| + | Punem la dispoziție un diff de pseudocod: [[https://pastebin.com/raw/8th3Pnjg | TARJAN_SCC vs TARJAN_CV]]. Se observă că este același algoritm, singurele diferențe relevante sunt: | ||
| + | * STEP **3.1**: condiția după care se reactulizează **low_link[node]** în funcție de **neigh** atunci când cel din urmă este deja vizitat | ||
| + | * STEP **7**: condiția prin care se determină dacă **node** este o rădăcină de SCC / CV. | ||
| + | \\ | ||
| - | * Componente Conexe | + | == Complexitate == |
| - | * Sortarea Topologică | + | * **complexitate temporală **: $T = O(n + m)$ |
| - | * Componente Tare-Conexe | + | * **complexitate spațială ** : $S = O(n)$ |
| - | * Componente Biconexe | + | * recursivitate + câteva tablouri auxiliare de lungime n |
| - | + | ||
| - | În acest laborator vom studia doar problema sortare topologică. | + | |
| - | ===== TopSort - Sortarea Topologică ===== | + | ===== Punți / muchii critice ===== |
| + | >> **Punte** / **muchie critică** / **Critical Edge** (**CE**) este o muchie într-un graf **neorientat** a cărei eliminare duce la creșterea numărului de componente conexe (CC) - se elimină muchia, fără a se sterge capetele (nodurile) acesteia. | ||
| - | ==== Problemă ==== | + | <spoiler CE - exemplu 01> |
| + | $n = 8$ $m = 6$ | ||
| - | >> O **sortare topologică** într-un **graf orientat aciclic** reprezintă o aranjare/permutare a nodurilor din graf care ține cont de arce. | + | $muchii: { (1,2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $ |
| - | 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. | + | {{pa:new_pa:lab08-ce-example01.png}} |
| - | <note> | + | Sunt **2 CE**-uri în graful dat: (1, 5) și (7,8) |
| - | 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> | + | Explicație: |
| - | {{pa:new_pa:lab07-topsort-example1.png}} | + | * Dacă ștergem muchia (1, 5), graful se sparte în 2 CC-uri: {1, 2, 3, 4}, {5, 6, 7, 8}. |
| + | * Dacă ștergem muchia (7, 8), graful se sparte în 2 CC-uri: {1, 2, 3, 4, 5, 6, 7}, {8}. | ||
| + | * Dacă ștergem oricare altă muchie, graful rămâne conex. | ||
| + | </spoiler> | ||
| - | În figura anterioară avem un graf cu: | + | ==== TARJAN CE ==== |
| - | * $n = 5$ $m = 4$ | + | Se modifică algoritmul de CV. Se folosesc aceleași definiții și semnificații pentru **found** și **low_link**. |
| - | * $arce: { (1,2); (1,3); (2,3); (2,4);} $ | + | |
| + | >> **TARJAN CE**: **(node, neigh)** is a **CE** if **low_link[neigh] > found[node]** where **neigh** in **adj[node]**. | ||
| - | Toate sortările topologice valide sunt: | + | <spoiler Explicație> |
| - | * cele date de ordinea relativa a primelor 4 noduri: (1,2,3,4) | + | Există 2 tipuri de muchii în parcugerea DFS într-un graf neorientat: |
| - | * $topsort = [1, 2, 3, 4, 5] $ | + | * **(node, neigh)**: muchiile din arbore (numită și muchie de arbore) |
| - | * $topsort = [1, 2, 3, 5, 4] $ | + | * **(y, x)**: muchie de la un nod **y** la un strămoș **x** (numită și muchie înapoi) |
| - | * $topsort = [1, 2, 5, 3, 4] $ | + | Tipul al 2-lea de muchie închide un ciclu, deci clar nu reprezintă un CE. Prin urmare trebuie să căutăm toate CE-urile printre muchiile **(node, neigh)** din arbore. |
| - | * $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] $: | + | Când se termină de vizitat subarborele lui **neigh** și cunoaștem valoarea finală a lui **low_link[neigh]** putem avea: |
| - | * deoarece avem arcele $1 \rightarrow 3$ si $1 \rightarrow 2$, 1 trebuie să apara înainte lui 2 și 3 | + | * **low_link[neigh] <= found[node]**: |
| - | * deoarece avem arcul $2 \rightarrow 3$ si $2 \rightarrow 4$, 2 trebuie să apara înainte lui 3 și 4 | + | * analog explicațiilor de la CV, din subarborele lui **neigh** se poate urca până la un nod **x** (x este **node** SAU un strămoș al lui **node**) => muchia **(node, neigh)** face parte dintr-un ciclu |
| - | * 5 nu depinde de nimeni, poate să apară oriunde | + | * prin urmare, dacă se taie aceasta, toate nodurilor de pe ciclu rămân conectate, deci nu este CE |
| + | * **low_link[neigh] > found[node]**: | ||
| + | * înseamnă că nu există acel ciclu de la pasul anterior (nu s-a putut urca în arbore mai sus de **node**) | ||
| + | * prin urmare, **(node, neigh)** este **CE** | ||
| </spoiler> | </spoiler> | ||
| - | ==== Algoritmi ==== | ||
| - | Sunt doi algoritmi cunoscuti pentru sortarea topologică. | ||
| - | |||
| - | === TopSort - DFS: sortare descrescătoare după timpul de finalizare === | ||
| - | <note> | ||
| - | 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 == | == Complexitate == | ||
| - | * $T(n) = O(n+ m)$ | + | * **complexitate temporală **: $T = O(n + m)$ |
| + | * **complexitate spațială ** : $S = O(n)$ | ||
| + | * recursivitate + câteva structuri de date de lungime $O(n)$ | ||
| - | === TopSort - BFS: algoritmul lui Kahn === | + | ===== Componente Biconexe ===== |
| - | <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> | + | |
| - | **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]$). | + | >> O **componentă biconexă** / **BiConnected Component** (**BCC**) într-un graf **neorientat** este o submulțime maximală de noduri cu proprietatea că nu conține puncte de articulație - oricare nod s-ar elimina, nodurile rămase sunt încă conectate. |
| - | == Complexitate == | + | <spoiler BCC - exemplu 01> |
| - | * $T(n) = O(n+ m)$ | + | $n = 8$ $m = 9$ |
| + | $muchii: {(1, 2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $ | ||
| - | ==== Concluzie ==== | + | {{pa:new_pa:lab08-bcc-example01.png}} |
| - | 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. | + | |
| - | ===== TLDR ===== | + | |
| - | * Cele mai uzuale moduri de reprezentare a unui graf sunt: liste de adiacentă și matrice de adiacentă. | + | Sunt **4 BCC**-uri în graful dat: |
| + | * {1, 2, 3, 4} | ||
| + | * {1, 5} | ||
| + | * {5, 6, 7} | ||
| + | * {7, 8} | ||
| - | * Cele doua moduri uzuale de parcurgere a unui graf sunt: **BFS** și **DFS**. | + | Explicație: |
| + | * Dacă ștergem muchia (1, 5), graful se sparge în 2 CC-uri: {1, 2, 3, 4}, {5, 6, 7, 8}. | ||
| + | * Dacă ștergem muchia (7, 8), graful se sparge în 2 CC-uri: {1, 2, 3, 4, 5, 6, 7}, {8}. | ||
| + | * Dacă ștergem oricare altă muchie, graful rămâne conex. | ||
| + | </spoiler> | ||
| - | * 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. | + | \\ |
| + | >> Un graf **neorientat** este **biconex** dacă nu conține puncte de articulație - conține o singură componentă biconexă. | ||
| - | ===== Exerciții ===== | + | <spoiler BCC - exemplu 02> |
| + | $n = 8$ $m = 10$ | ||
| + | $muchii: {(1, 2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8); (8, 2)} $ | ||
| - | <note warning> | + | {{pa:new_pa:lab08-bcc-example02.png}} |
| - | Î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 07-12]]. | + | |
| - | Prin citirea acestor precizari vă asigurați ca: | + | Sunt **1 BCC**-uri în graful dat: {1, 2, 3, 4, 5, 6, 7, 8} |
| - | * cunoasteți **convențiile** folosite | + | |
| - | * evitați **buguri** | + | |
| - | * evitați **depunctări** la lab/teme/test | + | |
| - | </note> | + | Explicație: |
| - | + | * Nu există noduri/puncte critice în graf (se poate șterge orice nod și graful rămâne conex). | |
| - | <note> | + | </spoiler> |
| - | Scheletul de laborator se găsește pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/skel/lab07|pa-lab::skel/lab07]]. | + | |
| - | </note> | + | |
| <note warning> | <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]]. | + | Împărțirea în componente biconexe a unui graf neorientat reprezintă **o partiție disjunctă a muchiilor grafului** (împreună cu vârfurile adiacente muchiilor). Acest lucru implică faptul că unele vârfuri pot face parte din mai multe componente biconexe diferite (vezi BCC - exemplu 01) - mai exact, punctele de articulație vor face parte din mai multe componente. |
| </note> | </note> | ||
| + | ==== TARJAN BCC ==== | ||
| + | Se modifică algoritmul de CV. Se folosesc aceleași definiții și semnificații pentru **found** și **low_link**. | ||
| - | === BFS === | + | * Se folosește o stivă **edges_stack** în care se adaugă toate muchiile **(node, neigh)** atunci când se înaintează în recursivitate. |
| - | Se dă un graf **neorientat** cu **n** noduri și **m** muchii. Se mai dă un nod special **source**, pe care îl vom numi sursa. | + | * Atunci când se termină de vizitat un copil **neigh**, dacă se îndeplinește condiția de **CV** (**low_link[neigh] >= found[node]**), înseamnă că prin eliminarea lui **node** tot subarborele **node - neigh - ...** rămâne deconectat. Prin urmare, toate muchiile din stivă de deasupra muchiei **(node, neigh)** (inclusiv) formează o componentă biconexă (mulțimea de noduri formată din capetele acestor muchii). |
| + | * Se termină de vizitat copilul curent și se trece la următorul. De fiecare dată când se găsește un copil **neigh** cu **low_link[neigh] >= found[node]** se formează o nouă **BCC**. | ||
| - | Se cere să se găsească **numărul minim de muchii** ce trebuie parcurse de la **source** la **toate ** celelalte noduri. | + | == Complexitate == |
| + | * **complexitate temporală **: $T = O(n + m)$ | ||
| + | * **complexitate spațială ** : $S = O(n + m)$ | ||
| + | * recursivitate + câteva structuri de date de lungime $O(n)$ / $O(m)$ | ||
| + | * ATENȚIE! În plus, față de CE/CV , se stochează o stivă de muchii. | ||
| - | <note warning> | + | ===== Importanţă – aplicaţii practice ===== |
| - | Restricții și precizări: | + | * SCC: Data Mining, Compilatoare, problema 2-SAT. |
| - | * $ n, m <= 10^5 $ | + | * BCC: cele mai importante aplicații se găsesc în rețelele de calculatoare, deoarece un BCC asigură redundanţă (există cel puțin 2 căi de a conecta o entitate la celelalte). |
| - | * timp de execuție | + | |
| - | * C++: 1s | + | ===== TLDR ===== |
| - | * Java: 1s | + | |
| - | </note> | + | |
| - | <note> | + | * Se poate folosi/modifica algoritmul lui Tarjan pentru a determina **SCC**, **CV** / **CE** / **BCC**. |
| - | Rezultatul se va returna sub forma unui vector **d** cu **n** elemente. | + | * Deoarece algoritmul se folosește de o parcurgere DFS, complexitatea este liniară în toate cazurile. |
| - | Convenție: | + | ===== Exerciții ===== |
| - | * ** d[node] ** = numărul minim de muchii ce trebuie parcurse de la **source** la nodul **node** | + | <note> |
| - | * ** d[source] = 0 ** | + | Scheletul de laborator se găsește pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/skel/lab07|pa-lab::skel/lab07]]. |
| - | * ** d[node] = -1**, dacă nu se poate ajunge de la **source** la **node** | + | |
| </note> | </note> | ||
| - | <spoiler Exemplu 1> | + | <note warning> |
| - | $n = 5$ $m = 4$ $source = 3$ | + | Înainte de a rezolva exercițiile, asigurați-vă că ați citit și înțeles toate precizările din secțiunea |
| + | [[https://ocw.cs.pub.ro/courses/pa/skel_graph | Precizari laboratoare 07-12]]. | ||
| - | $muchii: { (1,2); (1,3); (2,3); (2,4);} $ | + | Prin citirea acestor precizări vă asigurați că: |
| + | * știți **convențiile** folosite | ||
| + | * evitați **buguri** | ||
| + | * evitați **depunctări** la lab/teme/test | ||
| + | </note> | ||
| + | ====== Pool probleme (pentru prezentări) ====== | ||
| - | Răspuns: | + | ======= 1) Longest Flight Route ======= |
| - | |node|1|2|3|4|5| | + | |
| - | |d|1|1|0|2|-1| | + | |
| - | Explicație: | + | **Enunț:** Se consideră un graf orientat aciclic (DAG) cu ''n'' orașe și ''m'' zboruri. Cerința este să se determine cel mai lung traseu posibil de la orașul 1 la orașul ''n''. Traseul reprezintă secvența de orașe vizitate. Dacă există mai multe trasee de lungime maximă, oricare variantă este acceptată. |
| - | Graful dat este cel din figura urmatoare. | + | |
| - | {{pa:new_pa:lab07-bfs-example1.png}} | + | **Date de intrare:** Prima linie conține două numere întregi ''n'' și ''m''. Următoarele ''m'' linii conțin perechi de noduri ''a'' și ''b'', reprezentând un zbor orientat de la orașul ''a'' la orașul ''b''. |
| - | * ** d[3] = 0 ** pentru că 1 este sursa | + | **Date de ieșire:** Prima linie va conține un număr întreg reprezentând numărul maxim de orașe vizitate. A doua linie va conține secvența de orașe din traseul găsit, separate prin spațiu. Dacă nu există niciun drum valid de la 1 la ''n'', se va afișa textul "IMPOSSIBLE". |
| - | * ** 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> | + | |
| + | Problema se poate testa la: \\ | ||
| + | [[https://cses.fi/problemset/task/1680 | CSES - Longest Flight Route]] | ||
| - | <spoiler Exemplu 2> | + | ======= 2) Planets and Kingdoms ======= |
| - | $n = 7$ $m = 7$ $source = 1$ | + | |
| - | $muchii: { (1,2); (1,4); (2,3); (4,5); (5,6); (3,7); (7,6) } $ | + | **Enunț:** Se consideră un graf orientat cu ''n'' planete și ''m'' rute de zbor. O rută ''a → b'' indică faptul că se poate zbura de la planeta ''a'' la planeta ''b''. Două planete fac parte din același „regat” dacă există drumuri în ambele direcții între ele (direct sau indirect). Să se determine numărul total de regate și să se atribuie fiecărei planete un identificator de regat. |
| - | Răspuns: | + | **Date de intrare:** Prima linie conține două numere întregi ''n'' și ''m''. Următoarele ''m'' linii conțin câte două numere întregi ''a'' și ''b'', reprezentând o rută de zbor orientată de la ''a'' la ''b''. |
| - | |node|1|2|3|4|5|6|7| | + | |
| - | |d|0|1|2|1|2|3|3| | + | |
| - | Explicație: | + | **Date de ieșire:** Afișați pe prima linie numărul de regate. Pe a doua linie, afișați pentru fiecare planetă identificatorul regatului din care face parte. (indexare de la 1, se acceptă orice soluție validă) |
| - | Graful dat este cel din figura urmatoare. | + | |
| - | {{pa:new_pa:lab07-bfs-example2.png}} | + | Problema se poate testa la: \\ |
| + | [[https://cses.fi/problemset/task/1683 | CSES - Planets and Kingdoms]] | ||
| - | * ** d[1] = 0 ** pentru că 1 este sursa | + | ======= 3) Critical Connections in a Network ======= |
| - | * ** 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$) | + | |
| - | </spoiler> | + | |
| + | **Enunț:** Se dă o rețea de ''n'' noduri (servere) numerotate de la 0 la ''n - 1'', conectate prin conexiuni bidirecționale. O conexiune este considerată „critică” dacă eliminarea ei crește numărul de componente conexe (adică rețeaua devine mai puțin conectată). Cerința este de a găsi toate conexiunile critice din rețea. | ||
| - | <spoiler Exemplu 3> | + | **Date de intrare:** Un număr întreg ''n'', reprezentând numărul de servere, urmat de o listă de muchii bidirecționale care definesc conexiunile din rețea. |
| - | $n = 7$ $m = 8$ $source = 1$ | + | |
| - | $muchii: { (1,2); (1,4); (2,3); (4,5); (5,6); (3,7); (7,6); (1, 6) } $ | + | **Date de ieșire:** O listă a tuturor conexiunilor critice identificate în rețea. |
| - | Răspuns: | + | Problema se poate testa la: \\ |
| - | |node|1|2|3|4|5|6|7| | + | [[https://leetcode.com/problems/critical-connections-in-a-network/ | LeetCode - Critical Connections in a Network]] |
| - | |d|0|1|2|1|2|1|2| | + | |
| - | Explicație: | + | ======= 4) Planets Cycles ======= |
| - | Graful dat este cel din figura urmatoare. | + | |
| - | {{pa:new_pa:lab07-bfs-example3.png}} | + | **Enunț:** Se dau ''n'' planete, fiecare planetă având exact o muchie orientată către o altă planetă (se poate indica chiar către ea însăși). Pornind dintr-o planetă, un explorator va continua să sară pe următoarea conform muchiei. Pentru fiecare planetă, se cere să determini câte planete diferite va vizita exploratorul până când ajunge pe o planetă deja văzută. |
| - | * ** d[1] = 0 ** pentru că 1 este sursa | + | **Date de intrare:** Prima linie conține un număr întreg ''n''. A doua linie conține un vector de ''n'' elemente ''t_1, t_2, ..., t_n'', unde valoarea ''t_i'' indică destinația muchiei orientate care pleacă din planeta ''i'' (adică ''i → t_i''). |
| - | * ** 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> | + | |
| + | **Date de ieșire:** Afișați o singură linie cu ''n'' numere întregi: pentru fiecare nod, afișați numărul de noduri vizitate înainte de a repeta un nod. | ||
| - | === Topological Sort === | + | Problema se poate testa la: \\ |
| - | Se dă un graf **orientat** aciclic cu **n** noduri și **m** arce. Se cere să se găsească **o sortare topologica** validă. | + | [[https://cses.fi/problemset/task/1751 | CSES - Planets Cycles]] |
| - | <note warning> | + | ======= 5) Coin Collector ======= |
| - | Restricții si precizari: | + | |
| - | * $ n, m <= 10^5 $ | + | |
| - | * timp de executie | + | |
| - | * C++: 1s | + | |
| - | * Java: 1s | + | |
| - | </note> | + | |
| - | <note> | + | **Enunț:** Fie un graf orientat cu ''n'' noduri și ''m'' muchii. Fiecare nod conține un anumit număr de monede. Poți începe traseul din orice nod și te poți deplasa doar folosind muchiile orientate. De fiecare dată când vizitezi un nod, colectezi toate monedele din acel nod (lucru care se poate face o singură dată per nod pe parcursul unui traseu). Cerința este să determini numărul maxim de monede pe care le poți colecta. |
| - | 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. | + | **Date de intrare:** Prima linie conține două numere întregi ''n'' și ''m''. A doua linie conține valorile ''c_1, c_2, ..., c_n'', reprezentând monedele din fiecare nod. Următoarele ''m'' linii descriu muchiile orientate sub forma ''a b'', adică o muchie de la ''a'' la ''b''. |
| - | </note> | + | |
| - | <spoiler Exemplu 1> | + | **Date de ieșire:** Un singur număr întreg: maximul de monede ce pot fi colectate. |
| - | $n = 5$ $m = 4$ | + | |
| - | $arce: { (1,2); (1,3); (2,3); (2,4);} $ | + | Problema se poate testa la: \\ |
| + | [[https://cses.fi/problemset/task/1686 | CSES - Coin Collector]] | ||
| + | ======= 6) Giant Pizza ======= | ||
| - | Răspuns: $topsort = [1, 2, 3, 4, 5] $ | + | **Enunț:** O familie cu ''n'' membri dorește să comande o pizza. Există ''m'' ingrediente posibile. Fiecare membru al familiei are exact două preferințe legate de pizza, fiecare preferință specificând dacă un anumit ingredient ar trebui inclus (+) sau exclus (-). Sarcina ta este să determini dacă există o rețetă de pizza astfel încât cel puțin o preferință a fiecărui membru al familiei să fie respectată. |
| - | Explicație: | + | **Date de intrare:** Prima linie conține două numere întregi ''n'' și ''m''. Următoarele ''n'' linii conțin câte două perechi de valori reprezentând preferințele membrilor (ex. "+ 1 - 2" înseamnă că dorește ingredientul 1, dar nu dorește ingredientul 2). O preferință este îndeplinită dacă pizza finală respectă măcar una dintre cele două condiții alese de membru. |
| - | Graful dat este cel din figura următoare. | + | |
| - | {{pa:new_pa:lab07-topsort-example1.png}} | + | **Date de ieșire:** Afișați o linie cu textul "IMPOSSIBLE" dacă nicio combinație nu poate satisface cerințele. Dacă o rețetă validă există, afișați textul "SATISFIABLE" pe prima linie, iar pe a doua linie afișați o serie de caractere '+' și '-', despărțite prin spațiu, indicând pentru fiecare ingredient (de la 1 la ''m'') dacă va fi inclus sau nu în pizza. |
| - | * 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: | + | Problema se poate testa la: \\ |
| - | * cele date de ordinea relativa a primelor 4 noduri: 1,2,3,4 | + | [[https://cses.fi/problemset/task/1684 | CSES - Giant Pizza]] |
| - | * $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] $ | + | |
| - | </spoiler> | + | ===== Extra ===== |
| + | ==== Exerciții ==== | ||
| - | <spoiler Exemplu 2> | + | <spoiler SCC> |
| - | $n = 9$ $m = 8$ | + | Se dă un graf **orientat** cu **n** noduri și **m** arce. Să se găsească **componentele tare-conexe** folosind algoritmul lui **Tarjan**. Secțiunea de teorie conține exemple grafice explicate. |
| - | $arce: { | + | <note warning> |
| - | (1,2); | + | Restricții și precizări: |
| - | (1,3); | + | * $ n <= 10^5 $ |
| - | (3,4); | + | * $ m <= 2 * 10^5 $ |
| - | (3,5); | + | * timp de execuție |
| - | (5,9); | + | * C++: 1s |
| - | (4,6); | + | * Java: 4s |
| - | (4,7); | + | </note> |
| - | (4,8); | + | </spoiler> |
| - | } $ | + | |
| + | <spoiler CV> | ||
| + | Se dă un graf **neorientat conex** cu **n** noduri și **m** muchii. Se cere să se găsească toate **punctele critice** folosind algoritmul lui **Tarjan**. Secțiunea de teorie conține exemple grafice explicate. | ||
| - | Răspuns: $topsort = [1, 2, 3, 4, 6, 7, 8, 5, 9] $ | + | <note warning> |
| - | + | Restricții și precizări: | |
| - | Explicație: | + | * $ n <= 10^5 $ |
| - | Graful dat este cel din figura următoare. | + | * $ m <= 2 * 10^5 $ |
| - | + | * timp de execuție | |
| - | {{pa:new_pa:lab07-topsort-example2.png}} | + | * C++: 1s |
| - | + | * Java: 4s | |
| - | Se observă din desen că soluția menționată este validă. | + | </note> |
| </spoiler> | </spoiler> | ||
| - | === BONUS === | + | <spoiler CE> |
| - | **B1** Determinați componentele conexe ale unui graf neorientat. Puteți testa implementarea pe infoarena la problema [[https://infoarena.ro/problema/dfs| dfs]]. | + | Se dă un graf **neorientat conex** cu **n** noduri și **m** muchii. Se cere să se găsească toate **muchiile critice** folosind algoritmul lui **Tarjan**. Secțiunea de teorie conține exemple grafice explicate. |
| - | **B2** Rezolvați problema [[https://infoarena.ro/problema/muzeu| muzeu]] pe infoarena. | + | <note warning> |
| - | + | Restricții și precizări: | |
| - | === Extra === | + | * $ n <= 10^5 $ |
| - | + | * $ m <= 2 * 10^5 $ | |
| - | <spoiler arbore3> | + | * timp de execuție |
| - | Rezolvați problema [[https://infoarena.ro/problema/arbore3 | + | * C++: 1s |
| - | | arbore3]] pe infoarena. | + | * Java: 4s |
| + | </note> | ||
| </spoiler> | </spoiler> | ||
| - | <spoiler Pokemon GO AWAY> | + | <spoiler BCC> |
| - | 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 test PA 2017. | + | Se dă un graf **neorientat conex** cu **n** noduri și **m** muchii. Se cere să se găsească toate **componentele biconexe** folosind algoritmul lui **Tarjan**. Secțiunea de teorie conține exemple grafice explicate. |
| - | Cu ce problemă seamana? | + | <note warning> |
| + | Restricții și precizări: | ||
| + | * $ n <= 10^5 $ | ||
| + | * $ m <= 2 * 10^5 $ | ||
| + | * timp de execuție | ||
| + | * C++: 1s | ||
| + | * Java: 4s | ||
| + | </note> | ||
| </spoiler> | </spoiler> | ||
| - | <spoiler insule> | + | <spoiler rețele> |
| - | Rezolvați problema [[https://infoarena.ro/problema/insule | + | Rezolvați problema [[https://infoarena.ro/problema/retele| retele]] pe infoarena. |
| - | | insule]] pe infoarena. | + | |
| </spoiler> | </spoiler> | ||
| - | <spoiler tsunami> | + | <spoiler clepsidra> |
| - | Rezolvați problema [[https://infoarena.ro/problema/tsunami | + | Rezolvați problema [[https://infoarena.ro/problema/clepsidra| clepsidra]] pe infoarena. |
| - | | tsunami]] pe infoarena. | + | |
| </spoiler> | </spoiler> | ||
| - | + | <spoiler Course schedule> | |
| - | <spoiler berarii2> | + | Rezolvați problema [[https://leetcode.com/problems/course-schedule/description/| course-schedule]] pe leetcode. |
| - | Rezolvați problema [[https://infoarena.ro/problema/berarii2 | + | (aplicație tipuri de muchii) |
| - | | berarii2]] pe infoarena. | + | |
| </spoiler> | </spoiler> | ||
| - | |||
| - | |||
| - | |||
| ===== Referințe ===== | ===== Referințe ===== | ||
| Line 584: | Line 590: | ||
| [0] Chapter **Elementary Graph Algorithms**, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein | [0] Chapter **Elementary Graph Algorithms**, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein | ||
| - | [1] [[https://en.wikipedia.org/wiki/Breadth-first_search]] | + | [1] [[https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm]] |
| - | [2] [[https://en.wikipedia.org/wiki/Depth-first_search]] | + | [2] [[https://en.wikipedia.org/wiki/Biconnected_component]] |
| - | [3] [[https://en.wikipedia.org/wiki/Topological_sorting]] | + | [3] "Depth-first search and linear graph algorithms", R.Tarjan |
| + | [4] [[https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm]] | ||