Differences

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

Link to this comparison view

pa:laboratoare:laborator-07 [2018/04/12 00:11]
darius.neatu
pa:laboratoare:laborator-07 [2023/03/15 16:53] (current)
radu.nichita
Line 1: Line 1:
-====== Laborator 07: Parcurgerea ​GrafurilorSortare Topologica ​======+====== Laborator 07: Parcurgerea ​grafurilorAplicații (2/2) ====== 
  
 ===== Obiective laborator ===== ===== Obiective laborator =====
  
-  *Intelegerea conceptului ​de graf si a modurilor de parcurgere aferente +  * Înțelegerea conceptelor ​de graf, reprezentare și parcugere 
-  *Utilitatea si aplicabilitatea sortarii topologice+  * Studierea unor aplicații pentru parcurgeri
  
-===== Importanţă – aplicaţii practice ===== 
  
-Grafurile sunt utile pentru a modela diverse probleme si se regasesc implementati in multiple aplicatii practice:+===== 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.
  
-  ​*Retele de calculatoare (ex: stabilirea unei topologii fara bucle) +<spoiler CC - exemplu 01> 
-  *Pagini Web (ex: Google PageRank [1]) +$n = 6$   ​$m = 6$
-  *Retele sociale (ex: calcul centralitate [2]) +
-  *Harti cu drumuri (ex: drum minim) +
-  *Modelare grafica (ex: prefuse [3], graph-cut [4] )+
  
-===== Descrierea problemei și a rezolvărilor =====+$muchii: { (1,2); (1,5); (2,5); (2,3); (3, 5); (4, 6);} $
  
-Graful poate fi modelat drept o pereche de multimi G = (V, E). Multimea V contine nodurile grafului (vertices), iar multimea E contine muchiile (edges), fiecare muchie stabilind o relatie de vecinatate intre doua noduri. O mare varietate de probleme se modeleaza folosind grafuri, iar rezolvarea acestora presupune explorarea spatiului. O parcurgere isi propune sa ia in discutie fiecare nod al grafului, exact o singura data, pornind de la un nod ales, numit in continuare nod sursa. 
  
-Reprezentarea in memorie a grafurilor se face, de obicei, cu liste de adiacenta sau cu matrice de adiacenta. Se pot folosi insa si alte structuri de date, de exemplu un map de perechi < <​sursa,​destinatie>,​cost> ​.+{{pa:​new_pa:​lab08-cc-example01.png}}
  
-Pe parcursul rularii algoritmilor de parcurgereun nod poate avea culori:+Sunt **2 CC**-uri în graful dat: 
 +  * {1, 2, 3, 5} 
 +  * {4, 6}
  
-  *Alb = nedescoperit +Explicație:​ 
-  *Gri = a fost descoperit si este in curs de prcesare +  * Cele 2 sunt mulțimi maximale pentru care se respectă proprietatea ​de conexitate. 
-  *Negru = a fost procesat+  * 4 și 6 nu sunt accesibile din nodurile 1, 2, 3 și 5, prin urmare, acestea trebuie să facă parte din componente diferite.  
 +</​spoiler>​
  
-Se poate face o analogie cu o pata neagra care se extinde pe un spatiu alb. Nodurile gri se afla pe frontiera petei negre. 
-Algoritmii de parcurgere pot fi caracterizati prin completitudine si optimalitate. Un algoritm de explorare complet va descoperi intotdeauna o solutie, daca problema accepta solutie. Un algoritm de explorare optimal va descoperi solutia optima a problemei din perspectiva numarului de pasi care trebuie efectuati. 
  
-==== Parcurgerea in lățime - BFS ====+\\ 
 +>> Un graf **neorientat** este **conex** dacă conține **o singură** componentă conexă. ​
  
-Parcurgerea in latime **(Breadth-first Search - BFS)** este un algoritm de cautare in graf, in care, atunci cand se ajunge intr-un nod oarecare v, nevizitat, se viziteaza toate nodurile nevizitate adiacente lui v, apoi toate varfurile nevizitate adiacente varfurilor adiacente lui v, etc. +<spoiler CC exemplu 02> 
-Atentie! BFS depinde de nodul de start. Plecand dintr-un nod se va parcurge doar componenta conexa din care acesta face parte. Pentru grafuri cu mai multe componente conexe se vor obtine mai multi arbori de acoperire.+$n = 6$   $m = 7$
  
-In urma aplicarii algoritmului BFS asupra fiecarei componente conexe a grafuluise obtine un arbore de acoperire ​(prin eliminarea muchiilor pe care nu le folosim la parcurgere). Pentru a putea reconstitui acest arborese pastreaza pentru fiecare nod dat identitatea parintelui sau. In cazul in care nu exista o functie de cost asociata muchiilorBFS va determina si drumurile minime de la radacina la oricare nod.+$muchii: {(12); (1, 5); (25); (23); (3, 5); (4, 6); (5, 4)} $
  
-Pentru implementarea BFS se foloseste o coada. In momentul adaugarii in coada, un nod trebuie colorat gri (a fost descoperit si urmeaza sa fie prelucrat). 
  
-Algoritmul de explorare BFS este complet si optimal.+{{pa:​new_pa:​lab08-cc-example02.png}}
  
-Algoritm: +Graful dat este conex - există **1 CC**: {1234, 5, 6}.
-<code cpp> +
-BFS(s, G) { +
-    foreach (u ∈ V) { +
-        p(u) = null; // initializari +
-        dist(s,u) = inf; +
-        c(u) = alb;  +
-    } +
-    dist(s) = 0; // distanta pana la sursa este 0 +
-    c(s) = gri; //incepem prelucrarea noduluideci culoarea devine gri +
-    Q = (); //se foloseste o coada cu nodurile de prelucrat +
-    Q = Q + s; // adaugam sursa in coada +
-    while (!empty(Q)) { // cat timp mai am noduri de prelucrat +
-        u = top(Q); // se determina nodul din varful cozii +
-        foreach v ∈ succs(u) { // pentru toti vecinii +
-            if (c(v) = alb) {// nodul nu a fost gasitnu e in coada +
-                // actualizam structura date  +
-                dist(v) = dist(u) + 1; +
-                p(v) = u; +
-                c(v) = gri; +
-                Q = Q + v; +
-            } // close if +
-        } // close foreach +
-        c(u) = negru; //am terminat de prelucrat nodul curent +
-        Q = Q - u; //nodul este eliminat din coada +
-    } //close while +
-} +
-</​code>​+
  
-Complexitate+ExplicațieSe poate ajunge de la oricare nod la oricare altul. 
 +</​spoiler>​
  
-  *cu listaO(|E|+|V|) +<​note>​ 
-  *cu matrice: O(|V|^2)+O componentă conexă reprezintă o partiție a nodurilor în submulțimi! <=> Fiecare nod face parte dintr-o singură componentă conexă! 
 +</​note>​ 
 +==== Algoritmi ==== 
 +=== 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>​
  
-==== Parcurgerea in adancime – DFS ====+== Complexitate ​==
  
-Parcurgerea in adancime **(Depth-First Search - DFS)** porneste de la un nod dat (nod de start), care este marcat ca fiind in curs de procesare. Se alege primul vecin nevizitat al acestui nod, se marcheaza si acesta ca fiind in curs de procesare, apoi si pentru acest vecin se cauta primul vecin nevizitat, si asa mai departe. In momentul in care nodul curent nu mai are vecini nevizitati, se marcheaza ca fiind deja procesat si se revine la nodul anterior. Pentru acest nod se cauta primul vecin nevizitat. Algoritmul se repeta pana cand toate nodurile grafului au fost procesate.+$T = O(n + m)$
  
-In urma aplicarii algoritmului DFS asupra fiecarei componente conexe a grafului, se obtine pentru fiecare dintre acestea cate un arbore ​de acoperire (prin eliminarea muchiilor pe care nu le folosim la parcurgere). Pentru ​a putea reconstitui acest arbore, pastram pentru ​fiecare nod dat identitatea parintelui sau.+=== 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>​
  
-Pentru fiecare nod se vor retine:  +== Complexitate ==
-  *timpul descoperirii +
-  *timpul finalizarii +
-  *parintele +
-  *culoarea+
  
-Algoritmul de explorare DFS nu este nici complet ​(in cazul unei cautari pe un subarbore infinit), nici optimal (nu gaseste nodul cu adancimea minima).+$T = O(n + m)$
  
-Spre deosebire de BFS, pentru ​implementarea DFS se foloseste o stiva (abordare **LIFO** in loc de **FIFO**). Desi se poate face aceasta inlocuire in algoritmul de mai sus, de cele mai multe ori este mai intuitiva folosirea recusivitatii.+<note warning>​ 
 +Deși ambele abordări au aceeași complexitaterecomandăm abordarea cu DFS pentru ​simplitate. 
 +</​note>​
  
-Algoritm:+===== Componente Tare Conexe =====
  
 +>> 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.
 +
 +<spoiler SCC - exemplu 01>
 +$n = 6$   $m = 6$
 +
 +$arce: {(1, 2); (1, 5); (5, 2); (2, 3); (3, 5); (4, 6)} $
 +
 +{{pa:​new_pa:​lab08-scc-example01.png}}
 +
 +Sunt **4 SCC**-uri în graful dat:
 +  * {1}
 +  * {2, 3, 5}
 +  * {4}
 +  * {6}
 +
 +Explicaț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>​
 +
 +
 +\\
 +>> Un graf **orientat** este **tare conex** dacă conține o **singură componentă** tare conexă.
 +
 +<spoiler SCC - exemplu 02>
 +$n = 6$   $m = 6$
 +
 +$arce: {(1, 2); (1, 5); (5, 2); (2, 3); (3, 5); (4, 6); (4, 1); (5, 4); (6, 5)} $
 +
 +{{pa:​new_pa:​lab08-scc-example02.png}}
 +
 +Graful este tare conex - există **1 SCC**: {1, 2, 3, 4, 5, 6};
 +
 +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>​
 +==== 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**).
 +
 +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ă.
 +
 +Pentru a determina dacă un nod este rădăcina unei componente tare conexe, se definesc:
 <code cpp> <code cpp>
-DFS(G) { +// the timestamp when node was found (when started to visit its subtree) 
-    ​noduri(G+found[node] = start[node];​ 
-    foreach (u ∈ V) {  + 
-        // initializare structura date +// the minimum accessible timestamp that node can see/​access 
-        ​c(u) alb+low_link[node] =  min { found[x] | x is node OR x in ancestors(node) OR x in descendants(node) }; 
-        ​p(u)=null; ​     +</​code>​ 
 + 
 +>> **Tarjan SCC**: **node** is root for a SCC if **low_link[node] == found[node]**. 
 + 
 +<spoiler Explicații found+low_link>​ 
 +**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.  
 +   * 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_link[node] = +oo; // value not yet computed 
 +    } 
 +    nodes_stack = {}; // visiting order stack 
 + 
 +    // STEP 2: visit all nodes 
 +    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 
 +            DFS(node, adj, parent, timestamp, found, low_link, nodes_stack);​ 
 +        }
     }     }
-    timp = 0; // retine distanta de la radacina pana la nodul curent 
-    foreach (u ∈ V) 
-        if (c(u) = alb) explorare(u);​ // explorez nodul 
 } }
  
-explorare(u) { +DFS(node, adj, parent, ref timestamp, found, low_link, nodes_stack) { 
-    ​d(u) timp++; // timpul de descoperire al nodului u +    ​// STEP 1: a new node is visited - increment the timestamp 
-    ​c(u= gri; // nod in curs de explorare +    found[node] ​= ++timestamp; // the timestamp when node was found 
-    foreach (v ∈ succes(u)) // incerc sa prelucrez vecinii +    ​low_link[node] = found[node];​ // node only knows its timestamp 
-        if (c(v) alb) { // daca nu au fost prelucrati deja +    nodes_stack.push(node); // add node to the visiting stack 
-            ​p(v) = u+ 
-            ​explorare(v);+    // STEP 2: visit each neighbour 
 +    foreach (neigh in adj[node]
 +        ​// STEP 3: check if neigh is already visited 
 +        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;
         }         }
-    c(u) = negru; // am terminat de prelucrat nodul curent + 
-    ​f(u) = timp++; // timpul de finalizare al nodului u+        // 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]); 
 +    } 
 + 
 +    ​// STEP 7: node is root in a SCC if low_link[node] == found[node] 
 +    ​// (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>​
  
-Complexitate:​+Observații:​ 
 +  * 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)$
  
-  *cu lista: O(|E|+|V|) +=== Kosaraju === 
-  *cu matrice: O(|V|^2)+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.
  
-==== Sortarea Topologica ​====+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 =====
  
-Dandu-se un graf orientat aciclic, sortarea topologica realizeaza o aranjare liniara ​nodurilor in functie ​de muchiile dintre ele. Orientarea muchiilor corespunde unei relatii de ordine de la nodul sursa catre cel destinatie. Astfel, daca (u,veste una dintre ​muchiile ​grafului, u trebuie sa apara inaintea lui v in insiruire. 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)+>> **Punct de articulație** ​ / **nod critic** / **Cut Vertex** (**CV**) este un nod într-un graf **neorientat** ​cărui eliminare duce la creșterea numărului ​de componente conexe ​(CC- se elimină nodul împreună cu muchiile ​incidente.
  
-Sortarea topologica poate fi vazuta si ca plasarea nodurilor de-a lungul unei linii orizontale astfel incat toate muchiile sa fie directionate de la stanga la dreapta.+<spoiler CV exemplu 01> 
 +$n = 8$   $m = 6$
  
-Exemplu:+$muchii{(1, 2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $
  
-Profesorul Bumstead isi sorteaza topologic hainele inainte de a se imbraca.+{{pa:​new_pa:​lab08-cv-example01.png}}
  
-{{ :pa:​laboratoare:​6.1.png?500 |}}+Sunt **3 CV**-uri în graful dat: 1, 5 și 7.
  
 +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>​
  
-(a) Fiecare muchie (u,v) inseamna ca obiectul de imbracaminte u trebuie imbracat inaintea obiectului de imbracaminte vTimpii de descoperire d(u) si de finalizare f(u) obtinuti in urma parcurgerii DFS sunt notati langa noduri.+==== TARJAN CV ==== 
 +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]].
  
-{{ :​pa:​laboratoare:​6.2.png?500 |}} +În mod analog, pentru a determina dacă un nod este CV, se definesc și folosesc **found** și **low_link**.
  
-(bAcelasi graf, sortat topologic. Nodurile lui sunt aranjate de la stanga la dreapta ​in ordinea ​**descrescatoare** **timpului de finalizare** f(u). Observati ca toate muchiile sunt orientate de la stanga la dreapta. Acum profesorul Bumstead se poate imbraca linistit.+>> **TARJAN CV**: **node** is **CV** if 
 +>>**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
  
-Algoritm:+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.
  
-Sunt doi algoritmi cunoscuti pentru sortarea topologina.+<spoiler Explicații found+low_link>​ 
 +**found[node]** are aceeași semnificație ca la SCC.
  
-Algoritmul bazat pe DFS+**low_link[node]** are aceeași semnificație ca la SCC.  
-  *parcurgere DFS pentru ​determinarea timpilor +   * observații/​diferențe
-  *sortare descrescatoare ​in functie ​de timpul ​de finalizare+    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**)
  
-Pentru a evita sortarea nodurilor in functie de timpul de finalizare, se poate folosi o stiva ce retine aceste noduri in ordinea terminarii parcurgerii.+</​spoiler>​
  
-Un alt algoritm ​este cel descris ​de Kahn:+\\ 
 +\\ 
 +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. 
 +\\
  
-<code cpp> +== Complexitate == 
-TopSort(G{ +  * **complexitate temporală **: $T = O(n + m)$ 
-    ​V ​noduri(G+  * **complexitate spațială ** : $S O(n)$ 
-    ​vida;// lista care va contine elementele sortate +    ​* recursivitate +  câteva tablouri auxiliare de lungime n 
-    // initializare S cu nodurile care nu au in-muchii +===== Punți ​/ muchii ​critice ====
-    foreach (u ∈ V) { +>> **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 ​(nodurileacesteia.
-        if (u nu are in-muchii) +
-     S S + u; +
-    } +
-    while (!empty(S)) { // cat timp mai am noduri de prelucrat +
-        u = random(S); // se scoate ​un nod din multimea S +
-        L = L + u;  // adaug U la lista finala +
-        foreach v ∈ succs(u{ // pentru toti vecinii +
-     sterge u-v; // sterge ​muchia u-v +
-            if (v nu are in-muchii) +
-         S = S + v;  // adauga v la multimea S +
-        } // close foreach +
-    } //close while +
-    if (G are muchii) +
-        print(eroare); ​ // graf ciclic +
-    else +
-        print(L); // ordinea topologica +
-+
-</​code>​+
  
-Complexitate optima: O(|E|+|V|) +<spoiler CE - exemplu 01> 
-===== Concluzii si observatii =====+$n 8$   ​$m ​6$
  
-Grafurile sunt foarte importante pentru reprezentarea si rezolvarea unei multitudini de probleme. +$muchii{ (1,2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $
-Cele mai uzuale moduri de reprezentare a unui graf sunt: +
-  *liste de adiacenta +
-  *matrice de adiacenta+
  
-Cele doua moduri uzuale ​de parcurgere neinformata ​unui graf sunt: +{{pa:​new_pa:​lab08-ce-example01.png}} 
-  *BFS – parcurgere in latime + 
-  *DFS – parcurgere ​in adancime+Sunt **2 CE**-uri în graful dat: (1, 5) și (7,8) 
 + 
 +Explicație:​ 
 +  * 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 ==== 
 +Se modifică algoritmul ​de CV. Se folosesc aceleași definiții și semnificații pentru **found** și **low_link**. 
 + 
 +>> **TARJAN CE**: **(node, neigh)** is **CE** if **low_link[neigh] > found[node]** where **neigh** in **adj[node]**. 
 + 
 + 
 +<spoiler Explicație>​ 
 +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: 
 +  * **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 == 
 +  * **complexitate temporală **: $T = O(n + m)$ 
 +  * **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. 
 + 
 +<spoiler BCC - exemplu 01> 
 +$n = 8$   $m = 9$ 
 + 
 +$muchii: {(1, 2); (2, 3); (3, 4); (4, 1); (1, 5); (5, 6); (6, 7); (7, 5); (7, 8)} $ 
 + 
 +{{pa:​new_pa:​lab08-bcc-example01.png}} 
 + 
 +Sunt **4 BCC**-uri în graful dat:  
 +* {1, 2, 3, 4} 
 +* {1, 5} 
 +* {5, 6, 7} 
 +* {7, 8} 
 + 
 +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>​ 
 + 
 + 
 +\\ 
 +>> 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)} $ 
 + 
 +{{pa:​new_pa:​lab08-bcc-example02.png}} 
 + 
 +Sunt **1 BCC**-uri în graful dat: {1, 2, 3, 4, 5, 6, 7, 8} 
 + 
 +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>​ 
 +==== TARJAN BCC ==== 
 +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.  
 +  * 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 == 
 +  * **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. 
 + 
 + 
 +===== Importanţă ​– aplicaţii practice ===== 
 +  * 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 =====
  
-Sortarea topologica este o modalitate de aranjare ​nodurilor in functie de muchiile dintre eleIn functie ​de nodul de start al DFS, se pot obtine sortari diferite, pastrand insa proprietatile generale ale sortarii topologice.+  * Se poate folosi/​modifica algoritmul lui Tarjan pentru ​determina **SCC**, **CV** / **CE** / **BCC**. 
 +  * Deoarece algoritmul se folosește ​de o parcurgere ​DFS, complexitatea este liniară în toate cazurile.
  
 ===== Exercitii ===== ===== Exercitii =====
 <​note>​ <​note>​
-In acest laborator vom folosi scheletul ​de laborator ​din arhiva {{pa:new_pa:skel-lab07.zip}}.+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>​
  
-=== BFS === +<note warning> 
-Se da un graf **neorientat** cu **n** noduri si **m** muchii. Se mai da un nod special **source**, pe care il vom numi sursa. Se cere sa se gaseasca numarul minim de arce de trebuie parcurse de la **source** la **toate ** celelalte noduri+Î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]].
  
-<​note>​ +Prin citirea acestor precizări vă asigurați că: 
-Rezultatul se va returna sub forma unui vector cu ** ** elemente.+   ​știți ​**convențiile** folosite 
 +   * evitați **buguri** 
 +   * evitați **depunctări** la lab/​teme/​test
  
-** d[node] ** = numarul minim de arce ce trebuie parcurse de la **source** la nodul **node**+</​note>​
  
-Conventie: +=== SCC === 
-** d[source] = 0 ** +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. 
-** d[node] = -1**, daca nu se poate ajunge de la **source** la **node**+ 
 +<note warning>​ 
 +Restricții și precizări:​ 
 +  * $ n <= 10^5 $ 
 +  * $ m <= 2 * 10^5 $ 
 +  * timp de execuție 
 +    * C++: 1s 
 +    * Java: 4s
 </​note>​ </​note>​
  
-=== TOPOLOGICAL SORT === 
  
-===== Referinte ​=====+=== 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>​ 
 + 
 + 
 +=== 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>​ 
 +Restricții și precizări:​ 
 +  * $ n <= 10^5 $ 
 +  * $ m <= 2 * 10^5 $ 
 +  * timp de execuție 
 +    * C++: 1s 
 +    * Java: 4s 
 +</​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>​
  
-[1] [[http://​en.wikipedia.org/​wiki/​PageRank]] 
  
-[2] [[http://en.wikipedia.org/wiki/Social_network#​Social_network_analysis]]+=== Extra === 
 +<spoiler rețele>​ 
 +Rezolvați problema ​[[https://infoarena.ro/problema/retele| retele]] pe infoarena. 
 +</​spoiler>​
  
-[3] [[http://prefuse.org/]]+<spoiler clepsidra>​ 
 +Rezolvați problema ​[[https://infoarena.ro/problema/​clepsidra| clepsidra]] pe infoarena. 
 +</​spoiler>​
  
-[4] [[http://​classes.engr.oregonstate.edu/​eecs/​spring2008/​cs419/​Lectures/​jun_graphcut.pdf]] 
  
-[5] [[http://en.wikipedia.org/wiki/Breadth-first_search]]+<spoiler Course schedule>​ 
 +Rezolvați problema ​[[https://leetcode.com/problems/course-schedule/​description/​| course-schedule]] pe leetcode. 
 +(aplicație tipuri de muchii) 
 +</​spoiler>​
  
-[6] [[http://​en.wikipedia.org/​wiki/​Depth-first_search]]+===== Referințe =====
  
-[7[[http://en.wikipedia.org/​wiki/​Topological_sorting]]+[0Chapter **Elementary Graph Algorithms**,​ “Introduction to Algorithms”,​ Thomas HCormen, Charles ELeiserson, Ronald L. Rivest and Clifford Stein
  
-[8Introducere in Algoritmi, TCormen s.a., pag 403-419+[1[[https://​en.wikipedia.org/​wiki/​Tarjan%27s_strongly_connected_components_algorithm]]
  
-[9] [[http://ww3.algorithmdesign.net/handouts/DFS.pdf]]+[2] [[https://en.wikipedia.org/wiki/Biconnected_component]]
  
-[10[[http://​ww3.algorithmdesign.net/​handouts/​BFS.pdf]]+[3"​Depth-first search and linear graph algorithms",​ R.Tarjan ​
  
 +[4] [[https://​en.wikipedia.org/​wiki/​Kosaraju%27s_algorithm]]
pa/laboratoare/laborator-07.1523481078.txt.gz · Last modified: 2018/04/12 00:11 by darius.neatu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0