This is an old revision of the document!
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.
Un graf neorientat este conex dacă conține o singură componentă conexă.
$T = O(n + m)$
$T = O(n + m)$
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.
Un graf orientat este tare conex dacă conține o singură componentă tare conexă.
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:
// 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 low_link[node] = min { found[x] | x is node OR x in ancestors(node) OR x in descendants(node) };
Tarjan SCC: node is root for a SCC if low_link[node] == found[node].
// 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); } } } DFS(node, adj, parent, ref timestamp, found, low_link, nodes_stack) { // 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 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; } // 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); } }
Observații:
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:
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.
Putem modifica ușor algoritmul TARJAN SCC astfel încât să obținem Algoritmul lui Tarjan pentru CV.
În mod analog, pentru a determina dacă un nod este CV, se definesc și folosesc found și low_link.
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
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.
Punem la dispoziție un diff de pseudocod: TARJAN_SCC vs TARJAN_CV. Se observă că este același algoritm, singurele diferențe relevante sunt:
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.
Se modifică algoritmul de CV. Se folosesc aceleași definiții și semnificații pentru found și low_link.
TARJAN CE: (node, neigh) is a CE if low_link[neigh] > found[node] where neigh in adj[node].
O componentă biconexă / BiConnected Component (BCC) într-un graf neorientat este o submulțime maximală de noduri cu proprietatea că nu conține puncte de articulație - oricare nod s-ar elimina, nodurile rămase sunt încă conectate.
Un graf neorientat este biconex dacă nu conține puncte de articulație - conține o singură componentă biconexă.
Se modifică algoritmul de CV. Se folosesc aceleași definiții și semnificații pentru found și low_link.
Prin citirea acestor precizări vă asigurați că:
Enunț: Se consideră un graf orientat cu n planete și m rute de zbor. O rută a → b indică faptul că se poate zbura de la planeta a la planeta b. Două planete fac parte din același „regat” dacă există drumuri în ambele direcții între ele (direct sau indirect). Să se determine numărul total de regate și să se atribuie fiecărei planete un identificator de regat.
Date de intrare: Prima linie conține două numere întregi n și m. Următoarele m linii conțin câte două numere întregi a și b, reprezentând o rută de zbor orientată de la a la b.
Date de ieșire: Afișați pe prima linie numărul de regate. Pe a doua linie, afișați pentru fiecare planetă identificatorul regatului din care face parte. (indexare de la 1, se acceptă orice soluție validă)
Problema se poate testa la:
CSES - Planets and Kingdoms
Enunț: Se dă o rețea de n noduri (servere) numerotate de la 0 la n - 1, conectate prin conexiuni bidirecționale. O conexiune este considerată „critică” dacă eliminarea ei crește numărul de componente conexe (adică rețeaua devine mai puțin conectată). Cerința este de a găsi toate conexiunile critice din rețea.
Date de intrare: Un număr întreg n, reprezentând numărul de servere, urmat de o listă de muchii bidirecționale care definesc conexiunile din rețea.
Date de ieșire: O listă a tuturor conexiunilor critice identificate în rețea.
Problema se poate testa la:
LeetCode - Critical Connections in a Network
Enunț: Se dau n planete, fiecare planetă având exact o muchie orientată către o altă planetă (se poate indica chiar către ea însăși). Pornind dintr-o planetă, un explorator va continua să sară pe următoarea conform muchiei. Pentru fiecare planetă, se cere să determini câte planete diferite va vizita exploratorul până când ajunge pe o planetă deja văzută.
Date de intrare: Prima linie conține un număr întreg n. A doua linie conține un vector de n elemente t_1, t_2, …, t_n, unde valoarea t_i indică destinația muchiei orientate care pleacă din planeta i (adică i → t_i).
Date de ieșire: Afișați o singură linie cu n numere întregi: pentru fiecare nod, afișați numărul de noduri vizitate înainte de a repeta un nod.
Problema se poate testa la:
CSES - Planets Cycles
Enunț: Ai un graf orientat cu n noduri și m muchii. Fiecare nod conține un anumit număr de monede. Poți începe traseul din orice nod și te poți deplasa doar folosind muchiile orientate. De fiecare dată când vizitezi un nod, colectezi toate monedele din acel nod (lucru care se poate face o singură dată per nod pe parcursul unui traseu). Cerința este să determini numărul maxim de monede pe care le poți colecta.
Date de intrare: Prima linie conține două numere întregi n și m. A doua linie conține valorile c_1, c_2, …, c_n, reprezentând monedele din fiecare nod. Următoarele m linii descriu muchiile orientate sub forma a b, adică o muchie de la a la b.
Date de ieșire: Un singur număr întreg: maximul de monede ce pot fi colectate.
Problema se poate testa la:
CSES - Coin Collector
Enunț: O familie cu n membri dorește să comande o pizza. Există m ingrediente posibile. Fiecare membru al familiei are exact două preferințe legate de pizza, fiecare preferință specificând dacă un anumit ingredient ar trebui inclus (+) sau exclus (-). Sarcina ta este să determini dacă există o rețetă de pizza (o atribuire a stării incluse/excluse pentru fiecare ingredient) astfel încât cel puțin o preferință a fiecărui membru al familiei să fie respectată. (Aceasta este, în esență, o problemă clasică de satisfiabilitate booleană – 2-SAT).
Date de intrare: Prima linie conține două numere întregi n și m. Următoarele n linii conțin câte două perechi de valori reprezentând preferințele membrilor (ex. ”+ 1 - 2” înseamnă că dorește ingredientul 1, dar nu dorește ingredientul 2). O preferință este îndeplinită dacă pizza finală respectă măcar una dintre cele două condiții alese de membru.
Date de ieșire: Afișați o linie cu textul “IMPOSSIBLE” dacă nicio combinație nu poate satisface cerințele. Dacă o rețetă validă există, afișați textul “SATISFIABLE” pe prima linie, iar pe a doua linie afișați o serie de caractere '+' și '-', despărțite prin spațiu, indicând pentru fiecare ingredient (de la 1 la m) dacă va fi inclus sau nu în pizza.
Problema se poate testa la:
CSES - Giant Pizza
[0] Chapter Elementary Graph Algorithms, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein
[1] https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
[2] https://en.wikipedia.org/wiki/Biconnected_component
[3] “Depth-first search and linear graph algorithms”, R.Tarjan