Sunteți pe pagina 1din 11

Paduri de multimi disjuncte

Disjoint set union


Gheorghe Liviu Armand

Enunt:
https://infoarena.ro/problema/disjoint
https://www.pbinfo.ro/probleme/3338/disjoint

Solutia 1: O(N*M)
Problema se poate rezolva reprezentand multimile ca liste inlatuite.
Cand va trebui sa verificam daca 2 elemente se afla in aceeasi multime luam
fiecare element si parcurgem lista lui pana ajungem la sfarsit. Daca pentru
ambele noduri am ajuns la acelasi element atunci ele se afla in aceeasi
multime, altfel nu. Cand vrem sa unim 2 multimi alegem elementul de sfarsit
al primei multimi si il conectam la inceputul celeilalte liste.
Aceasta abordare are complexitatea O(N) pe operatie si se dovedeste
ineficienta.
Solutia 2:
O abordare eficienta este aceea de a reprezenta fiecare multime
(componenta conexa) ca pe un arbore cu radacina. Astfel pentru fiecare
operatie de tip 2 parcurgem arborele in sus din ambele elemente si daca la
sfarsit ajungem in aceeasi radacina atunci elementele noastre se afla in
aceeasi multime. Atunci cand vrem sa unim 2 multimi determinam radacinile
celor 2 arbori si le conectam printr-o muchie.
Momentan complexitatea timp a acestei solutii este tot O(N*M), dar
asupra acestei abordari putem aplica insa 2 optimizari care scad foarte mult
timpul de executie:

1. Reuniunea dupa rang:


Pentru fiecare multime tinem minte dimensiunea arborelui care
reprezinta acea multime si atunci cand vrem sa unim 2 arbori, il unim
pe cel mai mic de cel mai mare.
De ce aceasta optimizare aparent inutila scade complextitatea timp?

Sa presupunem ca avem o componenta conexa X care are la inceput


dimensiunea 1. Astfel cand o vom uni cu alta componenta conexa Y fie
aceasta are dimensiunea mai mare decat dimensiunea lui X, fie mai
mica.

Vom analiza din punct de vedere al componentei X. In cazul 1


complexitatea timp pentru X este O(0)->nu facem nicio operatie
d.p.d.v al lui X, iar in cazul 2 complexitatea timp este O(1). Astfel, in
cel mai rau caz, la fiecare pas vom face O(1).

Dar care este numarul maxim de pasi?

Daca la fiecare pas vom face operatia din cazul 2 inseamna ca va


trebui mereu sa introducem X intr-o componenta conexa de marime >=
cu dimensiunea lui X. Deci la fiecare pas se dubleaza marimea lui X.
Cum dimensiunea lui X poate sa fie maxim N, deducem ca numarul
maxim de pasi este de log(N), rezultand ca atunci cand vom calcula
radacina unui arbore vom avea tot log(N) operatii deoarece vom
parcurge radacini anterioare ale fostelor componente conexe care vor
crea un lant de lungime log(N).

Asadar, pentru fiecare componenta conexa vom face log(n)


operatii. Numarul maxim de componente conexe este N, deci
complexitatea finala a acestei solutii este O(M*log(N)).

2. Compresia drumurilor:

Atunci cand facem o interogare, dupa ce am aflat in ce multime se


afla nodul x, mai parcurgem o data drumul de la x la radacina si unim
toate nodurile direct de radacina.
Astfel data viitoare cand vom avea o interogare pentru unul din
aceste noduri vom ajunge intr-un singur pas la radacina si nu vom mai
parcurge un lant deja vizitat.
Datorita compresiei drumurilor, orice lant va avea lungime de maxim
2 noduri, pentru ca nu vom mai parcurge un lant vizitat si nodurile de pe
acest lant se vor uni direct de radacina.

Astfel un lant de lungime L va fi comprimat, in cel mai rau caz, abia


dupa ce 2 lanturi de lungime L/2 ar fi fost comprimate. Analog,intr-o
maniera recursiva, lanturile de lungime L/2 vor fi comprimate, in cel mai
nefavorabil caz, abia dupa ce lanturile de lungime (L/2)/2 = L/4 ar fi fost
comprimate.

Deducem ca numarul de comprimari este log(L). Avand in vedere ca


L poate fi maxim log(N), complexitatea pe o operatie a acestei metode
este aproximativ log(log(N))=~log*(N).(log de log de N este aproximativ
log stelat de N)

Demonstatii matematice mai riguroase


(https://en.wikipedia.org/wiki/Disjoint-set_data_structure) pot demonstra
ca complexitatea unei operatii este log*(N) (log star de N) ceea ce
reprezinta inversa functiei lui Ackermann si poate fi aproximat cu O(1).
(pentru restrictiile uzuale ale lui N in probleme este O(5), insa este o
valoare infima comparativ cu N si M si poate fi aproximat cu O(1)).

Deci, complexitate finala a acestei solutii este O(M*log*(N)).


Solutia O(N*M): (fara nicio optimizare)

#include<fstream>
using namespace std;
ifstream f("disjoint.in");
ofstream g("disjoint.out");
int tata[100002],dim[100002];
//in aceasta solutie nu mai aveam nevoie de vectorul dim
//insa l-am pastrat pentru a fi comparatia mai buna cu celelalte solutii

int tata_multime(int x)
{
if(x!=tata[x])
return tata_multime(tata[x]);
return x;
}

void unire(int x,int y)


{
x=tata_multime(x); // vedem radacina multimilor care contin
y=tata_multime(y); // nodurile x si y
tata[y]=x; //il legam pe tatal primei multimi de tatal celei de a 2-a
//multimi
dim[x]+=dim[y]; // dimensiunea noii multimi este dimensiunea
// primei multimi + dimensiunea celei de a 2-a
// multimi
}

int main()
{
int n,m,p,x,y,i;
f>>n>>m;
for(i=1;i<=n;i++)
tata[i]=i,dim[i]=1; // initial fiecare nod este singurul dintr-o
// multime de 1 element si este tatal acelei
// multimi
for(i=1;i<=m;i++)
{
f>>p>>x>>y;
if(p==1)
unire(x,y); // unim multimile
else
{
if(tata_multime(x)==tata_multime(y)) // verificam daca sunt
// in aceeasi multime
g<<"DA"<<'\n';
else
g<<"NU"<<'\n';
}
}
return 0;
}

Solutia O(M*log(N)): (cu prima optimizare)

#include<fstream>
using namespace std;
ifstream f("disjoint.in");
ofstream g("disjoint.out");
int tata[100002],dim[100002];

int tata_multime(int x)
{
if(x!=tata[x])
return tata_multime(tata[x]);
return x;
}

void unire(int x,int y)


{
x=tata_multime(x);
y=tata_multime(y);
if(dim[x]<dim[y]) // facem optimizarea 1 si adaugam multimea de
// de dimensiune mai mica in multimea de
// de dimensiunea mai mare
{
dim[y]+=dim[x];
tata[x]=y;
}
else
{
dim[x]+=dim[y];
tata[y]=x;
}
}

int main()
{
int n,m,p,x,y,i;
f>>n>>m;
for(i=1;i<=n;i++)
tata[i]=i,dim[i]=1;
for(i=1;i<=m;i++)
{
f>>p>>x>>y;
if(p==1)
unire(x,y);
else
{
if(tata_multime(x)==tata_multime(y))
g<<"DA"<<'\n';
else
g<<"NU"<<'\n';
}
}
return 0;
}

Solutia O(M*log*(N)): (cu ambele optimizari)

#include<fstream>
using namespace std;
ifstream f("disjoint.in");
ofstream g("disjoint.out");
int tata[100002],dim[100002];

int tata_multime(int x)
{
if(x!=tata[x])
tata[x]=tata_multime(tata[x]); //facem optimizarea 2 si unim
//fiecare nod de pe lant cu
//radacina multimii
return tata[x];
}

void unire(int x,int y)


{
x=tata_multime(x);
y=tata_multime(y);
if(dim[x]<dim[y])
{
dim[y]+=dim[x];
tata[x]=y;
}
else
{
dim[x]+=dim[y];
tata[y]=x;
}
}
int main()
{
int n,m,p,x,y,i;
f>>n>>m;
for(i=1;i<=n;i++)
tata[i]=i,dim[i]=1;
for(i=1;i<=m;i++)
{
f>>p>>x>>y;
if(p==1)
unire(x,y);
else
{
if(tata_multime(x)==tata_multime(y))
g<<"DA"<<'\n';
else
g<<"NU"<<'\n';
}
}
return 0;
}

Aplicatii

1. Arbore partial de cost minim (Minimum spanning tree)


https://infoarena.ro/problema/apm
https://www.pbinfo.ro/probleme/592/kruskal

O solutie ar fi ca la inceput sa eliminam toate muchiile ramanand


cu cele N noduri. Astfel la fiecare pas vom incerca sa unim 2
componente conexe diferite, deoarece nu are rost sa unim 2 noduri din
aceeasi componenta, pentru ca acea componenta nu va mai fi un arbore
si muchia respectiva ar fi in plus. Vom incerca sa adaugam muchiile cu
cel mai mic cost, asa ca le vom sorta crescator dupa cost si vom incerca
sa le adugam pe rand cu ajutorul padurilor de multimi disjuncte. Astfel
vom obtine arborele partial de cost minim.
#include<fstream>
#include<algorithm>
using namespace std;
ifstream f("apm.in");
ofstream g("apm.out");
struct elem
{
int x,y,c;
};
elem v[400002],sol[200002]; // in v retinem muchiile initiale
// in s retinem muchiile care alcatuiesc
// solutia
inline bool cmp(const elem &a,const elem &b)
{
return a.c<b.c; //sortam muchiile crescator dupa cost
}
int tata[200002],dim[200002];

int tata_multime(int x)
{
if(x!=tata[x])
tata[x]=tata_multime(tata[x]);
return tata[x];
}

void unire(int x,int y)


{
x=tata_multime(x);
y=tata_multime(y);
if(dim[x]<dim[y])
{
dim[y]+=dim[x];
tata[x]=y;
}
else
{
dim[x]+=dim[y];
tata[y]=x;
}
}

int main()
{
int n,m,i,k=0,s=0;
f>>n>>m;
for(i=1;i<=m;i++)
f>>v[i].x>>v[i].y>>v[i].c;
sort(v+1,v+m+1,cmp); // sortam muchiile
for(i=1;i<=n;i++)
tata[i]=i,dim[i]=1;
for(i=1;i<=m&&k<n-1;i++)
{
if(tata_multime(v[i].x)!=tata_multime(v[i].y))
{ // daca muchia actuala uneste 2 noduri din multimi diferite
unire(v[i].x,v[i].y); // unim cele 2 multimi
s+=v[i].c; // costul muchiei se aduga la costul apm-ului
sol[++k]=v[i]; // retinem muchia adaugata
}
}
g<<s<<'\n'<<n-1<<'\n'; // evident apm-ul are n-1 muchii
for(i=1;i<=k;i++) // iar k==(n-1)
g<<sol[i].x<<" "<<sol[i].y<<'\n';
return 0;
}

Acest algoritm are complexitate O(M*log(M)+M*log*(N)).


Tema:
https://www.pbinfo.ro/probleme/3339/disjoint1
https://www.pbinfo.ro/probleme/2282/componenteconexe4