L'ADT degli insiemi disgiunti


Sia S un insieme: una partizione finita di S è una famiglia finita di sottoinsiemi di S tale che S sia l'unione della famiglia, ed i membri della famiglia siano due a due disgiunti. Una partizione induce un concetto di equivalenza tra gli elementi di S: x equivale a y se entrambi appartengono allo stesso insieme della partizione. Nel seguito ogni insieme della partizione, ovvero ogni classe d' equivalenza, sarà associato ad un elemento che gli appartiene (si assume che S sia non vuoto e che tali siano gli insiemi della partizione), detto il suo rappresentante.

L'ADT degli insiemi disgiunti è così specificato:

datatype Disjoint_Set, Set, T;

constructors: create: Set -> Disjoint_Set;
              union: Disjoint_set,T,T -> Disjoint_Set;
			  
observation:  find: Disjoint_Set,T -> T;

semantics: create(S) ritorna la partizione di S in cui ogni insieme e' un singoletto
           union(D,x,y) ritorna la partizione che si ottiene da D rimpiazzando
                        gli insiemi di cui x e y sono rappresentanti con la loro unione
           find(D,a) ritorna il rappresentante dell'insieme in D cui a appartiene,
                     se esiste; un valore nullo altrimenti.
Una possibile realizzazione di questo ADT fa uso di una foresta di alberi realizzati con la tecnica del vettore dei padri (detti anche up-trees, perché ogni vertice punta al suo predecessore invece che ai suoi successori). Ad esempio l'insieme S={a,b,c,d,f} partizionato in D={{a,b,c,d},{e,f,g}} lo possiamo rappresentare con la foresta:

in cui gli elementi in radice, d,g, rappresentano i due insiemi della partizione. L'operazione union(D,d,g) trasforma questa partizione nella partizione {{a,b,c,d,e,f,g}}, ottenuta, ad esempio, facendo diventare g un figlio di d:

In questo modo il costo in tempo dell'unione è O(1). Lasciando indeterminato il criterio per cui si è scelto di rendere g figlio di d e non viceversa, successive operazioni di unione possono trasformere la foresta che rappresenti la partizione triviale (in singoletti) in un albero degenere, su cui l'operazione find avrebbe costo O(n). Si può invece ottenere che il tempo di find sia molto prossimo ad O(1) (un buon confine superiore è O(m lg* n) dove lg* è la funzione logaritmica iterata, m è il numero di operazioni compiute a partire dalla partizione iniziale, ed n è la cardinalità dell'insieme partizionato).

Il metodo per ottenere questo risultato combina due euristiche (Cormen et alii. cap. 22):

Esercizio. Si implementi l'ADT Disjoint_Set usando le euristiche dell'unione per rango e della compressione del cammino.
Soluzione