Algoritmo di Kruskal

algoritmo utilizzato per calcolare gli alberi di supporto minimi di un grafo

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
ClasseAlbero ricoprente minimo
Struttura datiGrafo
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 dell'algoritmo di Kruskal su un grafo connesso, con i pesi basati sulla distanza euclidea

Esempio pratico risolto

modifica

Lo 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
  •   1
  •   2
  •   3
  •   3
  •   4
  •   5

Procediamo considerando l'arco   (costo 2) e, analogamente a quanto appena fatto, lo evidenziamo nel grafico e lo segniamo nell'elenco:

 
Elenco degli archi
  •   1
  •   2
  •   3
  •   3
  •   4
  •   5

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
  •   1
  •   2
  •   3
  •   3
  •   4
  •   5

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

modifica

L'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

modifica

L'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

modifica

Una 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

modifica

Il 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  

  1. ^ 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

modifica

Altri progetti

modifica