Algoritmo di Kruskal
L'algoritmo di Kruskal è un algoritmo goloso[1] ottimo utilizzato per calcolare gli alberi di supporto minimi di un grafo non orientato.
Algoritmo di Kruskal | |
---|---|
Classe | Albero ricoprente minimo |
Struttura dati | Grafo |
Caso peggiore temporalmente | (usando Mfset) |
Caso peggiore spazialmente | |
Prende il nome dal matematico americano Joseph Kruskal che lo ideò e propose nel 1956.
Si consideri un grafo non orientato e connesso dove rappresenta il numero di vertici (o nodi) ed il numero di spigoli (o archi). Ad ogni spigolo è associato un peso (o distanza): lo scopo dell'algoritmo è quello di trovare un albero ricoprente di peso minimo, cioè quello in cui la somma dei pesi sia minima. L'algoritmo può essere applicato solo se si dispone di due o più vertici.
L'algoritmo di Kruskal si basa sulla seguente semplice idea: ordiniamo gli archi in ordine crescente di costo e successivamente li analizziamo singolarmente, inserendo l'arco nella soluzione se non forma cicli con gli archi precedentemente selezionati. Notiamo che ad ogni passo, se abbiamo più archi con lo stesso costo, è indifferente quale viene scelto.
Esempio pratico risolto
modificaLo scopo di questi tipi di problemi è trovare un percorso minimo che copra tutti i nodi della rete indipendentemente dall'ordine. Bisogna schematizzare il problema come nella figura qui sotto indicando con delle lettere i nodi (ossia i punti della rete che dobbiamo collegare) e con dei segmenti i possibili collegamenti tra i nodi, ad ognuno dei quali deve essere assegnato un numero che può indicare un costo, una distanza o comunque un valore da minimizzare.
Un esempio pratico potrebbe essere il progetto di una rete locale di computer dove ogni PC deve essere collegato alla rete ma utilizzando il minor numero possibile di cavi per il collegamento.
Impostiamo quindi un grafo in cui indichiamo con le lettere maiuscole dell'alfabeto i computer (nodi) e con dei segmenti (che si chiamano archi) a cui è associata una certa distanza, ossia la lunghezza di cavo necessaria per collegarli.
In questo caso si sta considerando una rete composta da quattro nodi e 6 archi che rappresentano i possibili collegamenti. Prima di proseguire vanno precisate due cose: l'arco e l'arco non si toccano e, in generale, non è necessario che ogni nodo sia collegato a tutti gli altri.
Per trovare l'albero ricoprente (a peso) minimo si procede elencando tutti gli archi in ordine crescente di costo (in questo caso distanza). Si ottiene quindi il seguente elenco:
- 1
- 2
- 3
- 3
- 4
- 5
Nel caso ci fossero due archi (in questo caso e ) con uno stesso costo si possono indicare in un ordine a piacere lasciando inalterato il risultato. Convenzionalmente si usa disporli in ordine alfabetico.
Ora, per trovare il minimo albero coprente dobbiamo considerare nuovamente il grafico iniziale andando ad evidenziare gli archi, procedendo nell'ordine dell'elenco appena descritto, in modo da non creare circoli chiusi. Vediamo in pratica come si procede: evidenziamo l'arco (costo 1) e lo segnaliamo nell'elenco:
Elenco degli archi
|
Procediamo considerando l'arco (costo 2) e, analogamente a quanto appena fatto, lo evidenziamo nel grafico e lo segniamo nell'elenco:
Elenco degli archi
|
Il prossimo arco dell'elenco sarebbe (costo 3) ma lo saltiamo perché creerebbe un circolo chiuso in quanto collegherebbe insieme il nodo e il nodo che sono già collegati attraverso e lo scopo del problema è creare una rete in cui tutti i nodi sono collegati ma utilizzando il minor cavo possibile.
Si procede dunque con (costo 3) colorando l'arco di rosso ed evidenziandolo nell'elenco:
Elenco degli archi
|
Ora tutti i nodi sono collegati alla rete quindi il problema è concluso. Procedendo con l'elenco, infatti, si potrebbe verificare che gli altri archi rimanenti creerebbero solamente circoli chiusi.
Per concludere si può facilmente calcolare il costo totale della rete facendo la somma dei costi degli archi segnati (quelli che sono stati evidenziati durante l'esercizio). Il costo totale della rete è
Se avessimo espresso i costi (ossia le distanze) tra i nodi in metri (per esempio per collegare il nodo al nodo serve 1 metro di cavo di rete) avremmo ottenuto che per realizzare la rete (al minor costo riguardante il cavo) servirebbero 6 metri di cavo.
L'algoritmo
modificaL'idea di base di Kruskal è la seguente: gestire una foresta di alberi disgiunti tra loro in modo tale da selezionare solo gli archi, di peso minimo, che collegano tra loro questi alberi. Tutti gli alberi della foresta devono essere aciclici.
Proprietà dell'algoritmo
modificaL'algoritmo gestisce una foresta di alberi, ognuno dei quali durante tutta l'esecuzione rimane aciclico. La foresta durante tutta l'esecuzione è sottoinsieme di qualche MST del grafo che si sta considerando. Quando inizia l'algoritmo la foresta è formata dai soli vertici del grafo, senza archi definiti.
- Formalmente
Consideriamo il grafo connesso, non orientato e pesato. Consideriamo sottoinsieme degli archi Inizialmente è vuoto ed è sottoinsieme di qualche MST. Consideriamo contenente tutti i vertici di Inizialmente il grafo non contiene archi. La foresta quindi è formata da alberi disgiunti, dove è il numero di nodi.
Durante l'esecuzione l'algoritmo ricerca un arco sicuro da aggiungere alla foresta. Ricerca, fra tutti gli archi che collegano due alberi qualsiasi nella foresta, un arco di peso minimo.
Quindi esiste una invariante di ciclo.
Finché non sarà composta da un solo albero, ogni albero contenuto in sarà aciclico e sarà un sottoinsieme di qualche MST.
Implementazione dell'algoritmo
modificaUna possibile implementazione dell'algoritmo di Kruskal potrebbe essere la seguente:
- Creare la foresta di grafi.
- Ordinare tutti gli archi del grafo in ordine crescente.
- Scandire gli archi ordinati, uno per volta, inserendoli se opportuno nella foresta.
- L'arco per essere aggiunto alla foresta deve collegare due alberi disgiunti.
- L'arco deve essere sicuro.
- L'arco non deve generare cicli, sempre vero se l'arco unisce due alberi disgiunti.
L'implementazione che viene descritta utilizza il TDA Partition e il TDA PriorityQueue. Il TDA Partition viene utilizzato per mantenere un insieme di oggetti disgiunti (insiemi disgiunti). Il TDA PriorityQueue invece viene utilizzato per mantenere la lista degli archi ordinati dal più piccolo al più grande.
Consideriamo il grafo connesso, non orientato e pesato. Consideriamo sottoinsieme di Quindi conterrà la foresta.
Q sarà la PriorityQueue
P sarà la Partizione
per ogni v vertice di V[G]
P.MAKE-SET(v) // all'interno della partizione vengono inseriti i vari alberi disgiunti
per ogni (u,v) arco di E[G]
Q.ADD(P,(u,v)) // gli archi vengono ordinati dal più piccolo al più grande
// La foresta è ora memorizzata nella partizione e tutti gli archi in ordine crescente nella coda a priorità.
finché la coda Q non è vuota
(u,v) = EXTRACT-MIN(Q)
if P.FIND-SET(u) != FIND-SET(v)
X = X UNION {(u,v)}
P.UNION(u,v)
La partizione viene utilizzata per capire se due vertici fanno parte dello stesso albero, quindi per mantenere valida l'invariante di ciclo. Analizziamo due casi che potrebbero verificarsi durante l'esecuzione del ciclo finché:
- Se l'arco estratto dalla coda Q ha insiemi SET disgiunti vuol dire che è sicuro per infatti l'arco unirebbe due alberi distinti nella foresta. L'arco viene quindi aggiunto all'insieme in modo tale che mantenga l'invariante di ciclo. I due SET vengono poi uniti insieme in modo da identificare un solo albero.
- Se l'arco estratto da Q collega due vertici che fanno già parte dello stesso albero significa che l'arco non è sicuro per Infatti se venisse aggiunto a si verrebbe a creare un ciclo in L'arco viene quindi scartato.
NB: vi è un abuso di notazione dopo i commenti sotto l'istruzione q.add(p....)
Complessità computazionale dell'algoritmo
modificaIl costo computazionale dell'algoritmo è nel caso peggiore dove è il numero di archi e il numero di vertici. È possibile diminuire il costo computazionale dell'algoritmo sino ad ottenere solo utilizzando strutture union-find. Si può migliorare ulteriormente la complessità temporale usando la compressione di cammino del quick-union, che riduce il singolo costo della find a , ottenendo una complessità temporale di
Note
modifica- ^ Marco Liverani, Programmare in C. Guida al linguaggio attraverso esercizi svolti e commentati, Società Editrice Esculapio, 2020, p. 279, ISBN 9788835807896.«È opportuno sottolineare che quello di Kruskal è un algoritmo che appartiene alla famiglia degli algoritmi "golosi", in gergo greedy.»
Bibliografia
modifica- Robert E. Tarjan, Data structures and network algorithms, Philadelphia, Society for Industrial and Applied Mathematics, 1983, ISBN 978-0-89871-187-5, OCLC 10120539.
Voci correlate
modificaAltri progetti
modifica- Wikimedia Commons contiene immagini o altri file sull'algoritmo di Kruskal