Fibonacci Heaps e il loro utilizzo nell’algoritmo di Prim Paolo Larcheri 52SI Cosa vedremo Cos’è uno heap Cenni sull’analisi ammortizzata Cosa sono gli Heap di Fibonacci Gli algoritmi di Minimum Spanning Tree L’utilizzo degli Heap di Fibonacci nell’algoritmo di Prim Complessità dell’algoritmo di Prim Esempio di funzionamento dell’algoritmo di Prim Gli heap Lo heap, in generale, è una struttura dati atta a contenere un insieme di dati ordinabili A prescindere dall’implementazione gli heap devono fornire le seguenti operazioni: INSERISCI: inserimento di un nuovo elemento TROVA-MIN: ricerca dell’ elemento con chiave minima ESTRAI-MIN: estrazione dell’elemento con chiave minima UNIONE: “fusione” di due heap DECREMENTA-CHIAVE: dato un elemento dello heap e un nuovo e minore valore per la chiave dello stesso, aggiorna la chiave al nuovo valore CANCELLA: cancellazione di uno specifico elemento Gli Heap di Fibonacci (1) Introdotti da Fredman e Tarjan nel 1987 Possono essere considerati un’ottima implementazione di Heap!! Sono stati progettati ispirandosi all’analisi ammortizzata al fine di ottenere ben precisi costi. Cos’è l’analisi ammortizzata L’analisi ammortizzata mira ad esprimere il tempo di esecuzione di un’intera sequenza di operazioni attribuendo un costo nominale ad ogni tipologia di operazione (inserimento, cancellazione, …). Anche se una singola operazione può forare il suo costo nominale, l’importante e che il costo complessivo dell’intera sequenza resti entro la somma dei costi nominali per le singole operazioni che la compongono. Ovviamente vogliamo dichiarare dei costi nominali il piu bassi possibili! Gli Heap di Fibonacci (2) Le complessità delle operazioni: Operazione costo reale INSERISCI O(1) O(1) O(1) O(lg(n) + t(H)) O(1) O(lg(n)) O(1) O(1) O(lg(n)) O(1) O(lg(n) + t(H)) O(lg(n)) TROVA-MIN ESTRAI-MIN UNIONE DECR-CHIAVE CANCELLA costo amm. t(H): numero di radici dello Heap di Fibonacci Gli Heap di Fibonacci (3) Gli Heap di Fibonacci sono costituiti da una lista di alberi caratterizzati ciascuno dall’ “ordinamento parziale dello heap”: la chiave di ogni nodo è minore o uguale alla chiave dei figli; in questo modo è garantito che il nodo con chiave minima è una delle radici Ogni elemento dello Heap di Fibonacci punta al nodo-padre Ogni nodo punta alla lista dei figli Gli Heap di Fibonacci (4) Ogni istanza di Heap di Fibonacci è costituita dai seguenti attributi: puntatore alla lista delle radici puntatore al nodo con chiave minima numero complessivo di alberi contenuti nello Heap numero totale di nodi presenti nello Heap Gli Heap di Fibonacci (5) Ogni elemento dello Heap di Fibonacci è collegato direttamente al nodo-padre Inoltre i suoi figli sono collocati in una lista Gli attributi di ogni nodo sono: puntatore al nodo-padre puntatore alla lista dei figli 2 puntatori: uno al suo fratello destro e uno al suo fratello sinistro il grado (intero): numero dei suoi figli chiave: il valore del nodo Gli Heap di Fibonacci (6) Rappresentazione “logica ” di uno Heap di Fibonacci all’interno di un calcolatore: Minimum Spanning Tree: nozioni fondamentali Dato un grafo G(N, E) connesso, pesato e non orientato è sempre possibile trovare il suo MST, cioè quell’albero T(N, E2) (E2 E) per cui la somma di tutti i pesi degli archi appartenenti a E2 è minima Chiaramente se il grafo G è un albero, il suo MST sarà G stesso Se G possiede archi con peso uguale è possibile che esistano più MST per G Minimum Spanning Tree: algoritmi Gli algoritmi più noti sono Algoritmo di Prim Algoritmo di Kruskal Algoritmo di Kruskal L’algoritmo di Kruskal è di tipo “Greedy”. Consiste nell’ordinare tutti gli archi del grafo secondo il loro peso. Inizialmente T è composto da i soli nodi di G. Vanno aggiunti (seguendo l’ordine di peso) uno a uno tutti gli archi che non generano cicli in T. L’operazione più costosa è ordinare gli archi; utilizzando un “buon” algoritmo di ordinamento, l’algoritmo di Kruskal costa O(mlog n) Algoritmo di Prim Grafo G(N, E1) -> grafo Albero T(N, E2) -> MST NB: E2 E1 Albero Prim(Grafo) { considera T formato da un nodo e da nessun arco; while(esistono nodi in T adiacenti a un nodo non in T) { seleziona l’arco di peso minimo che collega un nodo in T con un nodo non in T; aggiungi a T sia l’arco selezionato che il nuovo nodo; } return T; } Algoritmo di Prim Nell’algoritmo di Prim è indispensabile utilizzare una struttura dati che contenga ad ogni passo della computazione tutti gli archi candidati all’inserimento in T, ossia gli archi che connettono un nodo in T con uno non in T Bisogna quindi ad ogni iterazione effettuare degli inserimenti e o delle sostituzioni Algoritmo di Prim: le sostituzioni Ad ogni iterazione, per ogni nodo v non ancora in T, basta tenere traccia del miglior arco e(v) che lo connetta ad un nodo già in T. Di fatto, per ogni nodo v non in T, terremo traccia dell’estremo in T di e(v), (NULL se nessun arco incidente in v ha l’altro estremo in T). = arco con chiave minima = archi presenti nella SD a p T a min q p q T b q<p Algoritmo di Prim: utilizzo degli Heap di Fibonacci Gli Heap di Fibonacci si prestano particolarmente “bene” a svolgere questa funzione mediante l’operazione DECR-CHIAVE Come visto prima questa operazione ha costo (ammortizzato) costante Algoritmo di Prim: utilizzo degli Heap di Fibonacci n-1 Albero Prim(Grafo G) { considera T formato da un nodo scelto a caso; FH.inserisci(tutti gli archi del nodo scelto); while(anew = FH.estraiMin()) { nnew = nodo dell’arco appena estratto che non appartiene a T; T.inserisci(nnew, anew); for each(x congiunto a nnew da un arco y) { if(x non appartiene a T) if(FH contiene arco z che congiunge x a T) 2m FH.decrChiave(z, y); else FH.inserisci(y); } } return T; } Algoritmo di Prim: complessità ammortizzata WHILE: a ogni iterazione viene effettuata una ESTRAI-MIN che come abbiamo visto ha costo ammortizzato pari a O(lg n) FOR EACH: a ogni iterazione viene eseguita una DECR-CHIAVE o una INSERISCI aventi entrambe costo ammortizzato costante TOTALE: il costo totale ammortizzato è quindi O(m + n(lg n)) Algoritmo di Prim: osservazioni finali (1) Il costo dell’algoritmo di Prim con gli Heap di Fibonacci dipende anche dal numero di archi Quindi: Per grafi densi la complessità risulta essere O(n²) Per grafi sparsi la complessità risulta essere O(n(lg n)) Algoritmo di Prim: osservazioni finali (2) ATTENZIONE: per grafi densi risulta essere più efficiente l’utilizzo di strutture dati semplici (es: liste) in quanto la complessità della struttura degli Heap di Fibonacci comporta un’espansione della costante moltiplicativa non rilevabile a causa della notazione asintotica Per capirci: liste → O(an²) Heap di Fibonacci → O(bn²) a«b Algoritmo di Prim: esempio (1) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5 Algoritmo di Prim: esempio (2) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5 Algoritmo di Prim: esempio (3) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5 Algoritmo di Prim: esempio (4) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5 Algoritmo di Prim: esempio (5) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5 Algoritmo di Prim: esempio (6) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5 Algoritmo di Prim: esempio (7) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5 Algoritmo di Prim: esempio (8) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5 Algoritmo di Prim: esempio (8) 2 18 6 6 1 8 13 4 12 10 17 8 15 1 3 14 7 4 3 20 5