concetti base
raggiungibilità
Un nodo è raggiungibile da un nodo se esiste almeno un cammino che da arriva a .
connettività
Il problema della connettività si occupa di determinare se, comunque prendo due nodi e , tra essi c’è un cammino.
Depth-First Search (DFS)
La strategia della visita depth-first consiste nel visitare il grafo sempre più “in profondità” (unexpected), quando possibile. Partendo da un nodo, si prosegue lungo un sentiero finché non si arriva a un nodo che non ha più archi da seguire, quindi si torna indietro per esplorare eventuali altri archi dai nodi già visitati. Durante tutta la ricerca, non si visitano mai (all‘“andata”) nodi già visitati, per non cadere in cicli.
visita DFS su grafo rappresentato tramite matrice di adiacenza:
- usiamo un vettore dei visitati per tenere conto dei nodi già visitati: quando si trova un nodo , lo si visita solo se
def DFS(u, M)
visitati[u] = 1
for i in range(len(M)): # si scorrono i vicini di u - θ(n)
if M[u][i] and not visitati[i]:
DFS(i, M, visitati) # O(n) (entro massimo n volte)
# ^ quindi questa funzione ha costo O(n)xΘ(n)
n = len(M)
visitati = [0]*n # Θ(n)
DFS(u, M, visitati)
return [x for x in range(n) if visitati[x]] # Θ(n)
# ^ prendo dai nodi (range della lunghezza delle liste) solo quelli che sono stati visitati
- al termine dell’esecuzione della funzione ricorsiva, si ha se e solo se è raggiungibile da .
- questo algoritmo ha costo
visita DFS su grafo rappresentato tramite liste di adiacenza:
def DFS(u, G, visitati):
visitati[u] = 1
for v in G[u]:
if not visitati[v]:
DFS(v, G, visitati)
# ^ questa funzione ha costo O(n+m)
n = len(G)
visitati = [0]*n
DFS(u, G, visitati)
return [x for x in range(n) if visitati(x)] # Θ(n)
Per ogni nodo , l’algoritmo scorrerà la lista dei suoi adiacenti. Il ciclo for avrà quindi complessità (somma degli elementi nelle liste di tutti i nodi visitati), che, visto che non si visiterà mai un nodo due volte, corrisponde a (numero di archi) nel caso in cui il grafo è diretto, o nel caso di un grafo indiretto.
- in entrambi i casi, la somma di tutte le iterazioni del ciclo for sarà quindi in
Il costo totale sarà quindi .
- se il grafo è sparso, , quindi la complessità temporale totale sarà
versione iterativa
def DFS_i(u, G):
visitati = [0]*len(G)
stack = [u]
while stack:
u = stack.pop()
if not visitati[u]:
visitati[u] = 1
for v in G[u]:
if not visitati[v]:
stack.append(v)
return [x for x in range(len(G)) if visitati[x]]
- anche qui, la complessità temporale è
albero DFS
Visto che la ricerca visita solo nodi non precedentemente visitati, con una visita DFS, gli archi del grafo si dividono in attraversati e non attraversati. I nodi visitati e gli archi attraversati formano un albero detto albero DFS.
vettore dei padri
L’albero DFS si può memorizzare tramite il vettore dei padri ⇒ un vettore che contiene, per ogni entrata , l’indice del suo nodo padre.
- (per convenzione, il padre della radice è essa stessa - )
- se non è un nodo dell’albero (in questo caso, se non è stato visitato),
(a sinistra un grafo, a destra i tre alberi DFS che si ottengono partendo dai nodi 9, 4 e 3 rispettivamente)
esempio
per esempio, questo albero sarà rappresentato dal seguente vettore dei padri:
La visita DFS può essere modificata in modo da memorizzare il vettore dei padri anziché quello dei visitati.
def DFSp(x, G, P):
for y in G[x]:
if P[y] == -1:
P[y] = x
DFSp(y, G, P)
def padri(u, G):
n = len(G)
P = [-1]*n
P[u] = u
DFSr(u, G, P)
return P
- al termine dell’algoritmo, se non è stato visitato. altrimenti, contiene il padre di
trovare un cammino
Molto spesso non basta sapere se un nodo sia raggiungibile da un nodo , ma si vuole anche determinare un cammino che permetta di arrivare da a .
- il vettore dei padri rende questa operazione molto semplice: basta controllare che il nodo sia nell’albero DFS, ed effettuare un reverse dei nodi incontrati
versione iterativa:
def Cammino(u, P):
if P[u] == -1: return [] # se non è nell'albero (non visitato)
path = []
while P[u] != u: # se P[u] = u, siamo arrivati al nodo che cerchiamo
path.append(u)
u = P[u] # seguiamo i padri
path.append(u)
path.reverse()
return path
versione ricorsiva:
def CamminoR(u, P):
if P[u] == -1: return []
if P[u] == u: return [u]
return CamminoR(P[u], P) + [u]
- in entrambi i casi, disponendo del vettore dei padri, la complessità è
attenzione
se esistono più cammini da a , la procedura non garantisce la restituzione del cammino minimo