Sei sulla pagina 1di 18

Progetto Totale 2

Donici Ionut Bogdan


Matricola: 109585

Algoritmi e Strutture Dati


Laboratorio
Obbiettivi del progetto:

• Realizzare in Java una implementazione di un grafo generico non orientato con


liste di adiacenza e una implementazione di un grafo generico orientato con
matrice di adiacenza.
• Realizzare in Java un’implementazione di una coda con priorità implementata
tramite uno heap binario.
• Per il problema dei cammini minimi con sorgente singola: realizzare in Java
una implementazione dell’algoritmo di Dijkstra che usi la coda di priorità
implementata nel punto precedente e realizzare in Java una implementazione
dell’algoritmo di Bellman-Ford.
• Per il problema dei cammini minimi tra tutte le coppie di nodi: realizzare in
Java una implementazione dell’algoritmo di Floyd-Warshall.
• Per il problema del calcolo di un albero minimo di copertura: realizzare in Java
una implementazione dell’algoritmo di Kruskal e realizzare in Java una
implementazione dell’algoritmo di Prim.
• Realizzare una suite di test JUnit 5 che controlli le funzionalità implementate.
Grafo generico non orientato

Per la rappresentazione di un grafo non orientato utilizzeremmo le liste di adiacenza.


Queste consistono in una Lista adjacentLists di V liste, una per ogni nodo
appartenente all’insieme dei nodi del grafo.
Per ogni nodo u, la lista di adiacenza adjacentLists[u] contiene tutti i vertici v tali che
esista un arco (u,v) appartenente al grafo.

protected final Map<GraphNode<L>, Set<GraphEdge<L>>> adjacentLists;

Il punto saliente di questa implementazione è stato l’aggiunta e la rimozione degli


archi. Questo perché essendo un grafo non orientato, se abbiamo un arco (u,v)
questo non va aggiunto solamente nella lista di adiacenza del nodo u, ma anche nella
lista del nodo v. Ecco l’implementazione proposta per l’aggiunta di un arco al grafo
escludendo la gestione degli errori:

// Se l'arco è già contenuto nel grafo


if (containsEdge(edge)) {
return false;
} else {
// Altrimenti aggiungo l'arco su ogni node
for (Map.Entry<GraphNode<L>, Set<GraphEdge<L>>> entry : adjacentLists.entrySet()) {
if (entry.getKey().equals(edge.getNode1())) {
entry.getValue().add(edge);
}
if (entry.getKey().equals(edge.getNode2())) {
entry.getValue().add(edge);
}
}
return true;
}

Qui invece l’implementazione proposta per la rimozione di un arco dal grafo:

// L'arco è completamente rimosso quando il numero di cancellazioni è pari a 2


int del = 0;

for (Map.Entry<GraphNode<L>, Set<GraphEdge<L>>> entry : adjacentLists.entrySet()) {


if (entry.getValue().contains(edge)) {
del++;
entry.getValue().remove(edge);
}
}

return del == 2;

Ho usato una sorta di flag (del) per essere sicuro che il numero totale degli archi
rimossi siano 2, in quanto riferito a ciò che prima ho spiegato sulla loro aggiunta.
Per quanto riguarda i test invece, ho usato come riferimento il seguente grafo:

Grafo non orientato utilizzato per i test

La sua rappresentazione in una lista di adiacenza e all’interno di una matrice ha la


seguente struttura:

Rappresentazione di un grafo non orientato

Rappresentazione del grafo all’interno dei test:

MapAdjacentListUndirectedGraph<String> list = new MapAdjacentListUndirectedGraph<>();


assertEquals(0, list.edgeCount());

GraphNode<String> nodeA = new GraphNode<>("A"); list.addNode(nodeA);


GraphNode<String> nodeB = new GraphNode<>("B"); list.addNode(nodeB);
GraphNode<String> nodeC = new GraphNode<>("C"); list.addNode(nodeC);
GraphNode<String> nodeD = new GraphNode<>("D"); list.addNode(nodeD);
GraphNode<String> nodeE = new GraphNode<>("E"); list.addNode(nodeE);
GraphNode<String> nodeF = new GraphNode<>("F"); list.addNode(nodeF);

GraphEdge<String> edgeAB = new GraphEdge<>(nodeA, nodeB, false, 1);list.addEdge(edgeAB);


GraphEdge<String> edgeAC = new GraphEdge<>(nodeA, nodeC, false, 1);list.addEdge(edgeAC);
GraphEdge<String> edgeBC = new GraphEdge<>(nodeB, nodeC, false, 1);list.addEdge(edgeBC);
GraphEdge<String> edgeBD = new GraphEdge<>(nodeB, nodeD, false, 1);list.addEdge(edgeBD);
GraphEdge<String> edgeCD = new GraphEdge<>(nodeC, nodeD, false, 1);list.addEdge(edgeCD);
GraphEdge<String> edgeCE = new GraphEdge<>(nodeC, nodeE, false, 1);list.addEdge(edgeCE);
GraphEdge<String> edgeDE = new GraphEdge<>(nodeD, nodeE, false, 1);list.addEdge(edgeDE);
GraphEdge<String> edgeDF = new GraphEdge<>(nodeD, nodeF, false, 1);list.addEdge(edgeDF);
GraphEdge<String> edgeEF = new GraphEdge<>(nodeE, nodeF, false, 1);list.addEdge(edgeEF);
L’organizzazione che ho pensato per i test è stata la seguente (vale per tutte le classi
di test di questo progetto):
• Check delle Exceptions: cerco di lanciare tutte le eccezioni possibili che sono
previste per un metodo e i suoi parametri;
• Simulazione: effettuo dei test che siano privi di possibili eccezioni e faccio un
controllo sui risultati ottenuti e mi assicuro che siano quelli che mi aspetto;
• UnsupportedOperationException: in caso di questa eccezione l’unica cosa che
ho fatto è stata disabilitare il test per il metodo non supportato, attraverso il
tag @Disabled della suite JUnit 5:

/*
* Essendo un'operazione non supportata non ne eseguo il test
*/
@Disabled
final void testGetNodeAtIndex() {
fail("Not yet implemented");
}

Esempio di test eseguito per vedere se effettivamente i nodi adiacenti di un nodo


siano effettivamente quelli che mi aspetto:

Set<GraphNode<String>> toCheckA = new HashSet<>();


toCheckA.add(nodeB);
toCheckA.add(nodeC);
Set<GraphNode<String>> toCheckB = new HashSet<>();
toCheckB.add(nodeA);
toCheckB.add(nodeC);
toCheckB.add(nodeD);
Set<GraphNode<String>> toCheckC = new HashSet<>();
toCheckC.add(nodeA);
toCheckC.add(nodeB);
toCheckC.add(nodeD);
toCheckC.add(nodeE);
Set<GraphNode<String>> toCheckD = new HashSet<>();
toCheckD.add(nodeB);
toCheckD.add(nodeC);
toCheckD.add(nodeE);
toCheckD.add(nodeF);
Set<GraphNode<String>> toCheckE = new HashSet<>();
toCheckE.add(nodeC);
toCheckE.add(nodeD);
toCheckE.add(nodeF);
Set<GraphNode<String>> toCheckF = new HashSet<>();
toCheckF.add(nodeD);
toCheckF.add(nodeE);
assertEquals(toCheckA, list.getAdjacentNodesOf(nodeA));
assertEquals(toCheckB, list.getAdjacentNodesOf(nodeB));
assertEquals(toCheckC, list.getAdjacentNodesOf(nodeC));
assertEquals(toCheckD, list.getAdjacentNodesOf(nodeD));
assertEquals(toCheckE, list.getAdjacentNodesOf(nodeE));
assertEquals(toCheckF, list.getAdjacentNodesOf(nodeF));
Grafo generico orientato

Un grafo è detto orientato se ogni singolo arco è individuato da una coppia ordinata
di nodi. Dato un grafo orientato ed un arco w(u, v) appartenente ad esso si può dire
che:
• u è l’origine e v la destinazione dell’arco w;
• u e v sono nodi adiacenti;
• u è il predecessore di v e v è il successore di u;
• w è uscente da u ed entrante in v.

Gli archi di un grafo orientato vengono rappresentati evidenziando un orientamento


dai nodi origine ai nodi destinazione. La seguente figura illustra la struttura di un
grafo orientato nonché il grafo usato per i test, accanto anche la sua
rappresentazione in una matrice di adiacenza:

Rappresentazione di un grafo orientato

In questo progetto per poter rappresentare i grafi orientati infatti, utilizzeremo le


matrici di adiacenza. La rappresentazione con matrice di adiacenza di un grafo
consiste in una matrice di dimensioni V x V (V sta per l’insieme dei nodi) tale che: se i
nodi u e v formano un arco, all’interno della cella (u, v) andà inserito l’arco stesso,
altrimenti un null. Inoltre utilizzeremo anche una Map in modo che per ogni nodo sia
registrato il suo indice all’interno della matrice.

// Insieme dei nodi e associazione di ogni nodo con il proprio indice nella
// matrice di adiacenza
protected Map<GraphNode<L>, Integer> nodesIndex;

// Matrice di adiacenza, gli elementi sono null o oggetti della classe


// GraphEdge<L>. L'uso di ArrayList permette alla matrice di aumentare di
// dimensione gradualmente ad ogni inserimento di un nuovo nodo.
protected ArrayList<ArrayList<GraphEdge<L>>> matrix;
Di questa implementazione l’aggiunta dei nodi è stata particolarmente impegnativa in
quanto, un nodo per poter essere aggiunto all’interno di una matrice prevede che
quest’ultima venga incrementata di una colonna e una riga e le nuove eventuali celle
siano tutte riempite con un valore null e non siano lasciate vuote. Inoltre questo, oltre
che a dover essere aggiunto all’interno della matrice, va inserito anche nella Map dei
nodi ed a questo deve essere associato un valore che rappresenta la sua posizione
nella matrice.

@Override
public boolean addNode(GraphNode<L> node) {
/*
* Per aggiungere un nodo al grafo, mi devo innanzitutto accertare che
* questo non sia già presente all'interno dell'insieme indexNodes.
* Se questo non è presente, procedo con l'inserimento del nodo
* nell'insieme indexNodes. Una volta nell'insieme devo aggiornare
* la grandezza della matrice, incrementando le righe e le colonne.
* I valori dei nuovi campi sono impostati a null
*/

// Controllo che il parametro non sia null


if (node == null) {
throw new NullPointerException("Parametro null!");
}
// Controllo che il nodo non sia già stato inserito
if (nodesIndex.containsKey(node)) {
return false;
}
/*
* Comando base per l'inserimento del nodo nell'insieme dei nodi
* nodesIndex. node rappresenta la chiave, matrix.size() rappresenta
* invece il valore associato al nodo nella matrice
*/
nodesIndex.put(node, matrix.size());
// Incremento della matrice
matrix.add(new ArrayList<>());
for (int i = 0; i < matrix.size(); i++) {
for (int j = matrix.size() - 1; j < matrix.size(); j++) {
matrix.get(i).add(null);
// Se arrivo all'ultima riga della matrice
if (i == matrix.size() - 1) {
for (int k = 1; k < matrix.size(); k++) {
matrix.get(i).add(null);
}
}
}
}
return true;
}
Coda di priorità

Una coda di priorità è una struttura dati che serve a mantenere in ordine un insieme
S di elementi, ciascuno con un valore associato detto chiave. Una coda di min-
priorità supporta le seguenti operazioni:

• insert(x): inserisce l’elemento x nell’insieme S.


public void insert(PriorityQueueElement element) {
// In caso di parametro null
if (element == null) {
throw new NullPointerException("Parametro null!");
}
heap.add(element);
minHeap();
}

• minimum(): restituisce l’elemento di S con la chiave più piccola.


public PriorityQueueElement minimum() {
// Heap vuoto
if (heap.size() == 0) {
throw new NoSuchElementException("Heap Vuoto!");
}

return heap.get(0);
}

• extractMinimum(): rimuove e restituisce l’elemento di S con la chiave più


piccola.
public PriorityQueueElement extractMinimum() {
// Heap Vuoto
if (heap.size() == 0) {
throw new NoSuchElementException("Heap Vuoto!");
}

PriorityQueueElement toReturn = heap.remove(0);


// Prima di ritornare rifaccio l'operazione di minHeap
minHeap();
return toReturn;
}

• decreasePriority(x, k): diminuisce il valore della chiave dell’elemento x al


nuovo valore k.
public void decreasePriority(PriorityQueueElement element, double newPriority) {
if (!heap.contains(element)) {
throw new NoSuchElementException("Elemeno non presente nell'heap!");
}
if (heap.get(heap.indexOf(element)).getPriority() <= newPriority) {
throw new IllegalArgumentException("Nuova priorità più grande o uguale a
quella attuale!");
}
// Aggiorno il nuovo valore e rifaccio il minHeap
heap.get(heap.indexOf(element)).setPriority(newPriority);
minHeap();
}
Il metodo per effettuare l’ordinamento all’interno di questa struttura è heapSort(),
implementato all’interno delle classi private aggiunte per scopi di implementazione:

// Processo per riordinare gli elementi all'interno dell'albero


private void minHeap() {
for (int i = heap.size() / 2 - 1; i >= 0; i--) {
heapify(i);
}
}
// Restituisce l'indice del figlio sinistro
private int leftChild(int i) {
return (i * 2) + 1;
}
// Restituisce l'indice del figlio destro
private int rightChild(int i) {
return (i * 2) + 2;
}
// Implementazione di heapSort
private void heapify(int i) {
int left = leftChild(i);
int right = rightChild(i);
PriorityQueueElement min = heap.get(i);
if (heap.size() > left) {
if (heap.get(left).getPriority() < min.getPriority()) {
min = heap.get(left);
}
}
if (heap.size() > right) {
if (heap.get(right).getPriority() < min.getPriority()) {
min = heap.get(right);
}
}
if (heap.get(i).getPriority() != min.getPriority()) {
PriorityQueueElement tmp = heap.get(i);
int lastIndexOf = heap.lastIndexOf(min);
heap.set(i, min);
heap.set(lastIndexOf, tmp);
heapify(heap.indexOf(min));
}
}
Algoritmo di Dijkstra

L’algoritmo di Dijkstra risolve il problema dei cammini minimi da sorgente unica in un


grafo orientato pesato, nel caso in cui tutti i pesi degli archi non siano negativi.
L’algoritmo mantiene un insieme S di nodi i cui pesi finali dei cammini minimi dalla
sorgente s sono stati già determinati. L’algoritmo seleziona ripetutamente il nodo u
appartenente all’insieme dei nodi del grafo – S con la stima minima del cammino
minimo, aggiunge u a S e rilassa tutti gli archi che escono da u. Manteniamo anche
una coda di min-priorità Q di nodi, utilizzando come valore per le loro chiavi la
floatingDistance.

Algoritmo di Dijkstra in azione

Il calcolo del percorso minimo viene effettuato attraverso questa implementazione


dell’algoritmo di Dijkstra:

@Override
public void computeShortestPathsFrom(GraphNode<L> sourceNode) {
// In caso di parametro null
if(sourceNode == null){
throw new NullPointerException("Parametro null!");
}
// Non non appartenente al grafo
if(!graph.containsNode(sourceNode)){
throw new IllegalArgumentException("Il nodo non esiste nel grafo");
}
// IllegalStateException non necessario

// Inizializzo i nodi
initSingleSource(sourceNode);
// Creazione queue dopo che ho il sourceNode
createQueue();
while(!queue.isEmpty()){
GraphNode<L> node = (GraphNode<L>) queue.extractMinimum();
computedNodes.add(node);
for(GraphNode<L> adjNode : graph.getAdjacentNodesOf(node)){
relax(graph.getEdge(node, adjNode));
}
}

this.computed = true;
this.lastSource = sourceNode;
}
Le classi aggiuntive utili all’implementazione sono le seguenti:
• initSingleSource(GraphNode<L> sourceNode): inizializza le stime dei cammini
minimi e i predecessori;
private void initSingleSource(GraphNode<L> s) {
if (s == null) {
throw new NullPointerException("Parametro null!");
}
for (GraphNode<L> node : graph.getNodes()) {
if (!node.equals(s)) {
node.setFloatingPointDistance(Double.MAX_VALUE);
node.setPrevious(null);
} else {
node.setFloatingPointDistance(0);
}
}
}
• createQueue(): metodo che crea la coda di priorità per l’algoritmo;
private void createQueue(){
for(GraphNode<L> node : graph.getNodes()){
queue.insert(node);
}
}
• relax(GraphEdge<L> toRelax): metodo di rilassamento di un’arco
private void relax(GraphEdge<L> toRelax) {
if (toRelax == null) {
throw new NullPointerException("Parametro null!");
}
if (toRelax.getNode2().getFloatingPointDistance() >
toRelax.getNode1().getFloatingPointDistance() + toRelax.getWeight()) {

toRelax.getNode2().setFloatingPointDistance(toRelax.getNode1().getFloatingPointDis
tance() + toRelax.getWeight());
toRelax.getNode2().setPrevious(toRelax.getNode1());
}
}

Per quanto riguarda il metodo che riesce a tirarmi fuori il percorso di archi
attraversato dall’algoritmo per raggiungere un certo nodo, il meccanismo è
particolare. Infatti sono dovuto partire dal nodo di arrivo ed andare a ritroso fino al
nodo di partenza attraverso il metodo getPrevious() dei nodi, aggiungere ogni arco
attraversato in una List ed una volta arrivato al nodo di partenza fare il reverse() della
lista in modo da avere il percorso in ordine.

List<GraphEdge<L>> toReturn = new ArrayList<>();


// Se il nodo in riferimento ha un nodo precedente
if (targetNode.getPrevious() != null) {
// Finché ha un precedente
while (targetNode.getPrevious() != null) {
// Aggiungo l'arco che lo collega al precedente
toReturn.add(graph.getEdge(targetNode.getPrevious(), targetNode));
// Il targetNode diventa il suo previous e continuo finché non ne ha più
targetNode = targetNode.getPrevious();
}
}
/* Essendo che sono partito dal target e sono andato indietro fino al nodo di
partenza,
* faccio il reverse dell'array in modo da avere gli archi in ordine */
Collections.reverse(toReturn);
return toReturn;
Algoritmo di Bellman-Ford

L’algoritmo di Bellman-Ford risolve il problema dei cammini minimi da sorgente unica


nel caso generale in cui, a differenza di Dijkstra, i pesi degli archi possono essere
negativi. Avendo archi con pesi negativi però, si rischia di cadere in un loop negativo,
ovvero due nodi che entrano uno nell’altro con archi negativi. Esempio di loop
negativo:

Esempio di loop negativo

Se un tale ciclo esiste all’interno del grafo, il problema non ha soluzione (si farebbe
l’operazione -4-6 fino all’infinito), altrimenti l’algoritmo restituisce i cammini minimi
ed i loro pesi.

Algoritmo di Bellman-Ford in azione

L’algoritmo, come Dijkstra, usa il rilassamento, riducendo progressivamente il valore


stimato v.floatingDistance per il peso di un cammino minimo dalla sorgente s a
ciascun vertice v appartenenete all’insieme dei nodi del grafo, fino a raggiungere il
peso effettivo di un cammino minimo.
initSingleSource(sourceNode);
for (GraphNode<L> node : graph.getNodes()) {
for (GraphEdge<L> edge : graph.getEdges()) {
relax(edge);
}
}
// Controllo che non ci siano cicli negativi
for (GraphEdge<L> edge : graph.getEdges()) {
if (edge.getNode2().getFloatingPointDistance() >
edge.getNode1().getFloatingPointDistance() + edge.getWeight()) {
throw new IllegalStateException("Presenza ciclo negativo");
}
}
Rispetto all’algoritmo di Dijkstra, quello di Bellman-Ford non cambia molto, infatti i
metodi di rilassamento e inizializzazione dei camini minimi sono uguali.
Algoritmo di Floyd-Warshall
Risolve il problema dei cammini minimi tra tutte le coppie di nodi. Possono essere
presenti archi di peso negativo, ma si suppone che non ci siano loop negativi.
L’algoritmo considera i nodi intermedi di un cammino minimo, dove un nodo
intermedio di un cammino semplice p = <v1, …, vl> è un nodo qualsiasi di p diverso
da v1 e vl.

A sinistra la matrice delle distanze tra tutte le coppie di nodi, a destra la matrice dei predecessori, che serve per
estraree il percorso da un nodo sorgente ad un nodo destinazione. Questo, è anche il grafo che ho utilizzato per
la realizzazione dei test.

L’algoritmo, nella fase di inizializzazione crea due matrici V x V. La prima matrice


misura i costi minimi tra due nodi (u,v) in ogni percorso. La seconda indica i
predecessori di ciascun nodo in ogni percorso possibile.

private double[][] costMatrix;


private int[][] predecessorMatrix;
L’implementazione proposta dell’algoritmo Floyd-Warshall:

public void computeShortestPaths() {


// Implementazione Floyd-Warshall
for (int k = 0; k < costMatrix.length; k++) {
for (int i = 0; i < costMatrix.length; i++) {
for (int j = 0; j < costMatrix.length; j++) {
// Se la distanza attuale è maggiore rispetto alla nuova
if (costMatrix[i][j] > costMatrix[i][k] + costMatrix[k][j]) {
// Aggiorno il valore della distanza
costMatrix[i][j] = costMatrix[i][k] + costMatrix[k][j];
// Aggiorno anche l'indice del nodo precedente
predecessorMatrix[i][j] = predecessorMatrix[k][j];
}
// In caso di ciclo negativo
if (costMatrix[k][k] < 0) {
throw new IllegalStateException("Ciclo negativo!");
}
}
}
}

computed = true;
}

Cosa più complicata invece, è stata la realizzazione del metodo che restituisce il
percorso di archi da un nodo partenza ad un nodo destinazione:

if(sourceNode==targetNode){
return null;
}
// Indice nodo partenza
int i = graph.getNodeIndexOf(sourceNode.getLabel());
// Indice nodo destinazione
int j = graph.getNodeIndexOf(targetNode.getLabel());
// Indice del nodo precedente
int target = predecessorMatrix[i][j];
// Insieme degli archi che formano il percorso
List<GraphEdge<L>> toReturn = new ArrayList<>();
// Il previous del targetNode
GraphNode<L> toCheck = graph.getNodeAtIndex(target);
// Finché non arrivo al nodo di partenza
while(!toCheck.equals(sourceNode)){
// Aggiungo l'arco all'insieme degli archi
toReturn.add(graph.getEdge(toCheck, targetNode));
// Aggiorno l'indice del nodo precedente
target = predecessorMatrix[i][graph.getNodeIndexOf(toCheck.getLabel())];
// Scorro il nodo, andando indietro
targetNode = toCheck;
// Aggiorno con il nuovo previous
toCheck = graph.getNodeAtIndex(target);
}
// Aggiungo l'ultimo arco che collega il sourceNode al nodo successivo
toReturn.add(graph.getEdge(sourceNode, targetNode));
// Faccio il reverse, in modo da avere l'array di archi in ordine
Collections.reverse(toReturn);
return toReturn;
Algoritmo di Kruskal
Nell’algoritmo di Kruskal, l’insieme A è una foresta i cui vertici sono tutti quelli del
grafo. L’arco sicuro aggiunto ad A è sempre un arco di peso minimo nel grafo che
collega due componenti distinte. L’algoritmo trova un arco sicuro da aggiungere alla
foresta in costruzione scegliendo, fra tutti gli archi che collegano due alberi qualsiasi
nella foresta, un arco (u,v) di peso minimo. Kruskal usa una struttura dati per insiemi
disgiunti per mantenere vari insiemi disgiunti di elementi. Ogni insieme contiene i
vertici di un albero della foresta corrente.

Esempio del funzionamento dell’algoritmo di Kruskal

La seguente implementazione prevede l’uso di 3 metodi privati che ci aiutano a


creare, gestire e fare operazioni sull’insieme A.

public Set<GraphEdge<L>> computeMSP(Graph<L> g) {


if (g == null) {
throw new NullPointerException("Parametro null!");
}
if (g.isDirected()) {
throw new IllegalArgumentException("Il grafo è orientato");
}
for (GraphEdge<L> edge : g.getEdges()) {
if ((!edge.hasWeight()) || (edge.getWeight() < 0)) {
throw new IllegalArgumentException("Arco non pesato o negativo!");
}
}
// Insieme degli archi che completano l'algoritmo
Set<GraphEdge<L>> toReturn = new HashSet<>();
// Creo gli insiemi disgiunti all'interno di disjointSets
for (GraphNode<L> node : g.getNodes()) {
makeSet(node);
}
// Ordino gli edges
ArrayList<GraphEdge<L>> tmp = new ArrayList<>(g.getEdges());
ArrayList<GraphEdge<L>> orderedEdges = new ArrayList<>((orderEdges(tmp)));
// Per ogni edge
for(GraphEdge<L> edge : orderedEdges){
// Se i vertici non fanno parte dello stesso Insieme
if(findSet(edge.getNode1())!=findSet(edge.getNode2())){
// L'arco viene aggiunto all'insieme degli archi che effettuano il Kruskal
toReturn.add(edge);
// Unisco i due insiemei degli nodi in un'unico insieme
union(edge.getNode1(), edge.getNode2());
}
}
return toReturn;
}
L’implementazione dei metodi privati:

private void makeSet(GraphNode<L> node) {


disjointSets.add(new HashSet<>());
disjointSets.get(disjointSets.size() - 1).add(node);
}

// Restituisce l'indice dell'insieme che contiene node


private int findSet(GraphNode<L> node) {
Set<GraphNode<L>> no1 = new HashSet<>();
no1.add(node);
return disjointSets.indexOf(no1);
}

// Ordinamento dell'array di archi


private ArrayList<GraphEdge<L>> orderEdges(ArrayList<GraphEdge<L>> edges) {
for (int i = 0; i < edges.size(); i++) {
for (int j = 0; j < edges.size() - 1 - i; j++) {
if (edges.get(j).getWeight() > edges.get(j + 1).getWeight()) {
GraphEdge<L> temp = edges.get(j);
edges.set(j, edges.get(j + 1));
edges.set(j + 1, temp);
}
}
}
return edges;
}

// Unisce gli insiemi dinamici che contengono node1 e node2


private void union(GraphNode<L> node1, GraphNode<L> node2){
Set<GraphNode<L>> no2 = new HashSet<>();
no2.add(node2);

disjointSets.get(findSet(node1)+1).add(node2);
disjointSets.remove(no2);
}
Algoritmo di Prim

L’algoritmo di Prim opera in modo molto simile all’algoritmo di Dijkstra per trovare i
cammini minimi di un grafo. Prim ha la proprietà che gli archi nell’insieme A formano
sempre un albero singolo. L’albero inizia da un arbitrario vertice radice r e si sviluppa
fino a coprire tutti i vertici in V. Ad ogni passo viene aggiunto all’albero A un arco
leggero che collega A con un vertice isolato – un vertice che non sia estremo di
qualche arco in A. Questa regola aggiunge soltanto archi che sono sicuri per A,
quando l’algoritmo termina, gli archi in A formano un albero di connessione minimo.
Questa strategia è golosa perché l’albero cresce includendo a ogni passo un arco
che contribuisce con la quantità più piccola possibile a formare il peso dell’albero.

Esempio del funzionamento dell’algoritmo di Prim

Implementazione proposta:

public void computeMSP(Graph<L> g, GraphNode<L> s) {


// In caso di parametro null
if ((g == null) || (s == null)) {
throw new NullPointerException("Parametri null!");
}
// Il nodo s non esiste in g
if (!g.containsNode(s)) {
throw new IllegalArgumentException("Non non esistente!");
}
// Se g è orientato
if (g.isDirected()) {
throw new IllegalArgumentException("Grafo orientato!");
}
// Se g non è pesato o ha pesi negativi
Set<GraphEdge<L>> toCheckWeight = new HashSet<>(g.getEdges());
for (GraphEdge<L> edge : toCheckWeight) {
if ((!edge.hasWeight()) || (edge.getWeight() < 0)) {
throw new IllegalArgumentException("Arco non pesato o negativo!");
}
}
// Impostazione di tutti i nodi a WHITE tranne s
for (GraphNode<L> node : g.getNodes()) {
if (!node.equals(s)) {
node.setColor(0);
node.setFloatingPointDistance(Double.MAX_VALUE);
node.setPrevious(null);
}
}

// Inizializzazione del nodo passato


s.setColor(1);
s.setFloatingPointDistance(0);
s.setPrevious(null);
// Aggiungo il nodo di partenza alla queue
Enqueue(s);

// Finché non è vuota la coda


while (!queue.isEmpty()) {
// Estrago il nodo con peso minimo
GraphNode<L> previous = Dequeue();
// Per ogni suo adiacente
for (GraphNode<L> node : g.getAdjacentNodesOf(previous)) {
// Se è un nodo di quale sappiamo già la sua esistenza
if(node.getColor() == 1){
// Se il suo peso attuale è maggiore rispetto a quello nuovo
if(node.getPriority()>g.getEdge(previous, node).getWeight()){
// Aggiorno la sua priorità
node.setPriority(g.getEdge(previous, node).getWeight());
// Reimposto il suo precedente
node.setPrevious(previous);
// Aggiorno i dati all'interno dello heap
try{
queue.decreasePriority(node, node.getPriority());
}catch(IllegalArgumentException ignored){ }
}
}

// Se non è stato visitato


if (node.getColor() == 0) {
node.setColor(1);
node.setPriority(g.getEdge(previous, node).getWeight());
node.setPrevious(previous);
// Lo aggiungo alla queue
Enqueue(node);
}
}
// Una volta finito con i suoi nodi adiacenti lo rendo nero
previous.setColor(2);
}
}

Ci sono due metodi extra implementati per lavorare con la coda di priorità, non erano
necessari ma li ho aggiunti per cercare di tenere un’implementazione quanto più
modulare possibile.