Grafi

Proprietà dei grafi

Definizione e proprietà

Un grafo è definito come con

  • insieme di vertici
  • insieme di archi

Se allora il grafo si dice non orientato Se allora il grafo si dice orientato

Cammino

Dato un percorso è un cammino se:

  • (tutti gli elementi devono essere vertici del grafo)
  • (ogni coppia di nodi consecutivi deve essere collegata da un arco)

Se nel cammino abbiamo che allora c’è un ciclo

Cammino semplice

I nodi devono essere tutti diversi, quindi è un cammino senza cicli

Grafo aciclico

Un grafo è detto aciclico quando non ha cicli Se un grafo ha un ciclo allora ne ha infiniti

Grafo pesato

Un grafo è pesato se ad ogni arco diamo un peso una la funzione peso: quindi il costo totale di un eventuale cammino è: la maggior parte dei grafi pesati ha valori positivi negli archi

Ordinamento topologico

Un ordinamento topologico è un ordinamento lineare dei nodi in modo che valga la relazione: Ovviamente se c’è un ciclo questa caratteristica non può essere rispettata Un eventuale ordinamento di questo grafo è:

Componente connessa e fortemente connessa

Componente connessa: dato un grafo diciamo che due vertici sono connessi se esiste un cammino da a

Componente fortemente connessa: dato un grafo orientato , diciamo che due vertici sono fortemente connessi se esiste sia un cammino da a che un cammino da ad .

Rappresentazioni dei grafi

Tramite matrice di adiacenza: Dato un grafo con . La matrice detta matrice di adiacenza del grafo ha dimensione dove è il numero di vertici, ed è formata in questo modo:

  • se i vertici e sono connessi da un arco
  • se i vertici e non sono connessi da un arco Tramite lista di adiacenza: un grafo può essere rappresentato pure con le liste di adiacenza, ovvero un array i cui elementi sono i nodi e per ogni nodo viene associato un altro array con la lista dei nodi collegati ad esso.

BFS

Definizione

La Breadth-First-Search ovvero ricerca in ampiezza è una algoritmo di ricerca nei grafi. Dato un grafo e un vertice distinto detto sorgente, la visita in ampiezza ispeziona sistematicamente gli archi di per “scoprire” tutti i vertici che sono raggiungibili da .

Implementazione

Per tenere traccia del lavoro svolto, la visita in ampiezza colora i vertici di:

  • bianco: nodo non ancora visitato
  • grigio: nodo scoperto ma non ancora visitato
  • nero: nodo visitato

All’inizio tutti i nodi sono bianchi, si sceglie un nodo iniziale (la sorgente ) e da li:

  • Aggiungiamo ad una coda tutti i nodi adiacenti a da (colorandoli di grigio)
  • Per ogni nodo aggiunto alla coda calcoliamo la sua distanza da
BFS(V, s)
	Foreach v in V //for di inizializzazione
		color[v] = White
		d[v] = +infinito
		
	d[s] = 0; // la distanza della sorgente da stessa è 0
	Q = { } // inizializzazione della coda come vuota
	Q.enqueue(s)
	While Q != {} DO
		v = Q.dequeue(Q)
		foreach u in ADJ(v) //scorriamo tutti i nodi adiacenti a v
			if(color[u] == W) 
				d[u] = d[v]+1
				color[u] = Gray
				Q.enqueue(u)
		color[v] = Black

La ricerca oltre a calcolare la distanza di ogni nodo dalla sorgente crea un albero, detto albero BFS dove il cammino che va da ad un generico corrisponde al cammino minimo da a

DFS

Definizione

Un altro algoritmo di ispezione dei grafi è il depth first search, a differenza del BFS dove la ricerca era in ampiezza qui abbiamo una ricerca in profondità, vedremo successivamente che questo algoritmo ci permette di trarre dal nostro grafo diverse informazioni utili come:

  • la composizione delle componenti connesse
  • un ordinamento topologico tutto questo lo facciamo grazie all’albero DFS che crea durante la ricerca. Vedremo diverse implementazioni
Premesse comuni alle implementazioni fatte

I nodi sono colorati in modo diverso in base allo stato in cui si trovano:

  • bianco: nodo non ancora visitato
  • grigio: nodo scoperto ma non visitato
  • nero: nodo visitato Nella nostra implementazione teniamo traccia del tempo di inizio e fine visita di un nodo usando due array e , con e indichiamo rispettivamente il tempo di inizio e fine di ( sarà una variabile che scandirà il tempo di esecuzione)Avremo anche altri due array:
  • : che ci indicherà il colore del nodo v
  • : che ci indicherà il padre di quel nodo nel albero DFS Inoltre useremo una funzione che ritorna una lista dei nodi adiacenti a
Prima implementazione
DFS-visit(v):
	color[v] = gray;
	foreach u in adj(v)
		if color[u] = white
			π[u] = v
			DFS-visit(u)
		color[v] = black

DFS(G,s):
	foreach v in V DO:
		color[v] = white
		π[] = null
DFS-visit(s);

Questa è l’implementazione più grezza, infatti ci potrebbero essere dei nodi non visitati, ad esempio quelli non collegati a nulla. Da queste problematiche nasce la seconda implementazione.

Seconda implementazione

In questa implementazione aggiungiamo un for che scorre la lista dei nodi, in modo che qualsiasi nodo verrà sicuramente visitato, il fatto che ogni nodo può essere una ipotetica sorgente potrebbe creare più alberi DFS ovvero una foresta DFS

DFS-visit(v):
	color[v] = gray;
	foreach u in adj(v)
		if color[u] = white
			π[u] = v
			DFS-visit(u)
		color[v] = black

DFS(G,s):
	foreach v in V DO:
		color[v] = white
		π[] = null
	foreach v in V DO:
		if color[v] = white:
			DFS-visit(s);

Questa implementazione è perfetta, ma non raccoglie nessuna informazione sul nostro grafo, da qui nasce la necessità di introdurre e per scandire il tempo, usati nella terza implementazione

Terza implementazione
DFS-visit(v):
	d[v] = T
	T = T+1
	color[v] = gray;
	foreach u in adj(v)
		if color[u] = white
			π[u] = v
			DFS-visit(u)
		color[v] = black
	F[v] = t
	t = t+1

DFS(G,s):
	foreach v in V DO:
		color[v] = white
		π[] = null
	foreach v in V DO:
		if color[v] = white:
			DFS-visit(s);

Alla fine avremo alla fine di questo algoritmo negli array ed il tempo di inizio e fine, molto utile nell’utilizzo della DFS per scopi terzi. Di seguito un grafo con le informazioni ottenute dopo una DFS:

Etichette degli archi

Nel nostro grafo etichettiamo gli archi in base al ruolo che hanno durante l’esplorazione del grafo, e sono:

  • T archi consecutivi: sono gli archi usati dalla DFS per scoprire nuovi nodi
  • I archi all’indietro: collegano un nodo a un suo antenato nell’albero DFS
  • F archi in avanti: collegano un nodo al suo discendente
  • C archi trasversali: collegano nodi che non hanno relazione antenato-discendente
Per cosa usiamo la DFS

Ordinamento topologico: dato il tempo di inizio e fine visita di ogni vertice del grafo otteniamo un ordinamento topologico valido se mettiamo i vertici in ordine rispetto a di fine visita: Dai tempi di fine visita otteniamo un ordinamento topologico: Identificazione delle componenti connesse: per identificare le componenti connesse di un grafo facciamo uso della DFS nel seguente modo:

  1. Faccio una DFS e metto in ordine rispetto al tempo di inizio visita
  2. Creo il grafo trasposto di quello dato in input (inverto le direzioni di tutti gli archi)
  3. Faccio la DFS sul grafo trasposto usando nel foreach principale la lista di nodi definita nel punto 1 Otteniamo una foresta di alberi DFS, tanti alberi quanti sono le componenti connesse. Di seguito un esempio:

Ricerca dei cammini minimi

Introduzione

Definizioni

In un problema dei cammini minimi sono dati:

  • un grafo orientato pesato
  • funzione peso che associa agli archi dei pesi Il peso di un cammino è la somma dei pesi degli archi che lo compongono: il peso di un cammino minimo da a è definito in questo modo:

Un cammino minimo è definito come un cammino qualsiasi con peso

Varianti di questo problema

Esistono 4 varianti del problema dei cammini minimi:

  1. Problema dei cammini minimi da sorgente unica(single source): dato un grafo vogliamo trovare un cammino minimo che va da un dato vertice sorgente a ciascun vertice
  2. Problema dei cammini minimi con destinazione unica(single destination): trovare un cammino minimo da ciascun vertice a un dato vertice destinazione. (Invertendo la direzione di ciascun argo nel grafo lo possiamo ricondurre la primo caso)
  3. Problema del cammino minimo per una coppia di vertici(single pair): trovare un cammino minimo da a , e una variante del primo problema.
  4. Problema dei cammini minimi fra tutte le coppie di vertici(all-pairs): trovare un cammino minimo da a per ogni coppia di vertici. Di seguito affronteremo il primo e il quarto.

Proprietà

Sottostruttura ottima

Teorema: Dati

  • un grafo orientato
  • la funzione peso
  • sia un cammino minimo dal vertice al per qualsiasi e tali che sia un sotto-cammino di dal vertice al vertice allora è un cammino minimo da a Dimostrazione: Se scomponiamo il cammino in abbiamo . Supponiamo adesso che ci sia un cammino da a con peso . Allora è un cammino da a il cui peso è minore di che contraddice l’ipotesi che sia un cammino minimo da a
Archi di peso negativo e cicli

All’interno del nostro grafo potrebbero essere contenuti dei cammini di peso negativo, o anche dei cicli:

  • Quando non sono presenti dei cicli di peso complessivo negativo raggiungibili è sempre possibile definire
  • Quando sono presenti dei cicli di peso complessivo negativo non sarà possibile individuare di in quanto sarà sempre possibile diminuirlo effettuando un numero indefinito di passi all’interno del ciclo. In tal caso diremo che
Rilassamento dei cammini minimi

Il processo di rilassamento di un arco consiste nel verificare se passando per , è possibile migliorare il cammino minimo per da precedentemente trovato. Nelle nostre implementazioni avremo un array dove è peso del cammino da ad , con ogni passo di rilassamento possiamo ridurre questo valore.

RELAX(u,v,w):
	if(d[v]>d[u]+w(u,v))
		d[v] = d[u]+w(u,v)

Alla creazione del nostro grafo tutti i valori del nostro array vengono inizializzati a .

Proprietà dei cammini minimi
  1. Disuguaglianza triangolare Per qualsiasi arco , si ha .
  2. Proprietà del limite superiore Per tutti i vertici , si ha sempre e, una volta che il limite superiore assume il valore , esso non cambia più.
  3. Proprietà dell’assenza di un cammino Se non c’è un cammino da a , allora si ha sempre .
  4. Proprietà della convergenza Se è un cammino minimo in per qualche e se in un istante qualsiasi prima del rilassamento dell’arco , allora in tutti gli istanti successivi.
  5. Proprietà del rilassamento del cammino Se è un cammino minimo da a e gli archi di vengono rilassati nell’ordine , allora . Questa proprietà è soddisfatta indipendentemente da altri passi di rilassamento effettuati.
  6. Proprietà del sotto grafo dei predecessori Una volta che per ogni , il sotto grafo dei predecessori è un albero di cammini minimi radicato in .

Single-Source Shortest Path

Generic SSSP

Di seguito vediamo il primo algoritmo per la ricerca di un cammino minimo ovvero il Single-Source Shortest Path che risolve il primo problema dei cammini minimi.

Generic-SSSP(G,s)
	d = new array(len(V))
	foreach v in V do
		d[v] = +infinito
	d[s] = 0
	while esiste (u, v) in E tale che d[u] + w(u, v) < d[v]:
	    RELAX(u, v)
	return d
SSSP in un grafo orientato aciclico (DAG)

Rilassando gli archi di un dag (Directed Acyclic Graph) pesato secondo un ordine topologico dei suoi vertici è possibile calcolare i cammini minimi da una sorgente unica nel tempo . L’algoritmo inizia ordinando topologicamente il dag, se esiste un cammino dal vertice al vertice , allora precede nell’ordine topologico. Effettuiamo un passaggio sui vertici secondo l’ordine topologico. Durante l’elaborazione vengono rilassati tutti gli archi che escono dal vertice

DAG-SHORTEST-PATHS(G, w, s)
	U = getTopologicalOrder(G);
	foreach v in U:
	  foreach v in G.Adj[u]
		  RELAX(u, v, w)