Fondamenti di Programmazione

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.

Esempio visita 1

Questo è il grafo e la seguente figura mostra il nodo di partenza. I nodi in active sono visualizzati marcati di rosso:

Esempio visita 2

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.

Esempio visita 3

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.

Esempio visita 4

Le prossime tre figure mostrano la visita degli altri nodi in active:

Esempio visita 5

Esempio visita 6

Esempio visita 7

Quando tutti nosi in active sono stati considerati i nodi in newactive diventano i nosi attivi:

Esempio visita 8

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