Salta al contenuto
Note per Studenti Visite di grafi - BFS e DFS

Visite di grafi - BFS e DFS

In questa pagina 6

Una visita (traversal) è un'esplorazione sistematica di G=(V,E)G = (V, E) a partire da un vertice ss. Come per gli alberi (vedi Visite di alberiVisite in preorder e postorder come schemi generali (template) da adattare; complessità Theta(n + somma dei costi di visita) perché la somma dei figli è n-1; esempi (indice di un libro, spazio occupato in un file system); profondità con il preorder, altezza con il postorder; antenato comune più basso.Visite di alberi →) sono schemi generali (design pattern) in cui l'operazione di "visita" si adatta al problema. Una scansione semplice di LVL_V e LEL_E tocca tutto ma senza seguire la struttura del grafo, e serve a poco (vedi Rappresentazione dei grafiStrutture di base (lista dei vertici LV, lista degli archi LE), liste di adiacenza, matrice di adiacenza; operazioni incidentEdges, opposite, areAdjacent; spazio e tempi a confronto; esempio con un grafo di 5 vertici.Rappresentazione dei grafi →).

  • BFS (breadth-first search, in ampiezza): visitato un vertice, si visitano tutti i suoi vicini prima dei vicini dei vicini.
  • DFS (depth-first search, in profondità): visitato un vertice, si visita un vicino, poi un vicino del vicino, e così via, tornando indietro quando non ci sono nuovi vertici.

Convenzioni: ogni vertice vv ha un campo v.ID (00 = non visitato, ≥1\ge 1 = visitato); ogni arco ee ha e.label (null = non etichettato). incidentEdges(v) restituisce un iteratore agli archi incidenti su vv (la lista di adiacenza) e opposite(v, e) il vertice di ee diverso da vv, entrambi in Θ(1)\Theta(1) per elemento. Prima di una visita tutti i vertici hanno ID =0= 0 e tutti gli archi sono non etichettati.

BFS

La BFS da ss visita la componente connessa CsC_s di ss, etichetta gli archi di CsC_s come DISCOVERY EDGE o CROSS EDGE e divide i vertici in livelli LiL_i secondo la distanza ii da ss (distanza d(x,y)d(x, y) = lunghezza minima di un cammino; +∞+\infty se in componenti diverse).

Algoritmo BFS(G, s)
visita s; s.ID <- 1
L0 <- lista con s; i <- 0
while Li non è vuota do
    Li+1 <- lista vuota
    forall v in Li do
        forall e in G.incidentEdges(v) do
            if e.label = null then
                w <- G.opposite(v, e)
                if w.ID = 0 then
                    e.label <- DISCOVERY EDGE
                    visita w; w.ID <- 1; inserisci w in Li+1
                else e.label <- CROSS EDGE
    i <- i + 1

Proprietà. Dopo BFS(G, s): tutti i vertici di CsC_s sono visitati e tutti i suoi archi etichettati; i discovery edge formano uno spanning tree di CsC_s radicato in ss (il BFS tree); per ogni v∈Liv \in L_i il cammino nel BFS tree da ss a vv ha i=d(s,v)i = d(s, v) archi; se (u,v)(u, v) è un cross edge i livelli di uu e vv differiscono al più di 11.

Complessità. Ogni vertice di CsC_s entra in una lista una sola volta (poi ID ≠0\ne 0) e ogni arco di CsC_s viene etichettato una volta; dalla lista di adiacenza di ciascun vertice ogni arco è esaminato due volte (una per estremo). Con nsn_s vertici e msm_s archi in CsC_s il costo è Θ(ns+ms)=Θ(ms)\Theta(n_s + m_s) = \Theta(m_s) (se ss non è isolato, ms≥ns−1m_s \ge n_s - 1). Se GG è connesso, Θ(n+m)\Theta(n + m).

Per visitare tutto il grafo anche se non connesso:

forall v in V do v.ID <- 0
forall v in V do
    if v.ID = 0 then BFS(G, v)

DFS

Algoritmo DFS(G, v)          (prima invocazione: v = s)
visita v; v.ID <- 1
forall e in G.incidentEdges(v) do
    if e.label = null then
        w <- G.opposite(v, e)
        if w.ID = 0 then
            e.label <- DISCOVERY EDGE
            DFS(G, w)
        else e.label <- BACK EDGE

Dopo DFS(G, s): tutti i vertici di CsC_s sono visitati, tutti gli archi di CsC_s sono etichettati DISCOVERY o BACK, e i discovery edge formano uno spanning tree di CsC_s radicato in ss. Complessità Θ(ms)\Theta(m_s) con liste di adiacenza (Θ(n+m)\Theta(n + m) se connesso), con lo stesso argomento della BFS. Un vertice uu si dice discoverable da vv se esiste un cammino da vv a uu fatto di vertici non ancora visitati.

Esempio svolto

Grafo con vertici 1..71..7 e liste di adiacenza in ordine crescente: 1:2,41: 2, 4; 2:1,3,42: 1, 3, 4; 3:2,5,63: 2, 5, 6; 4:1,2,64: 1, 2, 6; 5:3,65: 3, 6; 6:3,4,5,76: 3, 4, 5, 7; 7:67: 6.

  • DFS da 1: ordine di visita 1,2,3,5,6,4,71, 2, 3, 5, 6, 4, 7. Discovery edge: (1,2),(2,3),(3,5),(5,6),(6,4),(6,7)(1,2), (2,3), (3,5), (5,6), (6,4), (6,7) (da 66 si va in 44, poi si torna e si scopre 77). Back edge: (3,6)(3,6), (1,4)(1,4), (2,4)(2,4).
  • BFS da 1: ordine 1,2,4,3,6,5,71, 2, 4, 3, 6, 5, 7; livelli L0={1}L_0 = \{1\}, L1={2,4}L_1 = \{2, 4\}, L2={3,6}L_2 = \{3, 6\}, L3={5,7}L_3 = \{5, 7\}. Discovery edge: (1,2),(1,4),(2,3),(4,6),(3,5),(6,7)(1,2), (1,4), (2,3), (4,6), (3,5), (6,7). Cross edge: (2,4)(2,4), (3,6)(3,6), (5,6)(5,6). In effetti d(1,6)=2d(1, 6) = 2 (tramite 44) e non 33 come nel cammino dell'albero DFS 1,2,3,5,61, 2, 3, 5, 6: la BFS dà le distanze minime, la DFS no.

(Tracce verificate eseguendo le implementazioni.) Un secondo esempio d'esame: liste A:B,C,DA: B,C,D; B:A,E,FB: A,E,F; C:A,GC: A,G; D:A,HD: A,H; E:BE: B; F:B,IF: B,I; G:CG: C; H:D,IH: D,I; I:F,HI: F,H. La BFS da AA visita A,B,C,D,E,F,G,H,IA, B, C, D, E, F, G, H, I (livello 1: B,C,DB, C, D; livello 2: E,F,G,HE, F, G, H; livello 3: II).

Applicazioni (tutte in O(n+m)O(n + m))

Problema Come si risolve
visitare tutto il grafo si ripete la visita da ogni vertice non visitato
connettività numero di componenti =1= 1
componenti connesse contatore kk; per ogni vv con ID =0= 0, k←k+1k \leftarrow k + 1 e BFS(G, v, k) con w.ID <- k; restituisce kk e ogni vertice porta l'identificativo della sua componente
spanning tree (grafo connesso) si restituiscono i discovery edge di una BFS/DFS da un vertice qualsiasi
cammino minimo in numero di archi tra ss e tt (BFS) campo u.parent: quando (v,u)(v, u) è discovery, u.parent <- v; dopo BFS(G, s), se tt non è visitato non c'è cammino, altrimenti si risale da tt a ss con parent
raggiungibilità s→ts \to t, un cammino qualsiasi DFS o BFS da ss
ciclo si trova un cross edge (u,v)(u, v) (BFS) o back edge (DFS) e si risale da uu e vv lungo parent fino a un antenato comune: il cammino più l'arco è un ciclo; se non ci sono cross/back edge il grafo è aciclico

Adattare la BFS (domande d'esame ricorrenti):

  • vertici a distanza ≤d\le d da ss: si esegue la BFS fermandosi quando i=di = d (non si costruisce Ld+1L_{d+1}) e si contano i vertici dei livelli L0,…,LdL_0, \dots, L_d; costo O(n+m)O(n + m);
  • numero di vertici raggiungibili da ss e distanza massima: si conta il numero di vertici inseriti in lista e si restituisce ii dell'ultimo livello non vuoto;
  • dimensione di ogni componente: una BFS che restituisce il contatore dei vertici scoperti (vedi l'Esercizio 23 · escursioni e componenti connesse).

BFS contro DFS

BFS DFS
struttura di appoggio liste di livelli (coda) ricorsione (pila)
archi non-tree cross edge, tra livelli adiacenti back edge
cammini nell'albero minimi in numero di archi non necessariamente minimi
costo (liste di adiacenza) Θ(n+m)\Theta(n + m) Θ(n+m)\Theta(n + m)

Per grafi pesati i cammini minimi richiedono l'algoritmo di Dijkstra, che generalizza la BFS (vedi Cammini minimi e algoritmo di DijkstraGrafi pesati, lunghezza di un cammino e distanza; sottocammini di un cammino minimo; problema SSSP; algoritmo di Dijkstra con cloud e priority queue, rilassamento degli archi, esempio svolto; correttezza (due lemmi) e complessità O(min(n^2, (n+m) log n)) con lista non ordinata o heap; pesi non negativi.Cammini minimi e algoritmo di Dijkstra →).

Errori comuni

  • Controllare ID ma non label: ogni arco verrebbe esaminato due volte e l'etichettatura non sarebbe consistente.
  • Dimenticare di rilanciare la visita dai vertici non visitati: si visita solo la componente di ss.
  • Dire che la BFS costa Θ(n)\Theta(n) o la DFS Θ(m)\Theta(m): il costo è Θ(n+m)\Theta(n + m) (liste di adiacenza); con la matrice sarebbe Θ(n2)\Theta(n^2).
  • Usare la DFS per cammini minimi in numero di archi.

Versione ripasso

Esercizi su questo argomento

Teoria collegata