This shows you the differences between two versions of the page.
pa:laboratoare:laborator-08 [2021/05/06 01:49] darius.neatu [Exercitii] |
pa:laboratoare:laborator-08 [2025/05/12 15:52] (current) alexandru.dima1609 [Dijkstra] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Laborator 08: Parcurgerea grafurilor. Aplicații (2/2) ====== | + | ====== Laborator 08: Drumuri minime în grafuri: sursă / destinație unică. (1/2) ====== |
- | Responsabili: | ||
- | * [[neatudarius@gmail.com|Darius-Florentin Neațu (2017-2021)]] | ||
- | * [[radunichita99@gmail.com | Radu Nichita (2021)]] | ||
- | * [[stefanpopa2209@gmail.com | Ștefan Popa (2018-2020)]] | ||
- | Autori: | + | {{:pa:new_pa:partners:adobe-logo.png?155 |}} |
- | * [[neatudarius@gmail.com|Darius-Florentin Neațu (2021)]] | + | \\ \\ \\ Changing the world through digital experiences is what Adobe’s all about. We give everyone - from emerging artists to global brands - everything they need to design and deliver exceptional digital experiences! We’re passionate about empowering people to create beautiful and powerful images, videos, and apps, and transform how companies interact with customers across every screen. |
- | * [[radunichita99@gmail.com | Radu Nichita (2021)]] | + | |
===== Obiective laborator ===== | ===== Obiective laborator ===== | ||
- | * Înțelegerea conceptelor de graf, reprezentare și parcugere | + | În laboratorul 8 vom introduce contextul pentru **Shortest-paths problem** și vom studia **Single-source shortest-paths problem**, iar în laboratorul 9 vom continua cu **All-pairs shortest-paths problem**. |
- | * Studierea unor aplicații pentru parcurgeri | + | |
+ | * Înțelegerea conceptelor de cost asociat unei muchii, relaxare a unei muchii. | ||
+ | * Prezentarea problemei drumului de cost minim (diverse variante). | ||
+ | * Prezentarea algoritmilor pentru calculul drumurilor minime. | ||
- | ===== Componente Conexe ===== | + | ===== Shortest-paths problem: single-source/destination ===== |
- | >> 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. | + | |
- | <spoiler CC - exemplu 01> | + | Vă rugăm să parcugeți [[https://ocw.cs.pub.ro/courses/pa/laboratoare/shortest-paths-problem|Shortest-paths problem]] pentru a vă familiariza cu contextul, problema și notațiile folosite. |
- | $n = 6$ $m = 6$ | + | |
- | $muchii: { (1,2); (1,5); (2,5); (2,3); (3, 5); (4, 6);} $ | + | Concepte necesare: |
+ | * **cost muchie** / **edge cost** | ||
+ | * **cost drum** / **path cost** | ||
+ | * **problema drumurilor minime: sursă / destinație unică** / **single-source/destination shortest-paths problem** | ||
+ | * **relaxarea unei muchii** / **edge relaxation** | ||
+ | * **reconstruirea unui drum** / **RebuildPath** | ||
- | {{pa:new_pa:lab08-cc-example01.png}} | + | ===== Algoritmi ===== |
- | Sunt **2 CC**-uri în graful dat: | + | În acest laborator vom studia **single-source/destination shortest-paths problem**. Pentru această problemă, vom prezenta 2 algoritmi: |
- | * {1, 2, 3, 5} | + | |
- | * {4, 6} | + | |
- | Explicație: | + | * **Dijkstra**: presupune ca toate costurile din graf sunt nenegative. |
- | * Cele 2 sunt mulțimi maximale pentru care se respectă proprietatea de conexitate. | + | * **Bellman-Ford**: permite costuri negative în graf, dar presupune că nu există cicluri de costuri negative. |
- | * 4 și 6 nu sunt accesibile din nodurile 1, 2, 3 și 5, prin urmare, acestea trebuie să facă parte din componente diferite. | + | |
- | </spoiler> | + | |
+ | Vom prezenta fiecare algoritm, îl vom analiza, iar la final vom vedea când îl vom folosi pe fiecare. | ||
- | \\ | + | Puteți consulta capitolul **Single-Source Shortest Paths** din **Introduction to Algorithms** [0] pentru mai multe detalii despre acești algoritmi. |
- | >> Un graf **neorientat** este **conex** dacă conține **o singură** componentă conexă. | + | |
- | <spoiler CC - exemplu 02> | + | ===== Dijkstra ===== |
- | $n = 6$ $m = 7$ | + | |
- | $muchii: {(1, 2); (1, 5); (2, 5); (2, 3); (3, 5); (4, 6); (5, 4)} $ | + | Algoritmul lui [[https://en.wikipedia.org/wiki/Edsger_W._Dijkstra|Edsger Wybe **Dijkstra**]] (**Dijkstra’s algorithm**) rezolvă **shortest-paths problem** în grafuri **G = (V, E)** cu costurile muchiilor **nenegative** ($w[u][v] \ge 0$). |
+ | ==== Dijsktra - Pseudocod ==== | ||
- | {{pa:new_pa:lab08-cc-example02.png}} | + | <code cpp> |
+ | // apply Dijkstra's algorithm from source | ||
+ | // | ||
+ | // source = the source for the computing distances | ||
+ | // nodes = list of all nodes from G | ||
+ | // adj[node] = the adjacency list of node | ||
+ | // example: adj[node] = {..., neigh, ...} => edge (node, neigh) | ||
+ | // | ||
+ | // returns: d, p | ||
+ | // d = distance vector | ||
+ | // p = parent vector | ||
+ | // | ||
+ | Dijsktra(source, G=(nodes, adj)) { | ||
+ | // STEP 0: initialize results | ||
+ | // d[node] = distance from source to node | ||
+ | // p[node] = parent of node on the shortest path from source to node | ||
+ | foreach (node in nodes) { | ||
+ | d[node] = +oo; // distance not yet computed | ||
+ | p[node] = null; // parent not yet found | ||
+ | } | ||
- | Graful dat este conex - există **1 CC**: {1, 2, 3, 4, 5, 6}. | + | // STEP 1: initialize a priority queue |
+ | pq = {}; | ||
- | Explicație: Se poate ajunge de la oricare nod la oricare altul. | + | // STEP 2: add the source(s) into q |
- | </spoiler> | + | d[source] = 0; // distance from source to source |
+ | p[source] = null; // source never has parent | ||
+ | pq.push( (source, d[source]) ); | ||
- | <note> | + | // STEP 3: start relaxation loop using the node(s) from pq |
- | O componentă conexă reprezintă o partiție a nodurilor în submulțimi! <=> Fiecare nod face parte dintr-o singură componentă conexă! | + | while (!pq.empty()) { |
- | </note> | + | // STEP 3.1: extract the next node (having the minimum estimate distance) |
- | ==== Algoritmi ==== | + | node = pq.pop_min(); |
- | === DFS === | + | |
- | <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> | + | |
- | == Complexitate == | + | // [optional] STEP 3.2: print/use the node |
- | $T = O(n + m)$ | + | // STEP 3.3: relax all edges (node, neigh) |
+ | foreach (neigh in adj[node]) { | ||
+ | if (d[node] + w[node][neigh] < d[neigh]) { // try to relax edge (node, neigh) | ||
+ | d[neigh] = d[node] + w[node][neigh]; // update the new distance from source to neigh | ||
+ | p[neigh] = node; // save parent | ||
- | === BFS === | + | pq.push( (neigh, d[neigh]) ); // replace distance for neigh in pq |
- | <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 == | + | return d, p; |
+ | } | ||
- | $T = O(n + m)$ | + | // Usage example: |
+ | d, p = Dijkstra(source, G=(nodes, edges)); | ||
+ | // 1. Use distances from d | ||
+ | // (e.g. d[node] = distance from source to node) | ||
+ | // | ||
+ | // 2. Rebuild path from node to source using parents (p) | ||
+ | RebuildPath(source, destination, p); | ||
+ | </code> | ||
+ | Acest algoritm menține un set de noduri (de exemplu, un min heap / priority queue) cu distanța minimă față de sursă fiind estimată. Cât timp mai există noduri pentru care distanța minimă față de sursă nu este finalizată, se extrage nodul $node$ pentru care estimarea este minimă și această se consideră finalizată. Deoarece acum distanța de la nodul sursă la $u$ este finală, se relaxează toate muchiile care pornesc din $node$, care sunt de forma $(node, neigh)$. | ||
- | <note warning> | + | Pentru ca acest algoritm alege mereu “cel mai apropiat nod de sursă” spunem că **Algoritmul lui Dijsktra** este de tip greedy. Demonstrația se poate găsi în **Introduction to algorithms**. |
- | Deși ambele abordări au aceeași complexitate, recomandăm abordarea cu DFS pentru simplitate. | + | |
- | </note> | + | |
- | ===== Componente Tare Conexe ===== | + | ==== Exemple ==== |
- | >> 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. | + | === Exemplu Dijkstra === |
- | <spoiler SCC - exemplu 01> | + | {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab09-graph-dijkstra-example.png?512| Exemplu Dijkstra}} |
- | $n = 6$ $m = 6$ | + | |
- | $arce: {(1, 2); (1, 5); (5, 2); (2, 3); (3, 5); (4, 6)} $ | + | Drumurile minime calculate de algoritmul lui Dijkstra sunt: |
- | {{pa:new_pa:lab08-scc-example01.png}} | + | |node|1|2|3|4|5|6|7|8|9| \\ |
+ | |d[node]|0|1|1|3|3|1|3|4|$+∞$| \\ | ||
+ | |p[node]|null|1|1|3|4|1|6|5|null| \\ | ||
- | Sunt **4 SCC**-uri în graful dat: | ||
- | * {1} | ||
- | * {2, 3, 5} | ||
- | * {4} | ||
- | * {6} | ||
- | Explicație: | + | <spoiler Explicație pas cu pas> În exemplul atașat, avem un graf **orientat** cu următoare configurație: |
- | * În nodul 1 nu se poate ajunge, prin urmare acesta formează o componentă separată. Analog pentru 4. | + | |
- | * 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> | + | |
+ | * ''%%n = 9%%'', ''%%m = 10%%'' | ||
+ | * Funcția de cost ''%%w%%'' are valorile menționate pe muchii. | ||
+ | * Avem mai multe drumuri de cost diferite între diverse perechi de noduri din graf. | ||
+ | * Alegem $source = 1$ și rulăm pas cu pas algoritmul. | ||
+ | * STEP 0: | ||
+ | * $d[node] = +∞$, pentru $node = 1:9$ | ||
+ | * $p[node] = null$, pentru $node = 1:9$ | ||
+ | * STEP 1: | ||
+ | * $pq = \{\}$ (coadă de priorități în care vom băga elemente $(node, d[node])$, deci vom ordona nodurilor **crescător după distanța minimă față de sursă** cunoscută în acel moment). | ||
+ | * STEP 2: initializări pentru sursă | ||
+ | * $d[1] = 0$ | ||
+ | * $p[1] = null$ | ||
+ | * $pq.push( (1, 0) )$ (inițial avem în coadă doar sura **1** cu distanța **0**). | ||
+ | * STEP 3: scoate câte un nod la fiecare pas și relaxăm toate muchiile care pornesc din acesta; ne oprim când coada e goală. | ||
+ | * $pq = \{ (1, 0) \}$ => $pq.{pop\_min}()$ scoate $node = 1$. Relaxăm muchiile care pornesc din **1**. | ||
+ | * $(1, 2)$: Verificăm dacă $d[1] + w[1][2] < d[2]$ (adică $0 + 1 < +∞$). **DA**, atunci **relaxarea muchiei are loc**: | ||
+ | * $d[2] = d[1] + w[1][2] = 0 + 1 = 1$ (actualizăm distanța) | ||
+ | * $p[2] = 1$ (actualizăm părintele de pe drum) | ||
+ | * $pq.push( (2, 1) )$ (adăugăm nodul cu noua distanță, pentru a putea fi folosit la un pas ulterior) | ||
+ | * $(1, 3)$: Verificăm dacă $d[1] + w[1][3] < d[3]$ (adică $0 + 1 < +∞$). DA, atunci relaxarea muchiei are loc: | ||
+ | * $d[3] = 0 + 1 = 1$ | ||
+ | * $p[3] = 1$ | ||
+ | * $pq.push( (3, 1) )$ | ||
+ | * $(1, 6)$: Verificăm dacă $d[1] + w[1][6] < d[6]$ (adică $0 + 1 < +∞$). DA, atunci relaxarea muchiei are loc: | ||
+ | * $d[6] = 0 + 1 = 1$ | ||
+ | * $p[6] = 1$ | ||
+ | * $pq.push( (6, 1) )$ | ||
+ | * $pq = \{ (2, 1); (3, 1); (6, 1); \}$ => $pq.{pop\_min}()$ scoate $node = 2$. Relaxăm muchiile care pornesc din **2**. | ||
+ | * $(2, 8)$: Verificăm dacă $d[2] + w[2][8] < d[8]$ (adică $1 + 10 < +∞$). DA, atunci relaxarea muchiei are loc: | ||
+ | * $d[8] = 1 + 10 = 11$ | ||
+ | * $p[8] = 2$ | ||
+ | * $pq.push( (8, 11) )$ | ||
+ | * $pq = \{ (3, 1); (6, 1); (8, 11); \}$ => $pq.{pop\_min}()$ scoate $node = 3$. Relaxăm muchiile care pornesc din **3**. | ||
+ | * $(3, 4)$: Verificăm dacă $d[3] + w[3][4] < d[4]$ (adică $1 + 2 < +∞$). DA, atunci relaxarea muchiei are loc: | ||
+ | * $d[4] = 1 + 2 = 3$ | ||
+ | * $p[4] = 3$ | ||
+ | * $pq.push( (4, 3) )$ | ||
+ | * $pq = \{ (6, 1); (4, 3) (8, 11); \}$ => $pq.{pop\_min}()$ scoate $node = 6$. Relaxăm muchiile care pornesc din **6**. | ||
+ | * $(6, 7)$: Verificăm dacă $d[6] + w[6][7] < d[7]$ (adică $1 + 2 < +∞$). DA, atunci relaxarea muchiei are loc: | ||
+ | * $d[7] = 1 + 2 = 3$ | ||
+ | * $p[7] = 6$ | ||
+ | * $pq.push( (7, 3) )$ | ||
+ | * $pq = \{ (4, 3); (7, 3) (8, 11); \}$ => $pq.{pop\_min}()$ scoate $node = 4$. Relaxăm muchiile care pornesc din **4**. | ||
+ | * $(4, 5)$: Verificăm dacă $d[4] + w[4][5] < d[5]$ (adică $3 + 0 < +∞$). DA, atunci relaxarea muchiei are loc: | ||
+ | * $d[5] = 3 + 0 = 3$ | ||
+ | * $p[5] = 4$ | ||
+ | * $pq.push( (5, 3) )$ | ||
+ | * $pq = \{ (5, 3); (7, 3) (8, 11); \}$ => $pq.{pop\_min}()$ scoate $node = 5$. Relaxăm muchiile care pornesc din **5**. | ||
+ | * $(5, 8)$: Verificăm dacă $d[5] + w[5][8] < d[8]$ (adică $3 + 1 < 11$). DA, atunci relaxarea muchiei are loc: | ||
+ | * $d[8] = 3 + 1 = 4$ | ||
+ | * $p[8] = 5$ | ||
+ | * $pq.push( (8, 4) )$ (înlocuim nodul 8 din coadă ținînd cont de noua distanță!) | ||
+ | * $pq = \{ (7, 3) (8, 4); \}$ => $pq.{pop\_min}()$ scoate $node = 7$. Relaxăm muchiile care pornesc din **7**. | ||
+ | * $(7, 8)$: Verificăm dacă $d[7] + w[7][8] < d[8]$ (adică $3 + 2 < 4$). **NU**, atunci relaxarea muchiei **NU** are loc (nu se trece la următorul pas). | ||
+ | * $pq = \{ (8, 4); \}$ => $pq.{pop\_min}()$ scoate $node = 8$. Nu există muchii care pornesc din **8**, nu avem ce relaxa la acest pas. | ||
+ | * $pq = \{ \}$ => Coada este goală. STOP! | ||
+ | * Drumurile minime sunt finale (cele menționate anterior). | ||
- | \\ | + | Observații: |
- | >> Un graf **orientat** este **tare conex** dacă conține o **singură componentă** tare conexă. | + | |
- | <spoiler SCC - exemplu 02> | + | * Distanța minimă față de sursă pentru un nod poate fi recalculată de mai multe ori (de exemplu, nodul **8**). De fiecare dată nodul se reintroduce în coadă (împreună cu noua distanță). Pentru eficiență, înlocuim elementul curent, deoarece are asociată o distanță mai mare și nu ar mai fi folosită. |
- | $n = 6$ $m = 6$ | + | * Mereu se extrage nodul cel mai apropiat de sursă. Distanța asociată cu el în acel moment este finală. În acest exemplu nodurile sunt scoase în ordinea: $1, 2, 3, 6, 4, 5, 7, 8$. |
+ | * Nodul **9** nu este accesibil, datele pentru el rămân ca la inițializare. | ||
- | $arce: {(1, 2); (1, 5); (5, 2); (2, 3); (3, 5); (4, 6); (4, 1); (5, 4); (6, 5)} $ | + | </spoiler> \\ |
- | {{pa:new_pa:lab08-scc-example02.png}} | ||
- | Graful este tare conex - există **1 SCC**: {1, 2, 3, 4, 5, 6}; | + | === [Studiu de caz] Exemplu Dijkstra: costuri negative, rezultate eronate === |
- | Explicație: Se poate vedea că pentru fiecare nod x se poate ajunge în oricare alt nod y. | + | Un nod scos din coadă în algoritmul lui Dijkstra are distanță calculată și finală. Ca această abordare greedy să fie corectă, trebuie ca toate costurile din graf să fie **nenegative**. |
- | * | + | |
- | </spoiler> | + | |
- | <note> | + | <spoiler Exemplu rezultate eronate aplicare Dijkstra pe graf cu costuri negative> |
- | O componentă tare conexă reprezintă o partiție a nodurilor în submulțimi! <=> Fiecare nod face parte dintr-o singură componentă tare conexă! | + | |
- | </note> | + | |
- | ==== 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**). | + | Vom analiza 2 exemple. Pentru simplitate, presupunem ca $source = 1$. |
- | 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ă. | + | În figura următoare avem o topologie simplă, pentru care se întâmplă ca algortimul Dijkstra sa producă vectorul de distanțe corect. |
- | Pentru a determina dacă un nod este rădăcina unei componente tare conexe, se definesc: | + | {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab09-graph-dijkstra-negative-costs-example01.png?512| Dijkstra cu costuri negative - exemplu 01}} |
- | <code cpp> | + | |
- | // the timestamp when node was found (when started to visit its subtree) | + | |
- | found[node] = start[node]; | + | |
- | // the minimum accessible timestamp that node can see/access | + | Dacă complicăm puțin topologia, de exemplu să adăugăm muchii astfel încât să forțăm actualizarea distanței de la sursă la sursă, obținem următoarea figură: |
- | low_link[node] = min { found[x] | x is node OR x in ancestors(node) OR x in descendants(node) }; | + | |
- | </code> | + | |
- | >> **Tarjan SCC**: **node** is root for a SCC if **low_link[node] == found[node]**. | + | {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab09-graph-dijkstra-negative-costs-example02.png?512| Dijkstra cu costuri negative - exemplu 02}} |
- | <spoiler Explicații found+low_link> | + | Când nodul **5** este scos din coadă la un moment dat, acesta va relaxa muschi $(5, 1)$ (pentru că $d[5] + w[5][1] = 1 - 2 = -1 < d[1]$). Acest lucru duce la $d[1] = -1$, adică reactualizarea distanței pentru un nod, care anterior a fost scos din coadă, deși distanța calculată era finală. Deci ajunge la o contradicție. |
- | **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). | + | |
- | **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. | + | </spoiler> \\ |
- | * observații prelimilare: | + | |
- | * 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)**) | + | |
- | * î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> | + | |
- | == Algoritm == | ||
- | <code cpp | TARJAN_SCC> | ||
- | // Tarjan_SCC | ||
- | // * visit all nodes with DFS | ||
- | // * compute found[node] and low_link[node] | ||
- | // * extract SCCs | ||
- | // | ||
- | // nodes = list of all nodes from G | ||
- | // adj[node] = the adjacency list of node | ||
- | // example: adj[node] = {..., neigh, ...} => edge (node, neigh) | ||
- | TARJAN_SCC(G = (nodes, adj)) { | ||
- | // STEP 1: initialize results | ||
- | // parent[node] = parent of node in the DFS traversal | ||
- | // | ||
- | // the timestamp when node was found (when started to visit its subtree) | ||
- | // Note: The global timestamp is incremented everytime a node is found. | ||
- | // | ||
- | // 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) }; | ||
- | // | ||
- | foreach (node in nodes) { | ||
- | parent[node] = null; // parent not yet found | ||
- | found[node] = +oo; // node not yet found | ||
- | low[node] = +oo; // value not yet computed | ||
- | } | ||
- | nodes_stack = {}; // visiting order stack | ||
- | // STEP 2: visit all nodes | + | ==== Complexitate ==== |
- | timestamp = 0; // global timestamp | + | |
- | foreach (node in nodes) { | + | |
- | if (parent[node] == null) { // node not visited | + | |
- | parent[node] = node; // convention: the parent of the root is actually the root | + | |
- | // STEP 3: start a new DFS traversal this subtree | + | * **complexitate temporală**: $T = O(m * log n)\ sau\ O(|E| * log |V|)$ |
- | DFS(node, adj, parent, timestamp, found, low_link, nodes_stack); | + | * **complexitate spațială** : $S = O(n)$ |
- | } | + | |
- | } | + | |
- | } | + | |
- | DFS(node, adj, parent, ref timestamp, found, low_link, nodes_stack) { | + | <spoiler Detalii (analiză + optimizări)> |
- | // STEP 1: a new node is visited - increment the timestamp | + | |
- | 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 | + | |
- | // STEP 2: visit each neighbour | + | * **complexitate temporală**: Se încearcă relaxarea tuturor celor **m** muchii din graf, care poate presupune o inserare în set / coada de priorități - $O(log n)$, deci $O(m * log n)$ în total pentru relaxări. În plus, se fac $n$ ștergeri, adică $O(n * log n)$, însă acest termen se poate neglija (presupunem că $n << m$). |
- | foreach (neigh in adj[node]) { | + | * **complexitate spațială** : Se ține o coadă de priorități / un set cum maximum **n** noduri. |
- | // STEP 3: check if neigh is already visited | + | * **optimizare**: După cum se poate vedea, complexitatea este dată de numărul de relaxări ($m$ - de care nu putem scăpa, deoarece vrem să relaxăm toate muchiile ca să fim siguri că am găsit drumurile de lungime minimă) și de complexitatea operațiilor de ștergere / căutare / inserare în structura de date de tip priority queue. Putem încerca mai multe tipuri de heapuri - [[https://en.wikipedia.org/wiki/Heap_(data_structure)#Comparison_of_theoretic_bounds_for_variants|Heap: Comparison of theoretic bounds for variants]]. |
- | if (parent[neigh] != null) { | + | * **Dijkstra cu heap binar** - $O(m log n)$: Soluția bazată pe un priority queue clasic de mai sus. |
- | // STEP 3.1: update low_link[node] with information gained through neigh | + | * **Dijkstra cu heap Fibonacci** - $O(n logn + m)$: Cea mai rapidă implementare care se poate obține. Exemple în [[https://kbaile03.github.io/projects/fibo_dijk/fibo_dijk.html|Fibonacci Heaps and Dijkstra’s Algorithm - A Visualization]]. |
- | // 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; | + | </spoiler> \\ |
- | } | + | |
- | // STEP 4: save parent | ||
- | parent[neigh] = node; | ||
- | // STEP 5: recursively visit the child subtree | + | ===== Bellman-Ford ===== |
- | DFS(neigh, adj, parent, timestamp, found, low_link, nodes_stack); | + | |
- | // STEP 6: update low_link[node] with information gained through neigh | + | Algoritmul Bellman-Ford a fost inițial propus de Alfonso Shimbel (1955), dar publicat (1956-1958) și numit ulterior după [[https://en.wikipedia.org/wiki/Richard_E._Bellman|Richard E. **Bellman**]] și [[https://en.wikipedia.org/wiki/L._R._Ford_Jr.|Lester Randolph **Ford** Jr. ]]. Acest algoritm rezolvă **shortest-paths problem** în grafuri **G = (V, E)** cu costurile muchiilor oarecare ($w[u][v]$ poate fi și negativ), dar **fără** cicluri de cost negativ (în grafurile de cost negativ nu putem defini costul unui drum care conține un ciclu). |
- | low_link[node] = min(low_link[node], low_link[neigh]); | + | |
- | } | + | |
- | // STEP 7: node is root in a SCC if low_link[node] == found[node] | + | Algoritmul **detectează** prezența ciclurilor de costuri negative în graf. Dacă nu există cicluri de cost negativ în graf, acesta furnizează vectorul de distanțe $d$ și vectorul de părinți $p$ (ca și Dijkstra). |
- | // (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 | + | ==== Bellman-Ford - Pseudocod ==== |
- | print(new_scc); | + | |
+ | <code cpp> | ||
+ | // apply Bellman-Ford's algorithm from source | ||
+ | // | ||
+ | // source = the source for the computing distances | ||
+ | // nodes = list of all nodes from G | ||
+ | // edges = list of all edges from G | ||
+ | // | ||
+ | // returns: has_cycle, d, p | ||
+ | // has_cycle = negative cycle detection flag (true if found) | ||
+ | // d = distance vector (defined only if has_cycle == false) | ||
+ | // p = parent vector (defined only if has_cycle == false) | ||
+ | // | ||
+ | Bellman-Ford(source, G=(nodes, edges)) { | ||
+ | // STEP 0: initialize results | ||
+ | // d[node] = distance from source to node | ||
+ | // p[node] = parent of node on the shortest path from source to node | ||
+ | foreach (node in nodes) { | ||
+ | d[node] = +oo; // distance not yet computed | ||
+ | p(node) = null; // parent not yet found | ||
+ | } | ||
+ | |||
+ | // STEP 1: set distance and parent for source node | ||
+ | d[source] = 0; // distance from source to source | ||
+ | p[source] = null; // source never has parent | ||
+ | |||
+ | // STEP 2: do |nodes| - 1 relaxations for all edges in G | ||
+ | for (i = 1 : |nodes| - 1) { | ||
+ | foreach ((node, neigh) in edges) { | ||
+ | if (d[node] + w[node][neigh] < d[neigh]) { // try to relax edge (node, neigh) | ||
+ | d[neigh] = d[node] + w[node][neigh]; // update the new distance from source to neigh | ||
+ | p[neigh] = node; // save parent | ||
+ | } | ||
} | } | ||
+ | } | ||
+ | |||
+ | // STEP 3: check if edge relaxations can still be made | ||
+ | foreach ((node, neigh) in edges) { | ||
+ | if (d[node] + w[node][neigh] < d[neigh]) { // try to relax edge (node, neigh) | ||
+ | // negative cycle detected! | ||
+ | return true, null, null; | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // STEP 4: no cycle detected | ||
+ | return false, d, p; | ||
} | } | ||
+ | // Usage example: | ||
+ | has_cycle, d, p = Bellman-Ford(source, G=(nodes, edges)); | ||
+ | if (has_cycle) { | ||
+ | print "Has Cycle!" | ||
+ | STOP. | ||
+ | } else { | ||
+ | // 1. Use distances from d | ||
+ | // (e.g. d[node] = distance from source to node) | ||
+ | // | ||
+ | // 2. Rebuild path from node to source using parents (p) | ||
+ | RebuildPath(source, destination, p); | ||
+ | } | ||
</code> | </code> | ||
+ | ==== Exemple ==== | ||
- | Observații: | + | === Exemplu Bellman-Ford === |
- | * La pasul **3.1** se încearcă actualizarea lui **low_link[node]** cu informația din **neigh** doar dacă **neigh** este în stivă. | + | |
- | * 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)$ | + | |
- | === Kosaraju === | + | {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab09-graph-bellman-example01.png?512| Exemplu Bellman-Ford}} |
- | 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. | + | |
- | Puteți consulta următoarele materiale dacă doriți să aflați mai multe: | + | Drumurile minime calculate de algoritmul lui Bellman-Ford sunt: |
- | * https://www.youtube.com/watch?v=RpgcYiky7uw | + | |
- | * https://iq.opengenus.org/kosarajus-algorithm-for-strongly-connected-components/ | + | |
- | ===== Puncte de articulație ===== | + | |
- | >> **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. | + | |node|1|2|3|4|5| \\ |
+ | |d[node]|0|-2|-2|-3|-2| \\ | ||
+ | |p[node]|null|4|1|3|4| \\ | ||
- | <spoiler CV - exemplu 01> | ||
- | $n = 8$ $m = 6$ | ||
- | $muchii: {(1, 2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $ | + | <spoiler Explicație pas cu pas> În exemplul atașat, avem un graf **orientat** cu următoare configurație: |
- | {{pa:new_pa:lab08-cv-example01.png}} | + | * ''%%n = 5%%'', ''%%m = 6%%'' |
+ | * Funcția de cost ''%%w%%'' are valorile menționate pe muchii. | ||
+ | * Avem mai multe drumuri de cost diferite între diverse perechi de noduri din graf. | ||
+ | * Alegem $source = 1$ și rulăm pas cu pas algoritmul. | ||
+ | * STEP 0: | ||
+ | * $d[node] = +∞$, pentru $node = 1:5n$ | ||
+ | * $p[node] = null$, pentru $node = 1:5$ | ||
+ | * STEP 1: initializări pentru sursă | ||
+ | * $d[1] = 0$ | ||
+ | * $p[1] = null$ | ||
+ | * STEP 2: Avem 5 noduri, relaxăm de 4 ori ($n - 1$ ori) toate muchiile din graf. Ordinea nu contează, pentru simplitate vom relaxa muchiile în ordine lexico-grafică: $(1, 2); (1, 3); (2, 3); (3, 4); (4, 2); (4, 5)$. | ||
+ | * **iterație relaxare #1**: | ||
+ | * $(1, 2)$: Verificăm dacă $d[1] + w[1][2] < d[2]$ (adică $0 - 1 < +∞$). **DA**, atunci **relaxarea muchiei are loc**: | ||
+ | * $d[2] = d[1] + w[1][2] = 0 - 1 = -1$ (actualizăm distanța) | ||
+ | * $p[2] = 1$ (actualizăm părintele de pe drum) | ||
+ | * $(1, 3)$: Verificăm dacă $d[1] + w[1][3] < d[3]$ (adică $0 - 2 < +∞$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[3] = 0 - 2 = -2$ | ||
+ | * $p[3] = 1$ | ||
+ | * $(2, 3)$: Verificăm dacă $d[2] + w[2][3] < d[3]$ (adică $-1 + 0 < -2$). **NU**, atunci relaxarea muchiei **NU** are loc. | ||
+ | * $(3, 4)$: Verificăm dacă $d[3] + w[3][4] < d[4]$ (adică $-2 - 1 < +∞$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[4] = -2 - 1 = -3$ | ||
+ | * $p[4] = 3$ | ||
+ | * $(4, 2)$: Verificăm dacă $d[4] + w[4][2] < d[2]$ (adică $-3 + 1 < -1$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[2] = -3 + 1 = -2$ | ||
+ | * $p[2] = 3$ | ||
+ | * $(4, 5)$: Verificăm dacă $d[4] + w[4][5] < d[2]$ (adică $-3 + 1 < +∞$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[5] = -3 + 1 = -2$ | ||
+ | * $p[5] = 4$ | ||
+ | * **iterație relaxare #2**: Nu se va relaxa nicio o muchie. | ||
+ | * **iterație relaxare #3**: Nu se va relaxa nicio o muchie. | ||
+ | * STEP 3: Se incearcă o etapă în plus (#4) de relaxări. Pentru că nu se mai poate relaxa muchii, înseamnă că distanțele găsite la ''%%STEP 2%%'' sunt finale și graful nu conține ciclu de cost negativ! | ||
+ | * STOP. | ||
- | Sunt **3 CV**-uri în graful dat: 1, 5 și 7. | + | </spoiler> \\ |
- | Explicație: | ||
- | * 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> | ||
- | ==== TARJAN CV ==== | + | === [Studiu de caz] Exemplu Bellman-Ford: detecție ciclu de cost negativ === |
- | 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]]. | + | |
- | În mod analog, pentru a determina dacă un nod este CV, se definesc și folosesc **found** și **low_link**. | + | Dacă graful are un ciclu de cost negativ, se va putea plimba pe acel ciclu la infinit, reactulizând distanțele nodurilor implicate. |
- | >> **TARJAN CV**: **node** is **CV** if | + | <spoiler Exemplu detecție ciclu de cost negativ cu Bellman-Ford> {{https://ocw.cs.pub.ro/courses/_media/pa/new_pa/lab09-graph-bellman-example02.png?512| Exemplu Bellman-Ford - ciclu negativ}} |
- | >>**i)** node is NOT root and **low_link[neigh] >= found[node]** for at least one **neigh** in **adj[node]** | + | |
- | >> OR | + | |
- | >> **ii)** node is root and children(node) > 1 | + | |
- | 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. | + | În exemplul atașat, avem un graf **orientat** cu următoare configurație: |
- | <spoiler Explicații found+low_link> | + | * ''%%n = 5%%'', ''%%m = 6%%'' |
- | **found[node]** are aceeași semnificație ca la SCC. | + | * Funcția de cost ''%%w%%'' are valorile menționate pe muchii. |
+ | * Avem mai multe drumuri de cost diferite între diverse perechi de noduri din graf. | ||
+ | * Alegem $source = 1$ și rulăm pas cu pas algoritmul. | ||
+ | * STEP 0: | ||
+ | * $d[node] = +∞$, pentru $node = 1:5n$ | ||
+ | * $p[node] = null$, pentru $node = 1:5$ | ||
+ | * STEP 1: initializări pentru sursă | ||
+ | * $d[1] = 0$ | ||
+ | * $p[1] = null$ | ||
+ | * STEP 2: Avem 5 noduri, relaxăm de 4 ori ($n - 1$ ori) toate muchiile din graf. Ordinea nu contează, pentru simplitate vom relaxa muchiile în ordine lexico-grafică: $(1, 2); (1, 3); (2, 3); (3, 4); (4, 2); (4, 5)$. | ||
+ | * **iterație relaxare #1**: | ||
+ | * $(1, 2)$: Verificăm dacă $d[1] + w[1][2] < d[2]$ (adică $0 - 1 < +∞$). **DA**, atunci **relaxarea muchiei are loc**: | ||
+ | * $d[2] = d[1] + w[1][2] = 0 - 1 = -1$ (actualizăm distanța) | ||
+ | * $p[2] = 1$ (actualizăm părintele de pe drum) | ||
+ | * $(1, 3)$: Verificăm dacă $d[1] + w[1][3] < d[3]$ (adică $0 - 2 < +∞$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[3] = 0 - 2 = -2$ | ||
+ | * $p[3] = 1$ | ||
+ | * $(2, 3)$: Verificăm dacă $d[2] + w[2][3] < d[3]$ (adică $-1 - 2 < -2$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[3] = -1 -2 = -3$ | ||
+ | * $p[3] = 2$ | ||
+ | * $(3, 4)$: Verificăm dacă $d[3] + w[3][4] < d[4]$ (adică $-3 - 1 < +∞$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[4] = -3 - 1 = -4$ | ||
+ | * $p[4] = 3$ | ||
+ | * $(4, 2)$: Verificăm dacă $d[4] + w[4][2] < d[2]$ (adică $-4 + 1 < -1$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[2] = -3 + 1 = -2$ | ||
+ | * $p[2] = 4$ | ||
+ | * $(4, 5)$: Verificăm dacă $d[4] + w[4][5] < d[2]$ (adică $-3 + 1 < +∞$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[5] = -3 + 1 = -2$ | ||
+ | * $p[5] = 4$ | ||
+ | * **iterație relaxare #2**: | ||
+ | * $(1, 2)$: Verificăm dacă $d[1] + w[1][2] < d[2]$ (adică $0 - 1 < -2$). **NU**, atunci relaxarea muchiei **NU** are loc. | ||
+ | * $(1, 3)$: Verificăm dacă $d[1] + w[1][3] < d[3]$ (adică $0 - 2 < -3$). **NU**, atunci relaxarea muchiei **NU** are loc. | ||
+ | * $(2, 3)$: Verificăm dacă $d[2] + w[2][3] < d[3]$ (adică $-2 - 2 < -2$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[3] = -2 -2 = -4$ | ||
+ | * $p[3] = 2$ | ||
+ | * $(3, 4)$: Verificăm dacă $d[3] + w[3][4] < d[4]$ (adică $-4 - 1 < -4$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[4] = -4 - 1 = -5$ | ||
+ | * $p[4] = 3$ | ||
+ | * $(4, 2)$: Verificăm dacă $d[4] + w[4][2] < d[2]$ (adică $-5 + 1 < -2$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[2] = -5 + 1 = -4$ | ||
+ | * $p[2] = 4$ | ||
+ | * $(4, 5)$: Verificăm dacă $d[4] + w[4][5] < d[2]$ (adică $-5 + 1 < -2$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[5] = -5 + 1 = -4$ | ||
+ | * $p[5] = 4$ | ||
+ | * **iterație relaxare #3**: | ||
+ | * $(1, 2)$: Verificăm dacă $d[1] + w[1][2] < d[2]$ (adică $0 - 1 < -4$). **NU**, atunci relaxarea muchiei **NU** are loc. | ||
+ | * $(1, 3)$: Verificăm dacă $d[1] + w[1][3] < d[3]$ (adică $0 - 2 < -4$). **NU**, atunci relaxarea muchiei **NU** are loc. | ||
+ | * $(2, 3)$: Verificăm dacă $d[2] + w[2][3] < d[3]$ (adică $-4 - 2 < -4$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[3] = -4 -2 = -6$ | ||
+ | * $p[3] = 2$ | ||
+ | * $(3, 4)$: Verificăm dacă $d[3] + w[3][4] < d[4]$ (adică $-6 - 1 < -5$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[4] = -6 - 1 = -7$ | ||
+ | * $p[4] = 3$ | ||
+ | * $(4, 2)$: Verificăm dacă $d[4] + w[4][2] < d[2]$ (adică $-7 + 1 < -4$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[2] = -7 + 1 = -6$ | ||
+ | * $p[2] = 4$ | ||
+ | * $(4, 5)$: Verificăm dacă $d[4] + w[4][5] < d[2]$ (adică $-7 + 1 < -4$). **DA**, atunci relaxarea muchiei are loc: | ||
+ | * $d[5] = -7 + 1 = -6$ | ||
+ | * $p[5] = 4$ | ||
+ | * STEP 3: Se incearcă o etapă în plus (#4) de relaxări. | ||
+ | * **iterație relaxare #4**: | ||
+ | * $(1, 2)$: Verificăm dacă $d[1] + w[1][2] < d[2]$ (adică $0 - 1 < -6$). **NU**, atunci relaxarea muchiei **NU** are loc. | ||
+ | * $(1, 3)$: Verificăm dacă $d[1] + w[1][3] < d[3]$ (adică $0 - 2 < -6$). **NU**, atunci relaxarea muchiei **NU** are loc. | ||
+ | * $(2, 3)$: Verificăm dacă $d[2] + w[2][3] < d[3]$ (adică $-6 - 2 < -6$). **DA**, atunci relaxarea muchiei **se poate face**. | ||
+ | * În acest moment putem trage **concluzia că avem un ciclu de cost negativ în graf**, căci altfel distanțele ar fi fost finale și nu am mai fi putut relaxa ceva în ''%%STEP 3%%''. | ||
+ | * STOP. | ||
- | **low_link[node]** are aceeași semnificație ca la SCC. | + | </spoiler> \\ |
- | * 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**) | + | |
- | </spoiler> | ||
- | \\ | + | ==== Complexitate ==== |
- | \\ | + | |
- | Punem la dispoziție un diff de pseudocod: [[https://pastebin.com/raw/4w1LWHnU | 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. | + | |
- | \\ | + | |
- | == Complexitate == | + | * **complexitate temporală**: $T = O(n * m)\ sau\ O(|V| * |E|)$ |
- | * **complexitate temporală **: $T = O(n + m)$ | + | * **complexitate spațială** : $S = O(1)$ |
- | * **complexitate spațială ** : $S = O(n)$ | + | |
- | * recursivitate + câteva tablouri auxiliare de lungime n | + | |
- | ===== 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. | + | |
- | <spoiler CE - exemplu 01> | + | <spoiler Detalii (analiză + optimizări)> |
- | $n = 8$ $m = 6$ | + | |
- | $muchii: { (1,2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $ | + | * **complexitate temporală**: Algoritmul relaxează de **n** ori toate cele **m** muchii din graf. |
+ | * **complexitate spațială** : Nu avem memorie spațială auxialiară. ATENȚIE! Vom aloca tablourile $d$ / $p$, însă acestea nu sunt specifice algoritmului. | ||
+ | * **optimizare**: Se poate obține o performanță mai bună în practică, dar cu aceeași complexitate asimptotică, folosind o coadă: | ||
+ | * Dacă $d[node]$ nu a fost schimbată la un pas, folosirea muchiilor care pornesc din $node$ nu produce efecte. | ||
+ | * Se poate ţine o coadă de noduri, la fiecare pas scoţând un element din aceasta. Se va încerca relaxarea muchiilor care pornesc din nodul scos. Nodurile cu distanțe actualizate se reintroduc în coadă. | ||
+ | * Dacă un nod a fost scos de **n** ori din coadă, atunci graful conține cel puțin un ciclu de cost negativ. | ||
- | {{pa:new_pa:lab08-ce-example01.png}} | + | </spoiler> \\ |
- | Sunt **2 CE**-uri în graful dat: (1, 5) și (7,8) | ||
- | Explicație: | + | ===== TLDR ===== |
- | * 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> | + | |
- | ==== TARJAN CE ==== | + | * Pentru **topologii particulare** ale problemei **shortest-paths**, vom folosi mereu un algoritm baza pe o parcugere (de exemplu, sortare topologic la DAG). Folosirea algoritmilor Dijkstra / Bellman-Ford ar duce la o soluție ineficientă pentru aceste cazuri particulare! |
- | Se modifică algoritmul de CV. Se folosesc aceleași definiții și semnificații pentru **found** și **low_link**. | + | * Pe **topologii generale**, pentru **single-source shortest-paths problem**, vom folosi Dijkstra / Bellman-Ford. |
+ | * **Dijkstra** are complexitate mai bună, deci este de preferat, însă acesta se poate folosi doar pe grafuri cu costuri **nenegative**. | ||
+ | * Dacă avem muchii cu costuri negative, suntem obligați să folosim **Bellman-Ford**, care ne poate detecta și prezența ciclurilor de cost negativ. | ||
+ | * Dacă avem mai multe surse / destinații, putem rula pentru fiecare sursă Dijkstra /Bellman-Ford, obținând complexitate $O(n * m * log n)$ / $O(n^2 * m)$. Vom vedea în laboratorul următor cum putem îmbunătăți acest lucru. | ||
- | >> **TARJAN CE**: **(node, neigh)** is a **CE** if **low_link[neigh] > found[node]** where **neigh** in **adj[node]**. | + | ===== Exerciții ===== |
- | <spoiler Explicație> | + | <note> |
- | Există 2 tipuri de muchii în parcugerea DFS într-un graf neorientat: | + | |
- | * **(node, neigh)**: muchiile din arbore (numită și muchie de arbore) | + | |
- | * **(y, x)**: muchie de la un nod **y** la un strămoș **x** (numită și muchie înapoi) | + | |
- | 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. | + | |
- | Când se termină de vizitat subarborele lui **neigh** și cunoaștem valoarea finală a lui **low_link[neigh]** putem avea: | + | Scheletul de laborator se găsește pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/skel/lab09|pa-lab::skel/lab09]]. |
- | * **low_link[neigh] <= found[node]**: | + | |
- | * 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 | + | |
- | * 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> | + | |
- | == Complexitate == | + | </note> |
- | * **complexitate temporală **: $T = O(n + m)$ | + | <note warning> |
- | * **complexitate spațială ** : $S = O(n)$ | + | |
- | * recursivitate + câteva structuri de date de lungime $O(n)$ | + | |
- | ===== Componente Biconexe ===== | + | |
- | >> O **componentă biconexă** / **BiConnected Component** (**BCC**) într-un graf **neorientat** este o submulțime maximală de noduri cu proprietarea că nu conține puncte de articulație - oricare nod s-ar elimina, nodurile rămase sunt încă conectate. | + | Î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]]. |
- | <spoiler BCC - exemplu 01> | + | Prin citirea acestor precizări vă asigurați că: |
- | $n = 8$ $m = 9$ | + | |
- | $muchii: {(1, 2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $ | + | * știți **convențiile** folosite |
+ | * evitați **buguri** | ||
+ | * evitați **depunctări** la lab/teme/test | ||
- | {{pa:new_pa:lab08-bcc-example01.png}} | ||
- | Sunt **4 BCC**-uri în graful dat: | + | </note> |
- | * {1, 2, 3, 4} | + | |
- | * {1, 5} | + | |
- | * {5, 6, 7} | + | |
- | * {7, 8} | + | |
- | Explicație: | + | ==== Dijkstra ==== |
- | * 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> | + | |
+ | Se dă un graf **orientat** cu **n** noduri și **m** arce. Graful are pe arce **costuri pozitive** (nenegative). | ||
- | \\ | + | Folosiți **Dijkstra** pentru a găsi **costul minim** (**lungimea minimă**) a unui drum de la o sursă dată (**source**) la toate celelalte $n - 1$ noduri din graf. |
- | >> Un graf **neorientat** este **biconex** dacă nu conține puncte de articulație - conține o singură componentă biconexă. | + | |
- | <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}} | + | Restricții și precizări: |
- | Sunt **1 BCC**-uri în graful dat: {1, 2, 3, 4, 5, 6, 7, 8} | + | * $ n <= 50.000 $ |
+ | * $ m <= 2.5 * 10^5 $ | ||
+ | * $ 0 <= c <= 20.000$, unde c este costul/lungimea unui arc | ||
+ | * timp de execuție | ||
+ | * C++: ''%%1s%%'' | ||
+ | * Java: ''%%2s%%'' | ||
- | Explicație: | ||
- | * Nu există noduri/puncte critice în graf (se poate șterge orice nod și graful rămâne conex). | ||
- | </spoiler> | ||
- | <note warning> | ||
- | Î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 ==== | + | <note> |
- | Se modifică algoritmul de CV. Se folosesc aceleași definiții și semnificații pentru **found** și **low_link**. | + | |
- | * Se folosește o stivă **edges_stack** în care se adaugă toate muchiile **(node, neigh)** atunci când se înaintează în recursivitate. | + | Rezultatul se va returna sub forma unui vector **d** cu **n + 1** elemente. |
- | * 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**. | + | |
- | == Complexitate == | + | Convenție: |
- | * **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. | + | |
+ | * $d[node]$ = costul minim / lungimea minimă a unui drum de la **source** la nodul **node** | ||
+ | * $d[source] = 0$ | ||
+ | * $d[node] = -1$, dacă nu se poate ajunge de la **source** la **node** | ||
- | ===== Importanţă – aplicaţii practice ===== | + | $d[0]$ nu este folosit, deci va fi initializat cu 0! (am pastrat indexarea nodurilor de la 1). |
- | * SCC: Data Mining, Compilatoare, problema 2-SAT. | + | |
- | * 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). | + | |
- | + | ||
- | ===== TLDR ===== | + | |
- | * Se poate folosi/modifica algoritmul lui Tarjan pentru a determina **SCC**, **CV**/**CE**/**BCC**. | ||
- | * Deoarece algoritmul se folosește de o parcurgere DFS, complexitatea este liniară în toate cazurile. | ||
- | |||
- | ===== Exercitii ===== | ||
- | <note> | ||
- | Scheletul de laborator se găsește pe pagina [[https://github.com/acs-pa/pa-lab/tree/main/skel/lab08|pa-lab::skel/lab08]]. | ||
</note> | </note> | ||
- | <note warning> | + | ==== RebuildPath ==== |
- | Î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]]. | + | |
- | Prin citirea acestor precizări vă asigurați că: | + | S-a rulat anterior un algoritm oarecare de calculare a drumurilor de lungime minimă într-un graf cu **n** noduri și **m** arce folosind sursa **source**. Se cunoaște vectorul de părinți $p$ rezultat: |
- | * știți **convențiile** folosite | + | |
- | * evitați **buguri** | + | |
- | * evitați **depunctări** la lab/teme/test | + | |
- | </note> | + | * $p[node]$ = părintele lui node de pe drumul minim de la sursă la node. |
+ | * $p[source] = 0$ - sursa nu are părinte. | ||
+ | * $p[node] = 0$ - nodul $node$ nu este accesibil din $source$. | ||
+ | |||
+ | Se cere să se reconstituie drumul de la nodul **source** la nodul **destination**. | ||
- | === SCC === | ||
- | 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. | ||
<note warning> | <note warning> | ||
+ | |||
Restricții și precizări: | Restricții și precizări: | ||
- | * $ n <= 10^5 $ | + | |
- | * $ m <= 2 * 10^5 $ | + | * $ n <= 50.000 $ |
* timp de execuție | * timp de execuție | ||
- | * C++: 1s | + | * C++: ''%%1s%%'' |
- | * Java: 4s | + | * Java: ''%%2s%%'' |
+ | |||
</note> | </note> | ||
+ | <note> | ||
+ | Rezultatul se va returna sub forma unui vector **path**: $path = (source, ..., destination)$. | ||
- | === 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. | ||
- | <note warning> | ||
- | Restricții și precizări: | ||
- | * $ n <= 10^5 $ | ||
- | * $ m <= 2 * 10^5 $ | ||
- | * timp de execuție | ||
- | * C++: 1s | ||
- | * Java: 4s | ||
</note> | </note> | ||
+ | ==== Bellman-Ford ==== | ||
+ | |||
+ | Se dă un graf **orientat** cu **n** noduri și **m** arce. Graful are pe arce **costuri oarecare** (pozitive sau negative). | ||
+ | |||
+ | Folosiți **Bellman-Ford** pentru a găsi **costul minim** (**lungimea minimă**) a unui drum de la o sursă dată (**source**) la toate celelalte $n - 1$ noduri din graf. Se va folosi aceeași convenție de reprezentare a vectorului de distanțe $d$. | ||
- | === CE === | ||
- | 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. | ||
<note warning> | <note warning> | ||
+ | |||
Restricții și precizări: | Restricții și precizări: | ||
- | * $ n <= 10^5 $ | + | |
- | * $ m <= 2 * 10^5 $ | + | * $ n <= 50.000 $ |
+ | * $ m <= 2.5 * 10^5 $ | ||
+ | * $ -1000 <= c <= +1000$, unde c este costul/lungimea unui arc | ||
* timp de execuție | * timp de execuție | ||
- | * C++: 1s | + | * C++: ''%%1s%%'' |
- | * Java: 4s | + | * Java: ''%%2s%%'' |
- | </note> | + | |
- | === BCC === | ||
- | 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. | ||
- | <note warning> | ||
- | Restricții și precizări: | ||
- | * $ n <= 10^5 $ | ||
- | * $ m <= 2 * 10^5 $ | ||
- | * timp de execuție | ||
- | * C++: 1s | ||
- | * Java: 4s | ||
</note> | </note> | ||
+ | ==== BONUS ==== | ||
- | === Extra === | + | La acest laborator, asistentul va alege 1-2 probleme din secțiunea extra. |
- | <spoiler rețele> | + | |
- | Rezolvați problema [[https://infoarena.ro/problema/retele| retele]] pe infoarena. | + | |
- | </spoiler> | + | |
- | <spoiler clepsidra> | + | ==== Extra ==== |
- | Rezolvați problema [[https://infoarena.ro/problema/clepsidra| clepsidra]] pe infoarena. | + | |
- | </spoiler> | + | |
- | + | * [[https://infoarena.ro/problema/distante|infoarena/distante]] | |
- | <spoiler Course schedule> | + | * [[https://infoarena.ro/problema/catun|infoarena/catun]] |
- | Rezolvați problema [[https://leetcode.com/problems/course-schedule/description/| course-schedule]] pe leetcode. | + | * [[https://infoarena.ro/problema/lanterna|infoarena/lanterna]] |
- | (aplicație tipuri de muchii) | + | * [[https://infoarena.ro/problema/ciclu|infoarena/ciclu]] |
- | </spoiler> | + | * [[https://codeforces.com/contest/464/problem/E|codeforces/the-classic-probem]] |
+ | * [[https://codeforces.com/contest/229/problem/B|codeforces/planets]] | ||
+ | * [[http://poj.org/problem?id=1734|acm/sightseeingtrip]] | ||
===== Referințe ===== | ===== Referințe ===== | ||
- | [0] Chapter **Elementary Graph Algorithms**, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein | + | [0] Chapters **Single-Source Shortest Paths** / **All-Pairs Shortest Paths**, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein. |
- | + | ||
- | [1] [[https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm]] | + | |
- | + | ||
- | [2] [[https://en.wikipedia.org/wiki/Biconnected_component]] | + | |
- | + | ||
- | [3] "Depth-first search and linear graph algorithms", R.Tarjan | + | |
- | [4] [[https://en.wikipedia.org/wiki/Kosaraju%27s_algorithm]] |