Visite di Grafi
Molte informazioni interessanti relative a elementi interconnessi possono essere ottenute visitando il corrispondente grafo. La visita di un grafo è semplicemente l'esplorazione dei nodi del grafo partendo da un certo nodo e seguendo gli archi per passare da un nodo ad un altro. Bisogna solamente prendere le dovute accortezze per evitare di ritornare su nodi già visitati. I file e mosuli utili per questa lezione sono contenuti nel seguente pacchetto
Una delle proprietà più semplici che una visita permette di verificare è la connessione del grafo, cioè, se partendo da un nodo tutti gli altri nodi del grafo sono raggiungibili, seguendo gli archi. Ad esempio in un grafo relativo ad una rete sociale, la connessione significa che una persona e collegata ad una qualsiasi altra seguendo gli amici, gli amici degli amici, gli amici degli amici degli amici e così via. Se un grafo non è connesso sarà formato da due o più sottografi che sono connessi.
Un'altra proprietà che può essere facilmente calcolata con una visita sono le distanze tra i nodi. La distanza tra due nodi è il numero minimo di archi che bisogna attraversare per raggiungere uno dei nodi partendo dall'altro. C'è una teoria denominata sei gradi di separazione che dice che ogni persona è collegata a una qualsiasi altra con una catena di conoscenze di lunghezza al più sei.
Visita
La visita che considereremo è la visita in ampiezza (breadth first search).
Nella lezione sui labirinti abbiamo già incontrato la visita di un grafo. Si trattava di un grafo particolare in cui i nodi erano gli incroci del labirinto e gli archi i corridoi che collegano gli incroci. La visita di un grafo generale si basa sugli stessi principi. Si parte da un nodo si visitano i sui vicini (in altro nome per i nodi adiacenti), poi per ognuno di questi si fa la stessa cosa e così via finché non ci sono più altri nodi visitabili. Ovviamente bisogna evitare di scambiare nodi già visitati per nodi nuovi. Per questo basterà salvare in un insieme, che chiameremo visited
, i nodi già visitati. Inoltre, ad ogni passo della visita dovremmo sapere quali sono i nodi tra quelli in visited
che sono ancora utili per visitare nuovi nodi. Questo insieme di nodi lo chiameremo active
. Quando un nodo in active
ha un vicino che non è già stato visitato, quet'ultimo dovrà essere salvato in un altro insieme che chiameremo newactive
. Quando tutti i nodi in active
saranno stati considerati, l'insieme newactive
prenderà il ruolo di active
e la visita continuerà in modo analogo. Per rappresentare i grafi useremo i tipi Node
e Graph
che abbiamo definito nella scorsa lezione. Vediamo quindi una implementazione della visita in ampiezza:
def visit(graph, node):
node = graph.get_node(node)
visited = set([node])
active = set([node])
while len(active) > 0:
newactive = set()
while len(active) > 0:
u = active.pop()
for v in u.adjacent():
if v not in visited:
visited.add(v)
newactive.add(v)
active = newactive
return visited
La funzione prende in input un grafo (di tipo Graph
) e un nodo (di tipo Node
) e ritorna l'insieme dei nodi che si possono visitare partendo da qual nodo. Posiamo vedere un esempio dei primi passi di una visita nelle figure seguenti.
Questo è il grafo e la seguente figura mostra il nodo di partenza. I nodi in active
sono visualizzati marcati di rosso:
La prossima figura mostra marcati di giallo i nodi nuovi visitati dal nodo attivo. Questi sono salvati in newactive
. Gli archi che sono stati attraversati sono marcati.
Poi i quattro nodi in newactive
diventano i nodi di active
(per cui sono marcati in rosso) e nella figura qui sotto si vede il primo di questi che ha permesso di visitare altri nuovi nodi. I nodi visitati che non sono più attivi sono marcati in nero.
Le prossime tre figure mostrano la visita degli altri nodi in active
:
Quando tutti nosi in active
sono stati considerati i nodi in newactive
diventano i nosi attivi:
E la visita procede fino a visitare tutti i nodi del grafo dato che in questo caso il grafo è connesso.
Per visualizzare in modo interattivo il comportamento dell'algoritmo di visita di un grafo si può usare la funzione visualize()
del modulo vgraph
nel pacchetto di questa lezione. Se si invoca vgraph.visualize()
senza parametri si ottiene la visualizzazione di un grafo d'esempio. Altrimenti si può passare alla funzione o un grafo (di tipo Graph
) oppure una lista di nodi (tutti di tipo Node
). Quest'ultima opzione è utile se si vuole visualizzare solo una parte di un grafo (verrà visualizzato solamente il sottografo indotto dai nodi nella lista). La funzione visualizza il grafo in una finestra. Il primo botone redraw ridisegna il grafo, il bottone Start Visit predispone l'inizio di una visita che iniziarà quando si sceglierà il nodo di partenza faccendo click con il mouse su di esso. Il terzo bottone Visit Step avanza di un passo la visita, il bottone Visit porta a termine la visita (con una animazione dei singoli passi). L'ultimo bottone Clear Visit cancella la visita, permettendo così di iniziarne una nuova.
Purtroppo il nostro grafo dei film e degli attori/attrici è troppo grande per essere visualizzato. Possiamo però provare a vistarlo a partire da certi nodi. Ad esempio
graph = create_graph('films.txt', 'actors.txt')
print 'Number of nodes:', len(graph.nodes())
node = graph.get_node(Node('Robert De Niro'))
visited = visit(graph, node)
print 'Visited from '+node.name, len(visited)
Il risultato è
Number of nodes: 24610
Visited from Robert De Niro 23023
Questo ci dice che il grafo non è connesso. la componente connessa che contiene Rober De Niro è molto grande perché comprende 23023
nodi. Quindi ne rimangono fuori solamente 1587
.
Distanze
Come nel caso dei labirinti in cui avevamo già notato che la visita in ampiezza permette di determinare i percorsi più brevi tra due nodi, lo stesso vale per grafi qualsiasi. Possiamo modificare la funzione della visit()
per far sì di calcolare il percorso più breve tra due nodi (se ne esiste almeno uno). Per fare ciò durante la visita dobbiamo salvare per ogni nuovo nodo che viene visitato il nodo che ha permesso di visitarlo. Così alla fine della visita potremmo partendo dal nodo che dovevamo raggiunegere ripercorrere a ritroso il cammino che ci ha portato la visita a raggiungerlo. Quindi l'insieme visited
viene sostituito con un dizionario prev
che ad ogni ad associa il nodo che lo ha scoperto. La visita termina o quando abbiamo raggiunto il nodo voluto oppure quando non ci sono più nodi da visitare.
def shortestpath(g, u1, u2):
node1 = graph.get_node(u1)
node2 = graph.get_node(u2)
prev = {}
prev[node1] = None
active = set([node1])
while len(active) > 0 and node2 not in prev:
newactive = set()
while len(active) > 0 and node2 not in prev:
u = active.pop()
for v in u.adjacent():
if v not in prev:
prev[v] = u
newactive.add(v)
active = newactive
if u2 in prev:
path = [node2]
v = node2
while not v == u1:
v = prev[v]
path.insert(0, v)
return path
else:
return []
Al termine della visita se il nodo u2
è stato raggiunto si costruisce il cammino a ritroso come abbiamo già detto. Possiamo fare alcune prove:
def printpath(graph, u1, u2):
path = shortestpath(graph, u1, u2)
print 'Distance:', len(path) - 1
for u in path:
print ' ', u.name
print
printpath(graph, Node('Robert De Niro'), Node('Sophia Loren'))
printpath(graph, Node('Virna Lisi'), Node('George Clooney'))
printpath(graph, Node('Ugo Tognazzi'), Node('Marilyn Monroe'))
E scopriamo che le distanze sono:
Distance: 6
Robert De Niro
Cape Fear (1991)
Gregory Peck
Roman Holiday (1953)
Tullio Carminati
El Cid (1961)
Sophia Loren
Distance: 6
Virna Lisi
La reine Margot (1994)
Thomas Kretschmann
Valkyrie (2008)
Tom Wilkinson
Michael Clayton (2007)
George Clooney
Distance: 8
Ugo Tognazzi
La grande bouffe (1973)
Michel Piccoli
Atlantic City (1980)
Burt Lancaster
Birdman of Alcatraz (1962)
Thelma Ritter
The Misfits (1961)
Marilyn Monroe