Visite di grafi - BFS e DFS
In questa pagina 6
Una visita (traversal) è un'esplorazione sistematica di a partire da un vertice . 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. Su un albero preorder e postorder sono DFS; la BFS corrisponde alla visita per livelli, quella della numerazione a livelli degli heap (HeapAlbero binario completo e sua altezza floor(log2 n); heap = albero completo con heap-order property; proprietà (radice minima, cammini non decrescenti); rappresentazione su array con level numbering; insert con up-heap bubbling, removeMin con down-heap bubbling, rimozione di una entry qualsiasi; invarianti e costi Theta(log n); esempio svolto.Heap →). La differenza con gli alberi è che un grafo può avere cicli, quindi bisogna ricordare i vertici già visti (campo ID) per non girare all'infinito. Una scansione semplice di 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. Le liste di livelli dell'algoritmo fanno da coda FIFO (Liste, pile e codeRipasso degli ADT elementari: lista index-based (array) e position-based (lista doppiamente concatenata con sentinelle), pila LIFO, coda FIFO, deque, iteratori; interfacce, implementazioni e costi; array estendibile e coda circolare.Liste, pile e code →): si esaminano i vertici nell'ordine in cui sono stati scoperti.
- 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. La ricorsione usa lo stack delle chiamate (Algoritmi ricorsiviAlgoritmi ricorsivi come induzione eseguita; albero della ricorsione e record di attivazione nello stack; esempi ReverseArray, LinearSum, Power in tempo logaritmico, Fibonacci ricorsivo (esponenziale) e con memoizzazione; analisi della complessità con l'albero della ricorsione e correttezza per induzione.Algoritmi ricorsivi →, Stack e chiamata di funzioni in ARMChiamata con BL e ritorno con BX LR; convenzione di chiamata AAPCS (argomenti in r0-r3, risultato in r0, registri da preservare r4-r11); stack discendente pieno con PUSH e POP; record di attivazione; funzioni foglia e non foglia; esempio ricorsivo del fattoriale con evoluzione dello stack.Stack e chiamata di funzioni in ARM →): è una pila LIFO, e per questo la DFS si può scrivere anche con una pila esplicita al posto della ricorsione. In un grafo molto profondo (un cammino di vertici) la ricorsione consuma di stack e in C può esaurirlo (Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →).
Convenzioni: ogni vertice ha un campo v.ID ( = non visitato, = visitato); ogni arco ha e.label (null = non etichettato). incidentEdges(v) restituisce un iteratore agli archi incidenti su (la lista di adiacenza) e opposite(v, e) il vertice di diverso da , entrambi in per elemento. Prima di una visita tutti i vertici hanno ID e tutti gli archi sono non etichettati.
BFS
Definizione (visita in ampiezza, BFS). La BFS da visita la componente connessa di , etichetta gli archi di come
DISCOVERY EDGEoCROSS EDGEe divide i vertici in livelli secondo la distanza da (distanza = lunghezza minima di un cammino; se in componenti diverse).
Esempio. Sul grafo con liste ; ; ; ; ; ; , la BFS da ha livelli , , , .
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 + 1Proprietà (BFS e spanning tree). Dopo
BFS(G, s): tutti i vertici di sono visitati e tutti i suoi archi etichettati; i discovery edge formano uno spanning tree di radicato in (il BFS tree); per ogni il cammino nel BFS tree da a ha archi; se è un cross edge i livelli di e differiscono al più di .
Esempio. Nello stesso grafo (cammino ) e il BFS tree ha discovery edge.
Perché i livelli sono le distanze (per induzione sul livello: Principio di induzionePer dimostrare una proprietà per ogni n basta il passo base e il passo induttivo da n a n+1.Principio di induzione →). è a distanza . Se i vertici di sono esattamente quelli a distanza , i vertici scoperti scorrendo sono vicini di vertici a distanza non ancora visti: non possono essere a distanza (sarebbero già in un livello precedente), quindi sono a distanza . E ogni vertice a distanza ha un vicino a distanza , perciò viene scoperto. Per un cross edge con : alla fine dell'esame di il vertice è già visitato, quindi sta in un livello ; per simmetria sta in un livello , e i livelli differiscono al più di .
Traccia sul grafo dell'esempio (liste ; ; ; ; ; ; ), con il livello in esame:
| esame di | archi non etichettati incontrati | azione | lista del livello successivo |
|---|---|---|---|
| , vertice | , | scoperti e (discovery) | |
| , vertice | , | scoperto; già visto: cross | |
| , vertice | scoperto | ||
| , vertice | , | scoperto; già visto: cross | |
| , vertice | , | già visto: cross; scoperto | |
| nessuno nuovo | tutti gli archi sono già etichettati | , fine |
Formula (complessità della BFS). Ogni vertice di entra in una lista una sola volta (poi
ID) e ogni arco di viene etichettato una volta; dalla lista di adiacenza di ciascun vertice ogni arco è esaminato due volte (una per estremo). Con vertici e archi in il costo è (se non è isolato, ). Se è connesso, . Il conto: ogni vertice di è estratto da una lista una volta e per ciascuno si percorre la sua lista di adiacenza, di lunghezza (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 →); il costo totale è perché la somma dei gradi è (Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →), e . Con la matrice di adiacenza lo scorrimento dei vicini di ogni vertice costerebbe e il totale .
Esempio. Con vertici e archi il costo è .
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 EDGEProprietà (DFS). Dopo
DFS(G, s): tutti i vertici di sono visitati, tutti gli archi di sono etichettatiDISCOVERYoBACK, e i discovery edge formano uno spanning tree di radicato in . Complessità con liste di adiacenza ( se connesso), con lo stesso argomento della BFS. Un vertice si dice discoverable da se esiste un cammino da a fatto di vertici non ancora visitati.
Esempio. La DFS da nello stesso grafo visita con discovery edge e back edge, , e .
Traccia (la pila delle chiamate cresce a ogni discovery edge e si accorcia ai ritorni): da si scende a (primo vicino non visitato), da a , da a , da a . In , con lista : l'arco non è etichettato ma è già visitato, quindi è un back edge; non è visitato, discovery edge e si scende in , dove e portano a vertici già visitati: back edge e . Si torna in : l'arco è già etichettato (è il discovery edge visto da ); non è visitato, discovery edge . Nessun vertice è visitato due volte, perché l'ID lo impedisce.
Esempio svolto
Grafo con vertici e liste di adiacenza in ordine crescente: ; ; ; ; ; ; .
- DFS da 1: ordine di visita . Discovery edge: (da si va in , poi si torna e si scopre ). Back edge: , , .
- BFS da 1: ordine ; livelli , , , . Discovery edge: . Cross edge: , , . In effetti (tramite ) e non come nel cammino dell'albero DFS (quattro archi): la BFS dà le distanze minime, la DFS no.
(Tracce verificate eseguendo le implementazioni.) Un secondo esempio d'esame: liste ; ; ; ; ; ; ; ; . La BFS da visita (livello 1: ; livello 2: ; livello 3: ).
Applicazioni (tutte in )
| Problema | Come si risolve |
|---|---|
| visitare tutto il grafo | si ripete la visita da ogni vertice non visitato |
| connettività | numero di componenti |
| componenti connesse | contatore ; per ogni con ID , e BFS(G, v, k) con w.ID <- k; restituisce 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 e (BFS) | campo u.parent: quando è discovery, u.parent <- v; dopo BFS(G, s), se non è visitato non c'è cammino, altrimenti si risale da a con parent |
| raggiungibilità , un cammino qualsiasi | DFS o BFS da |
| ciclo | si trova un cross edge (BFS) o back edge (DFS) e si risale da e lungo parent fino a un antenato comune: il cammino più l'arco è un ciclo; se non ci sono cross/back edge il grafo è aciclico (è una foresta, per il confronto con si veda Grafi - definizioni e proprietàGrafo G=(V,E) diretto e non diretto, grafo semplice e pesato; incidenza, adiacenza, grado; cammini, cicli, sottografi, grafi connessi e componenti connesse; alberi liberi, foreste, spanning tree e spanning forest; proprietà con dimostrazioni (somma dei gradi = 2m, m <= n(n-1)/2, alberi m = n-1, connessi m >= n-1, foreste m <= n-1).Grafi - definizioni e proprietà →); esempio: cross edge con parent di e uguale a dà il ciclo |
Adattare la BFS (domande d'esame ricorrenti):
- vertici a distanza da : si esegue la BFS fermandosi quando (non si costruisce ) e si contano i vertici dei livelli ; costo ;
- numero di vertici raggiungibili da e distanza massima: si conta il numero di vertici inseriti in lista e si restituisce 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
Proprietà (confronto tra BFS e 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)
Esempio. La BFS trova , mentre il cammino da a nell'albero DFS () ha lunghezza .
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 →). Nelle reti di calcolatori la BFS è l'idea dell'instradamento con il minimo numero di salti (Algoritmi di instradamento - link state e distance vectorL'instradamento (routing) trova il percorso di costo minimo in un grafo pesato in cui i router sono nodi e le reti tra due router sono archi. Link state: ogni router diffonde con un flooding i pacchetti LSP sui propri collegamenti, ricostruisce tutto il grafo e applica Dijkstra (nodi con stato (distanza, permanente o temporaneo)). Distance vector: ogni router conosce solo i vicini e scambia con loro il proprio vettore delle distanze, aggiornato con Bellman-Ford $D_{A,w}=\min{D_{A,w},,D_{A,Y}+d_{Y,w}}$; converge in al più $n-1$ giri ma può soffrire del conteggio all'infinito (limite a 16, hold down, aggiornamenti immediati, split horizon con poison reverse). Path vector: ogni annuncio porta l'intero cammino, si sceglie per politica e non per costo, e i cicli si scoprono trovando sé stessi nel cammino. Dijkstra è corretto se i costi sono non negativi (invariante: un nodo permanente ha già la distanza vera); Bellman-Ford è corretto perché dopo $k$ giri conosce i cammini minimi con al più $k+1$ archi.Algoritmi di instradamento - link state e distance vector →); nella DFS la stessa struttura "vai avanti finché puoi, poi torna indietro" è il backtracking della ricorsione (RicorsioneFunzioni che chiamano se stesse: caso base e passo ricorsivo, stack delle chiamate, esempi su numeri, stringhe e liste, ricorsione multipla e suo costo, divide et impera, confronto con l'iterazione.Ricorsione →).
Errori comuni
- Controllare
IDma nonlabel: 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 .
- Dire che la BFS costa o la DFS : il costo è (liste di adiacenza); con la matrice sarebbe .
- Usare la DFS per cammini minimi in numero di archi.
Versione ripasso
- Campi:
v.ID( = non visitato, altrimenti visitato);e.label(null, poiDISCOVERYoCROSSnella BFS,DISCOVERYoBACKnella DFS). Le visite sono design pattern da adattare (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 →); con i cicli serve ricordare i vertici già visti. - BFS() (in ampiezza): livelli e = nuovi vicini dei vertici di . Arco non etichettato: se è
DISCOVERYe va in ; altrimentiCROSS. Le liste di livelli fanno da coda FIFO (vedi Liste, pile e codeRipasso degli ADT elementari: lista index-based (array) e position-based (lista doppiamente concatenata con sentinelle), pila LIFO, coda FIFO, deque, iteratori; interfacce, implementazioni e costi; array estendibile e coda circolare.Liste, pile e code →).- Proprietà: i discovery edge formano il BFS tree (spanning tree di ); ⇒ ; i cross edge collegano livelli che differiscono al più di .
- Perché i livelli sono le distanze (induzione sul livello, Principio di induzionePer dimostrare una proprietà per ogni n basta il passo base e il passo induttivo da n a n+1.Principio di induzione →): i vertici scoperti da non sono ancora visti, quindi sono a distanza ; e ogni vertice a distanza ha un vicino a distanza .
- DFS() (in profondità): visita ; per ogni arco non etichettato, se è
DISCOVERYe si ricorre su , altrimentiBACK. La pila delle chiamate fa da stack; i discovery edge formano uno spanning tree, ma i cammini non sono minimi. Su un cammino molto lungo la ricorsione può esaurire lo stack del C (Gestione della memoria in CSegmenti di memoria di un processo (codice, dati statici, stack, heap); durata delle variabili; allocazione dinamica con malloc, calloc, realloc e free; errori classici: memory leak, dangling pointer, double free, buffer overflow; strumenti di controllo.Gestione della memoria in C →). - Costo: per la componente di , se il grafo è connesso: con le liste di adiacenza ogni arco è visto due volte, e (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 →). Con la matrice sarebbe .
- Grafo non connesso:
forall v: v.ID <- 0, poiif v.ID = 0 then BFS(G, v)per ogni vertice. - Esempio (liste ; ; ; ; ; ; ): la DFS da dà , con discovery e back edge (, , ). La BFS da dà livelli , , , e tramite , mentre il cammino nell'albero DFS () ha archi.
- Applicazioni in : connettività (una sola componente); componenti (contatore con
w.ID <- k); spanning tree (discovery edge); cammino minimo in numero di archi conu.parent(BFS); raggiungibilità; ciclo (cross edge o back edge, risalendo conparentfino all'antenato comune). - Adattamenti della BFS: vertici a distanza (fermarsi a e contare); dimensione di ogni componente (contatore dei vertici scoperti).
- BFS contro DFS: la BFS ha cross edge tra livelli adiacenti e cammini minimi nell'albero; la DFS ha back edge e cammini non minimi; il costo è per entrambe.
- Pseudocodice BFS:
s.ID <- 1; L0 <- [s]; finché non è vuota: per ogni e ogni arco cone.label = null,opposite(v,e); sew.ID = 0:e.label <- DISCOVERY,w.ID <- 1, ; altrimentie.label <- CROSS. - Esempio d'esame: liste ; ; ; ; ; ; ; ; danno, con BFS da , l'ordine .
- Errori tipici: controllare
IDsenzalabel(ogni arco va esaminato due volte); non rilanciare la visita dagli altri vertici; usare la DFS per cammini minimi in numero di archi; scrivere il costo come o invece di .
Esercizi su questo argomento
- Esercizio - Algoritmi di ricerca in Python con Problem, Node e best-first search (laboratorio 3)
- Esercizio 21 · visita BFS e spanning forest
- Esercizio 22 · vertici influenzabili e vertici entro distanza d
- Esercizio 23 · escursioni e componenti connesse
- Esercizio - Passeggiata aleatoria su un grafo con triangolo e coda - stazionaria, raggiungimento e copertura
- Esercizio - Grafo di allocazione delle risorse e individuazione dei cicli (eserciziario, esercizi 20, 21, 56, 106 e 109)
Teoria collegata
- Inferenza causale - modelli causali strutturali e scala della causalità
- Prolog - fatti, regole, ricorsione, liste e cut
- Ricerca nello spazio degli stati - formulazione dei problemi e ricerca non informata
- Cammini minimi e algoritmo di Dijkstra
- Grafi - definizioni e proprietà
- Grafi diretti e ordinamento topologico
- Liste, pile e code
- Minimum spanning tree
- Rappresentazione dei grafi
- RIP in Katharà
- Deadlock - condizioni, prevenzione, evitamento e rilevamento