Laborator 11: Arbori minimi de acoperire

Bitdefender provides cybersecurity solutions with leading security efficacy, performance and ease of use to small and medium businesses, mid-market enterprises and consumers. Guided by a vision to be the world’s most trusted cybersecurity solutions provider, Bitdefender is committed to defending organizations and individuals around the globe against cyberattacks to transform and improve their digital experience.

Obiective laborator

În acest laborator vom introduce contextul pentru minimum spanning tree problem și vom studia algoritmi care pot rezolva această problemă.

  • Prezentarea problemei (diverse variante).
  • Prezentarea algoritmilor pentru calcul a unui arbore minim de acoperire.

Importanţă – aplicaţii practice

Algoritmii pentru determinarea unor arbori (minimi) de acoperire au multiple aplicații practice. Câteva exemple de aplicații sunt:

  • Designul circuitelor electronice:
    • Maparea pinilor pe un circuit astfel încât să se foloseacă o lungime minimă de traseu.
  • Rețele de calculatoare:
    • Interconectarea mai multor stații în rețea, cu un cost / latență redus(ă).
    • STP Protocol: protocol care previne apariția buclelor într-un LAN.
  • Segmentarea imaginilor:
  • Clustering:

Minimum spanning tree problem

Puteți consulta capitolul Minimum Spanning Trees din Introduction to Algorithms [0] pentru mai multe definiții formale. Această secțiune sumarizează principalele notații folosite în laboratoarele de PA.

Cele mai uzuale notații din laboratoarele de grafuri sunt descrise în Precizări laboratoare 07-12 (ex. $n$, $m$, $adj$, $adj\_trans$, $(x, y)$, etc).

Vom adăuga alte notații în continuarea celor introduse anterior.

Arbore de acoperire / spanning tree: Într-un graf neorientat conex $G = (V, E)$, cu funcția de cost $w: E -> W$, numim un arbore de acoperire un subgraf al lui G cu număr minim de muchii și care interconectează toate nodurile din graf.

Exemplu

Exemplu

 Arbore de acoperire

În exemplul atașat, avem un graf neorientat și conex cu următoare configurație:

  • n = 5, m = 5
  • Pentru acest exemplu, avem 3 arbori de acoperire evidențiați în aceeași figură (rândul 2).

Observație: Un graf conex are cel puțin un arbore de acoperire!


Pădure de arbori de acoperire / spanning tree forest: Se definește similar, dar pentru graf neorientat neconex. Fiecare componentă conexă are un arbore de acoperire / spanning tree. Spunem că acești arbori formează împreună o pădure (forest).

Exemplu

Exemplu

 Pădure de arbori de acoperire

În exemplul atașat, avem un graf neorientat și neconex cu următoare configurație:

  • n = 6, m = 6
  • În figura următoare evidențăm toate pădurile de arbori de acoperire - 9 pe acest exemplu.
    • Putem elimina în 3 moduri câte o muchie din prima componentă conexă - deci 3 arbori posibili.
    • Putem elimina în 3 moduri câte o muchie din a doua componentă conexă - deci 3 arbori posibili.
    • Numărul de păduri este $3 * 3 =9$ (combinăm oricare 2 arbori corespunzători componentelor conexe diferite).

Observație: Un graf neconex are cel puțin o pădure de arbori de acoperire!


Arbore minim de acoperire (AMA)/ minimum spanning tree (MST): Un arbore de acoperire este ȘI arbore minim de acoperire dacă costul total al muchiilor din arbore este minim posibil. Analog se poate defini și noțiunea de pădure de arbori minimi de acoperire (pornind de la cea de pădure de arbori de acoperire).

Exemplu

Exemplu

 Exemplu funcție de cost pentru graf orientat

În exemplul atașat, avem un graf neorientat și conex cu următoare configurație:

  • n = 5, m = 5
  • În figura următoare evidențăm toți arborii de acoperire. Doar 2 dintre aceștia au cost minim 5.

Observație: Un graf conex are cel puțin un arbore minim de acoperire!


Algoritmi

Pentru minimum spanning tree problem există mai mulți algoritmi, dintre care amintim:

  • Kruskal: abordare greedy care se bazează pe sortarea muchiile după cost. Mai multe detalii în Introduction to Algorithms [0].
  • Prim: abordare greedy care extinde arborele căutat pornind de la o rădăcină (nod ales oarecare). Mai multe detalii în Introduction to Algorithms [0].
  • Karger, Klein & Tarjan: algoritm randomizat cu complexitate liniară (mai bună decât Kruskal și Prim). Mai multe detalii în A Randomized Linear-Time Algorithm to Find Minimum Spanning Trees [1].
  • Bernard Chazelle: algoritm determinist cu o complexitate liniară (cu complexitate mai bună decât soluția anterioară). Mai multe detalii în A Minimum Spanning Tree Algorithm with Inverse Ackermann Type Complexity [2].

În acest laborator vom studia și vom implementa algoritmul lui Kruskal. De asemenea, oferim ca studiu de caz pentru acasă, un material pentru algoritmul lui Prim.

Kruskal

Algoritmul lui Joseph Bernard **Kruskal** (Kruskal’s algorithm) rezolvă minimum spanning tree problem în grafuri neorientate G = (V, E) cu costurile muchiilor oarecare aplicând o strategie greedy foarte simplă: muchiile de cost minim probabil fac parte din MST!

Kruskal - Pseudocod

Algoritmul lui Kruskal folosește o structură de date care suportă 2 operații:

  • În ce mulțime este elementul x?
  • Să se reunească mulțimile din care fac parte elementele x și y.

Se pot folosi mai multe structuri de date. Pentru o implementare eficientă, alegem DisjointSet. Vă recomandăm să parcurgeți DisjointSet. ATENȚIE! Scheletul de laborator oferă o astfel de implementare.

// apply Kruskal's algorithm for computing a Minimum Spanning Tree (MST).
//
// nodes     = list of all nodes from G
// edges     = the list of all edges in G
//             example: edge (node, neigh, weight)
//
// returns: cost, mst
//          cost = the cost of the MST
//          mst = the actual set of edges in MST
//
Kruskal(G=(nodes, edges)) {
  // STEP 0: initialize results
  cost = 0; // cost of the built MST
  mst = {}; // the actual set of edges (u, v) in the built MST
 
  // STEP 1: initialize disjoint set data structure
  // (a tree is created for each node)
  disjointset = DisjointSet(nodes);
 
  // STEP 2:
  sort(edges, compare=nondecreasing order by weight);
 
  // STEP 3: Add edge by edge to MST if no cycles are created.
  foreach ( (u, v, w) in edges ) {
    // STEP 3.1: Check if u anv v are in different trees.
    if (disjointset.setOf(u) != disjointset.setOf(v)) {
      // STEP 3.2: Merge these 2 trees (no cycles created).
      disjointset.union(u, v);
 
      // STEP 3.3: Extend MST with the current edge.
      cost += w;
      mst += { (u, v) };
    }
  }
 
  return cost, mst;
}
 
// Usage example:
cost, mst = Kruskal(G=(nodes, edges));
// 1. Use cost of MST.
// 2. Use edges of MST (e.g. actually build the tree).

Algoritmul lui Kruskal pornește cu structura de date Păduri de mulțimi disjuncte inițializată cu n noduri izolate. Muchiile sunt sortate crescător după cost și se încearcă adăugarea lor, rând pe rând la pădurea de arbori.

  • Dacă o muchie $(u, v)$ are capetele în 2 arbori diferiți, atunci prin adăugarea acesteia, nu vom crea ciclu, ci vom crea un arbore mai mare (din cei 2 mici reuniți).
  • Altfel, nu putem adăuga muchia, deoarece am crea un ciclu. Această muchie se ignoră.

Exemple

Exemplu Kruskal

 Exemplu Kruskal

Un MST găsit cu algoritmul lui Kruskal este: ${ (1, 2); (2, 4); (2, 3); (3, 5)}$ de cost 5.

Explicație pas cu pas

Explicație pas cu pas

În exemplul atașat, avem un graf neorientat cu următoare configurație:

  • n = 5, m = 5
  • Funcția de cost w are valorile menționate pe muchii.
  • Muchiile sortate după cost sunt:
    • $(3, 5)$ de cost $-1$
    • $(2, 4)$ de cost $1$
    • $(2, 3)$ de cost $2$
    • $(2, 5)$ de cost $2$
    • $(1, 2)$ de cost $3$
  • Explicație pas cu pas
    • STEP 0: Inițializăm pădurile de mulțimi disjuncte - pornind cu n arbori (noduri izolate).
    • STEP 1: Muchia cu costul cel mai mic care e nefolosită este $(3, 5)$ de cost -1. Se adaugă la MST. Se reunesc 2 arbori.
    • STEP 2: Muchia cu costul cel mai mic care e nefolosită este $(2, 4)$ de cost 1. Se adaugă la MST. Se reunesc 2 arbori.
    • STEP 3: Muchia cu costul cel mai mic care e nefolosită este $(2, 3)$ de cost 2. Se adaugă la MST. Se reunesc 2 arbori.
      • Observație: În acest pas am fi putut alege muchia $(2, 5)$ cu același cost.
    • STEP 4:
      • Muchia $(2, 5)$ nu va fi folosită deoarece are capetele în același arbore.
      • Muchia cu costul cel mai mic care e nefolosită este $(1, 2)$ de cost 3. Se adaugă la MST. Se reunesc 2 arbori.
    • Avem un arbore. STOP.

Observație: În STEP 3 pas am fi putut alege muchia $(2, 5)$ cu același cost. Obțineam un MST care diferea printr-o singură muchie ({${ (1, 2); (2, 5); (2, 3); (3, 5)}}$), dar cu același cost 5.


Complexitate

  • complexitate temporală: $T = O(m * log m)\ sau\ O(|E| * log |E|)$
  • complexitate spațială : $S = O(n)$

Detalii (analiză + optimizări)

Detalii (analiză + optimizări)

  • complexitate temporală:
    • Se sortează cele m muchii în ordine crescătoare după cost - $O(m log m)$.
    • Se folosesc n operații pe DisjointSet - $O(n log_{*}n)$, care poate fi aproximat cu $O(n)$ (demonstrație pe pagina DisjointSet).
      • ATENȚIE! Complexitatea conține funcția logaritm iterat, nu logaritm ( Iterated logarithm), care pentru valorile uzuale din problemele de algoritmică poate fi aproximată cu o constantă foarte mică.
  • complexitate spațială : Se ține o structură de date DisjointSet cu maximum n noduri.


[Studiu de caz] Prim

Algoritmul lui Robert C **Prim** (Prim’s algorithm) rezolvă minimum spanning tree problem în grafuri neorientate G = (V, E) cu costurile muchiilor oarecare aplicând o strategie constructivă greedy, asemănătoare cu algoritmul lui Dijkstra: merge pe muchiile de cost minim, din aproape în aproape, considerând o sursă inițial aleasă aleator și extinzând MST-ul curent cu muchia de cost minim neadăugată încă la arbore.

Prim Pseudocod

Prim - Pseudocod

Prim - Pseudocod

// apply Prim's algorithm for computing a Minimum Spanning Tree (MST).
//
// source    = source / starting node for computing / expading MST
//             (if not specified, node 1 is defaulted)
// nodes     = list of all nodes from G
// adj[node] = the adjacency list of node
//             example: adj[node] = {..., (neigh, weight) ...} => edge (node, neigh, weight)
//
// returns: cost, mst
//          cost = the cost of the MST
//          mst = the actual set of edges in MST
//
Prim(source=1, G=(nodes, adj)) {
  // STEP 0: initialize results
  // d[node] = distance from MST (any node in current/partial MST) to node
  // p[node] = parent of node: node is connected to MST with edge p[node] - node
  foreach (node in nodes) {
      d[node] = +oo;                          // distance not yet computed
      p[node] = null;                         // parent not yet found
  }
 
  // STEP 0: initialize results
  cost = 0; // cost of the built MST
  mst = {}; // the actual set of edges (u, v) in the built MST
 
  // STEP 1: initialize a priority queue
  pq = {};
 
  // STEP 2: add the source(s) into pq
  d[source] = 0;                              // distance from source to MST
  p[source] = null;                           // source never has parent
  pq.push( (source, d[source]) );
 
  // STEP 3: Build MST.
  while ( !pq.empty() ) {
    // STEP 3.1: Pop next possible node to connect with MST.
    node, _ = pq.pop()
    if (used[node]) {
      continue;
    }
 
    // STEP 3.2: Extend MST with edge p[node] - node
    used[node] = true;
    if (p[node] != null) {
      cost += d[node];
      mst += { (node, p[node]) };
    }
 
    // STEP 3.3: relax all edges (node, neigh)
    foreach ( (neigh, weight) in adj[node]) {
      // Can we find a better / shorter link / edge from node to current MST?
      if (!used[neigh] && weight < d[neigh]) {
        d[neigh] = weight;                              // update the new distance from neigh to MST
        p[neigh] = node;                                // save parent
 
        pq.push( (neigh, d[neigh]) );                   // replace distance for neigh in pq
      }
    }
  }
 
  return cost, mst;
}
 
// Usage example:
cost, mst = Prim(1, G=(nodes, edges)); // e.g. source = 1, can be any node in G
// 1. Use cost of MST.
// 2. Use edges of MST (e.g. actually build the tree).


Complexitate

  • complexitate temporală: $T = O(m * log n)\ sau\ O(|E| * log |V|)$
  • complexitate spațială : $S = O(n)$

Detalii (analiză + optimizări)

Detalii (analiză + optimizări)

  • complexitate temporală: Se încearcă relaxarea tuturor celor m muchii din graf, analog algoritmului Dijkstra.
  • complexitate spațială : Se ține o coadă de priorități / un set cu maximum n noduri.
  • optimizare: Analog Dijkstra, putem să obținem:
    • Prim cu heap binar - $O(m log n)$.
    • Prim cu heap Fibonacci - $O(n logn + m)$.


TLDR

  • Pentru minimum spanning tree problem, am studiat 2 algoritmi (Kruskal și Prim) cu aceeași complexitate ($O(m log m)$).
  • Algoritmul lui Kruskal este de preferat pentru simplitatea / intuitivitatea lui, dificultatea reducându-se la a implementa structura de date DisjointSet.

Exerciții

Scheletul de laborator se găsește pe pagina pa-lab::skel/lab11.

Înainte de a rezolva exercițiile, asigurați-vă că ați citit și înțeles toate precizările din secțiunea Precizari laboratoare 07-12.

Prin citirea acestor precizări vă asigurați că:

  • știți convențiile folosite
  • evitați buguri
  • evitați depunctări la lab/teme/test

Kruskal

Se dă un graf neorientat și conex cu n noduri și m muchii (cu costuri oarecare pe muchii).

Folosiți algoritmul lui Kruskal pentru a găsi un MST.

Task-uri:

  1. Găsiți costul MST.
  2. Găsiți care sunt muchiile din MST. ATENȚIE! Se poate găsi orice MST (în caz că există mai mulți). Ordinea muchiilor din vectorul rezultat NU este relevantă.

Restricții și precizări:

  • $ n <= 2 * 10^5 $
  • $ m <= 4 * 10^5 $
  • $ -10^3 <= c <= 10^3$, unde c este costul unei muchii
  • timp de execuție
    • C++: 1s
    • Java: 7s
  • Vă recomandăm să parcurgeți DisjointSet. ATENȚIE! Scheletul de laborator oferă o astfel de implementare.

BONUS

La acest laborator, asistentul va alege 1-2 probleme din secțiunea extra.

Extra

Referințe

[0] Chapters Minimum Spanning Trees, “Introduction to Algorithms”, Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein.

[1] A Randomized Linear-Time Algorithm to Find Minimum Spanning Trees, David R. Karger, Philip N. Klein, Robert E. Tarjan.

[2] A Minimum Spanning Tree Algorithm with InverseAckermann Type Complexity|, Bernard, Chazelle.

pa/laboratoare/laborator-11.txt · Last modified: 2022/05/17 15:08 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