Î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] ++; } } }
Un arbore este un graf conex și aciclic. Se mai numește și arbore liber.
Următoarele propoziții sunt adevărate:
n
vârfuri are n-1
muchii.Pentru un arbore se poate stabili un nod special, numit rădăcină. Putem spune că “agățăm” arborele în rădăcină, iar restul nodurilor cad.
Mai jos avem trei arbori cu rădăcină. Toți pornesc de la arborele de mai sus, dar diferă prin alegerea rădăcinii.
Fie un arbore cu rădăcina r
și x
un nod în acest arbore. Atunci:
x
orice nod y
, diferit de x
, aflat pe lanțul de la rădăcină la x
;
y
este ascendent al lui x
și există muchia (y,x)
, atunci y
se numește ascendent direct al lui x
sau tatăl lui x
;
y
este descendent al nodului x
, diferit de y
, dacă x
aparține lanțului de la r
la y
;
(x,y)
, atunci y
este descendent direct sau fiu al lui x
;x
reprezintă nivelul sau adâncimea nodului x
;Fie arborele următor:
3
;4
sunt 5
, 2
și 3
. Ascendentul direct (tatăl) al nodului 4
este nodul 5
;2
sunt 1 7 10 5 4 6
. Descendenții direcți ai nodului 2
sunt 1 5
;1
și 5
sunt frați;6 7 8 10 12
sunt frunze;0
conține doar rădăcina: 3
;1
conține nodurile 2 9
;2
conține nodurile 1 5 8 11
;3
conține nodurile 7 10 4 12
;4
conține nodul 6
;4
;9 8 11 12
formează un subarbore;Pentru fiecare nod al arborelui se memorează informații despre descendenții săi direcți. Este similară cu reprezentarea prin liste de adiacențe a grafurilor. Pentru arborele de mai sus avem:
F[1]={7,10}
F[2]={1,5}
F[3]={2,9}
F[4]={6}
F[5]={4}
F[6]={}
F[7]={}
F[8]={}
F[9]={8,11}
F[10]={}
F[11]={12}
F[12]={}
Pentru fiecare nod se memorează informații despre ascendenții direcți. Vom obține un vector de tați, în care:
t[r] = 0
, unde r
este rădăcina arboreluit[k] =
tatăl nodului k
Pentru arborele de mai sus avem:
k |
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
t[k] |
2 |
3 |
0 |
5 |
2 |
4 |
1 |
9 |
3 |
1 |
9 |
11 |
Observații
0
, corespunzătoare rădăcinii;x
x
, y = t[x]
;y
, t[y]
z
pentru care t[z]=0
. acesta va fi rădăcina și ne oprim.