Algoritmi su Grafi: Minimo Albero Ricoprente, Algoritmo di Kruskal e di Prim

Minimo albero ricoprente - Kruskal e Prim header

Il “Minimo Albero Ricoprente” (Minimum Spanning Tree – MST) di un grafo connesso è un sottoinsieme degli archi di quel grafo che forma un albero, e questo albero deve connettere tutti i nodi del grafo. Inoltre, la somma dei pesi degli archi in questo albero deve essere la più bassa possibile.

L’algoritmo del Minimo Albero Ricoprente

In altre parole, immagina di avere un grafo in cui ogni arco ha un peso associato. Un Minimo Albero Ricoprente di questo grafo è un insieme di archi che collega tutti i nodi senza formare cicli e la somma dei pesi di questi archi è la più bassa possibile tra tutti gli alberi che possono essere formati collegando tutti i nodi del grafo.

Questo problema è rilevante in molte situazioni pratiche. Ad esempio, potrebbe rappresentare la rete stradale di una città, dove si desidera costruire una serie di strade in modo che ogni zona della città sia accessibile e la somma delle lunghezze delle strade sia minima. O potrebbe rappresentare una rete di computer, dove gli archi rappresentano i collegamenti tra i computer e si desidera minimizzare il costo della connettività.

Gli algoritmi come l’algoritmo di Kruskal e l’algoritmo di Prim sono utilizzati per risolvere il problema del Minimo Albero Ricoprente. Questi algoritmi sono implementati per trovare in modo efficiente l’insieme di archi che costituiscono un Minimo Albero Ricoprente in un grafo pesato.

Algoritmo di Kruskal

L’algoritmo di Kruskal è un algoritmo greedy utilizzato per trovare il Minimo Albero Ricoprente (MST) di un grafo non orientato e pesato. La sua idea di base è quella di selezionare gli archi più leggeri in ordine crescente e aggiungerli all’albero, evitando di formare cicli. L’algoritmo termina quando tutti i nodi sono collegati o quando tutti gli archi sono stati considerati.

Ecco una descrizione dettagliata del funzionamento dell’algoritmo di Kruskal:

  1. Inizializzazione: Ordina gli archi del grafo in ordine crescente rispetto ai loro pesi.
  2. Ciclo principale: Itera attraverso gli archi in ordine crescente. Per ogni arco, controlla se i nodi che connette appartengono a componenti connesse diverse. Se non formano un ciclo, aggiungi l’arco all’albero e unisci le componenti connesse dei due nodi.
  3. Terminazione: Ripeti il passo 2 fino a quando tutti i nodi sono collegati o tutti gli archi sono stati considerati.

Caratteristiche dell’algoritmo di Kruskal:

  • Greedy: L’algoritmo fa scelte locali ottimali in ogni passo nella speranza di ottenere una soluzione globalmente ottimale.
  • Non forma cicli: Per evitare la formazione di cicli, verifica se l’aggiunta di un arco crea un ciclo nel MST corrente.
  • Struttura dati Union-Find: L’algoritmo di Kruskal spesso utilizza una struttura dati chiamata Union-Find per tenere traccia delle componenti connesse e facilitare la verifica della presenza di cicli.

Ecco un esempio di implementazione in Python dell’algoritmo di Kruskal:

class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = []

    def add_edge(self, u, v, w):
        self.graph.append([u, v, w])

    def kruskal(self):
        self.graph = sorted(self.graph, key=lambda item: item[2])
        parent = [i for i in range(self.V)]
        result = []

        def find(x):
            if parent[x] != x:
                parent[x] = find(parent[x])
            return parent[x]

        def union(x, y):
            root_x = find(x)
            root_y = find(y)
            if root_x != root_y:
                parent[root_x] = root_y
                return True
            return False

        for edge in self.graph:
            u, v, w = edge
            if union(u, v):
                result.append([u, v, w])

        return result

# Example of usage
g = Graph(4)
g.add_edge(0, 1, 10)
g.add_edge(0, 2, 6)
g.add_edge(0, 3, 5)
g.add_edge(1, 3, 15)
g.add_edge(2, 3, 4)

mst = g.kruskal()
print("Minimum Spanning Tree:")
for edge in mst:
    print(edge)

In questo esempio, g.kruskal() restituirà il Minimo Albero Ricoprente del grafo definito. I pesi degli archi sono dati dalla terza componente di ciascun arco nella rappresentazione [u, v, w].

Eseguendo otterrai il risultato seguente:

Minimum Spanning Tree:
[2, 3, 4]
[0, 3, 5]
[0, 1, 10]

Algoritmo di Prim

L’algoritmo di Prim è un altro algoritmo greedy utilizzato per trovare il Minimo Albero Ricoprente (MST) di un grafo non orientato e pesato. A differenza di Kruskal, l’algoritmo di Prim inizia da un nodo arbitrario e aggiunge iterativamente l’arco più leggero che collega un nodo già incluso nell’albero a uno fuori dall’albero.

Ecco come funziona l’algoritmo di Prim:

  1. Inizializzazione: Scegli un nodo di partenza. Inizializza un set di nodi già inclusi nell’albero e un set di archi dell’albero, entrambi inizialmente vuoti.
  2. Aggiornamento degli insiemi: Finché ci sono nodi fuori dall’albero, trova l’arco più leggero che collega un nodo all’interno dell’albero a un nodo al di fuori dell’albero. Aggiungi il nodo e l’arco all’albero.
  3. Terminazione: Continua fino a quando tutti i nodi sono inclusi nell’albero.

Caratteristiche dell’algoritmo di Prim:

  • Greedy: Come Kruskal, l’algoritmo di Prim fa scelte locali ottimali in ogni passo nella speranza di ottenere una soluzione globalmente ottimale.
  • Non forma cicli: Poiché un solo nodo viene aggiunto in ogni passo, l’algoritmo di Prim non forma cicli.
  • Struttura dati Heap: L’algoritmo spesso utilizza una coda di priorità (heap) per mantenere traccia degli archi più leggeri.

Ecco un esempio di implementazione in Python dell’algoritmo di Prim:

import heapq

class Graph:
    def __init__(self, vertices):
        self.V = vertices
        self.graph = {}

    def add_edge(self, u, v, w):
        if u not in self.graph:
            self.graph[u] = {}
        if v not in self.graph:
            self.graph[v] = {}

        self.graph[u][v] = w
        self.graph[v][u] = w

    def prim(self, start_node):
        heap = [(0, start_node)]
        included = set()
        mst = []

        while heap:
            weight, node = heapq.heappop(heap)
            if node not in included:
                included.add(node)

                for neighbor, edge_weight in self.graph[node].items():
                    heapq.heappush(heap, (edge_weight, neighbor))
                if len(mst) < self.V - 1:
                    mst.append((node, neighbor, edge_weight))

        return mst

# Example of usage
g = Graph(4)
g.add_edge(0, 1, 10)
g.add_edge(0, 2, 6)
g.add_edge(0, 3, 5)
g.add_edge(1, 3, 15)
g.add_edge(2, 3, 4)

mst = g.prim(0)
print("Minimum Spanning Tree:")
for edge in mst:
    print(edge)

In questo esempio, g.prim(0) restituirà il Minimo Albero Ricoprente del grafo definito, partendo dal nodo 0 come nodo di partenza. I pesi degli archi sono dati dai valori associati agli archi nel dizionario graph.

Eseguendo otterrai il seguente risultato:

Minimum Spanning Tree:
(0, 3, 5)
(3, 2, 4)
(2, 3, 4)

Alcune considerazioni su questi algoritmi

Entrambi gli algoritmi di Kruskal e Prim per il calcolo del Minimo Albero Ricoprente (MST) in un grafo pesato hanno complessità asintotiche che dipendono principalmente dal modo in cui vengono implementati e dalle strutture dati utilizzate.

Algoritmo di Kruskal:
La complessità asintotica dell’algoritmo di Kruskal dipende principalmente dall’ordinamento degli archi e dalla struttura dati Union-Find utilizzata. Supponendo che si utilizzi un algoritmo di ordinamento efficiente come il Merge Sort o l’Heap Sort, e una struttura dati Union-Find basata su union-per-rank e path compression, la complessità asintotica dell’algoritmo di Kruskal è di O(E log E), dove E è il numero degli archi nel grafo.

Algoritmo di Prim:
La complessità asintotica dell’algoritmo di Prim dipende principalmente dall’implementazione della coda di priorità (heap). Utilizzando una coda di priorità basata su min-heap, l’algoritmo di Prim ha una complessità asintotica di O((V + E) log V), dove V è il numero di nodi e E è il numero di archi nel grafo.

In entrambi i casi, la complessità è dominata dall’operazione di ordinamento (per Kruskal) o dalle operazioni sulla coda di priorità (per Prim). Scegliendo algoritmi efficienti per queste operazioni, è possibile ottenere prestazioni accettabili anche per grafi di grandi dimensioni.

Strutture Dati Utilizzate:

  • Kruskal: Utilizza la struttura dati Union-Find per gestire le componenti connesse e verificare la presenza di cicli.
  • Prim: Utilizza una coda di priorità (heap) per gestire gli archi più leggeri da aggiungere all’albero.

Complessità Temporale:

  • Kruskal: La complessità asintotica è O(E log E), dove E è il numero di archi. Questa complessità è spesso migliore per grafi sparsi.
  • Prim: La complessità asintotica è O((V + E) log V), dove V è il numero di nodi e E è il numero di archi. Questa complessità può essere migliore per grafi densi.

Caso Migliore e Peggior Caso:

  • Kruskal: Solitamente performa bene anche su grafi densi, ma il caso migliore si ha quando gli archi sono già ordinati.
  • Prim: Solitamente performa bene su grafi densi e il caso migliore si ha quando la coda di priorità è implementata efficientemente.

Struttura dell’Albero Prodotta:

  • Kruskal: L’algoritmo seleziona gli archi in base al peso e potrebbe creare più alberi temporanei prima di ottenere un MST unico.
  • Prim: L’algoritmo costruisce l’albero dal nodo di partenza, garantendo un MST unico senza la necessità di fusioni successive.

Implementazione e Memoria:

  • Kruskal: Richiede una fase di ordinamento degli archi e l’implementazione di Union-Find.
  • Prim: Richiede una coda di priorità e non richiede ordinamenti, ma la gestione della coda può richiedere memoria addizionale.

Scelta del Nodo di Partenza:

  • Kruskal: Indipendente dalla scelta del nodo di partenza.
  • Prim: Il risultato può variare in base al nodo di partenza, ma tutti gli MST ottenuti sono equivalenti in peso.

Utilizzo in Contesti Specifici:

  • Kruskal: Può essere preferito quando il grafo è sparso e la ricerca di archi più leggeri è più efficiente.
  • Prim: Può essere preferito quando il grafo è denso e la gestione di una coda di priorità è più efficiente.

In generale, la scelta tra Kruskal e Prim dipende dalle caratteristiche specifiche del grafo e dai requisiti prestazionali del contesto in cui vengono utilizzati. Entrambi gli algoritmi sono efficienti e forniscono soluzioni ottimali per il problema del Minimo Albero Ricoprente.

Lascia un commento