În unele situații se cere gruparea elementelor unei mulțimi date într-o colecție de submulțimi disjuncte. Pentru o astfel de colecție sunt importante următoarele operații:
Pentru fiecare submulțime se stabilește un reprezentant – unul dintre elementele submulțimii. Fiecare element al submulțimii este asociat într-un anumit mod cu reprezentantul acesteia. În acest fel, operația de stabilire a submulțimii din care face parte un anumit element constă în simpla identificare a reprezentantului, iar operația de reuniune a două submulțimii constă în asocierea elementelor unei submulțimii cu reprezentantul celeilalte. Două elemente fac parte din aceeași submulțime dacă sunt asociate cu același reprezentant.
Operațiile cu mulțimi disjuncte pot fi folosite pentru determinarea componentelor conexe ale unui graf, astfel:
(x,y)
stabilim dacă x
și y
fac parte din submulțimi diferite, caz în care reunim cele două submulțimi;O altă aplicație a acestor structuri de date este Algoritmul lui Kruskal pentru determinarea arborelui parțial de cost minim a unui graf neorientat cu costuri.
O modalitate eficientă de gestionare a submulțimilor și a operațiilor cu acestea este utilizarea unor structuri arborescente (a unei păduri), numite pădure de mulțimi disjuncte, în care:
Gestionarea arborilor se poate face prin intermediul unui vector de tați:
T[k] = 0
, dacă k
este rădăcină a unui arbore (și reprezentant al submulțimii corespunzătoare)T[k] =
tatăl lui k
în arborele din care face parteExemplu
Fie mulțimea {1,2,3,4,5,6,7, 8}
cu submulțimile disjuncte {1,3,4}
, {2,5,7}
și {6,8}
. O reprezentare prin păduri de mulțimi disjuncte poate fi:
Ei îi corespunde următorul vector de tați:
k |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
T[k] |
0 |
5 |
4 |
1 |
0 |
0 |
5 |
6 |
Dacă reunim submulțimile {2,5,7}
și {6,8}
, pădurea devine:
și îi corespunde următorul vector de tați:
k |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
T[k] |
0 |
5 |
4 |
1 |
0 |
5 |
5 |
6 |
Ambele operații (stabilirea submulțimii din care face parte un anumit element și reuniunea a două submulțimi) presupun identificarea reprezentantului unei submulțimii, adică a rădăcinii arborelui asociat submulțimii, operație ce presupune parcurgerea arborelui de la un nod spre rădăcină.
Următoarea secvență C++ gestionează vectorul de tați T[]
, declarat global, prin intermediul a două funcții: int Radacina(int k)
, care determină rădăcina arborelui din care face parte nodul k
(reprezentantul lui k
) și void Unire(int k, int p)
, care realizează operația de reuniune a submulțimilor din care face parte k
și p
.
int T[...]; int Radacina(int k){ if(T[k] == 0) return k; else return Radacina(T[k]); } void Unire(int k, int p) { int rk = Radacina(k), rp = Radacina(p); if(rk != rp) T[rk] = rp; }
Această implementare a operațiilor poate conduce la arbori cu înălțime mare. Acest fapt are efect asupra operației de determinare a rădăcinii arborelui din care face parte un nod dat, care este cu atât mai rapidă cu cât lungimea lanțului de la nod la rădăcină este mai mică. În gestionarea pădurilor de mulțimi disjuncte se pot folosi două euristici care duc la complexitate aproape liniară în raport cu numărul total de operații:
Următoarea secvență C++ îmbunătățește operațiile de mai sus cu ajutorul celor două euristici. Pentru determinarea rangurilor folosim un vector suplimentar, Rang[]
:
int t[...]; int Radacina(int k){ if(T[k] == 0) return k; else { int x = Radacina(T[k]); T[k] = x; return x; } } void Unire(int k, int p) { int rk = Radacina(k), rp = Radacina(p); if(rk != rp) { if(Rang[rk] > Rang[rp]) T[rp] = rk; else { T[rk] = rp; if(Rang[rk] == Rang[rp]) Rang[rp] ++; } } }