Perché dire che la prima ricerca viene eseguita nel tempo


9

Si afferma spesso (ad esempio, in Wikipedia ) che il tempo di esecuzione della prima ricerca (BFS) su un grafico G=(V,E) è O(|V|+|E|) . Tuttavia, qualsiasi grafico collegato ha |V||E|+1 e, anche in un grafico non connesso, BFS non guarderà mai un vertice esterno al componente che contiene il vertice iniziale. Quel componente contiene al massimo |E| bordi, quindi contiene al massimo | E| +|E|+1 vertici, e questi sono gli unici che l'algoritmo visiterà.

Ciò significa che |V|+|E|2|E|+1 , quindi perché non diciamo che il tempo di esecuzione è solo ?O(|E|)

Ciò è emerso nei commenti su una domanda sul tempo di esecuzione dell'algoritmo di Disjkstra .


Perché pensi che ci sia un vertice iniziale? BFS nel massimo problema di corrispondenza, ad esempio, inizia da tutti i vertici senza pari dell'algoritmo hopcroft karp. In questo caso, se il grafico dato è la foresta di molti componenti collegati avremo più vertici che bordi e li visiteremo tutti
narek Bojikian

2
@narekBojikian Sebbene BFS possa essere utilizzato in vari modi, quando presentato come algoritmo autonomo, ha quasi sempre un vertice di avvio.
David Richerby,

Risposte:


9

Di solito BFS è descritto in modo simile al seguente (da Wikipedia ).

 1  procedure BFS(G,start_v):
 2      let Q be a queue
 3      label start_v as discovered
 4      Q.enqueue(start_v)
 5      while Q is not empty
 6          v = Q.dequeue()
 7          if v is the goal:
 8              return v
 9          for all edges from v to w in G.adjacentEdges(v) do
10             if w is not labeled as discovered:
11                 label w as discovered
12                 w.parent = v
13                 Q.enqueue(w)

Il problema è in qualche modo sottile: si nasconde nella riga 3! La domanda è: quale struttura di dati utilizzeremo per memorizzare quali vertici sono stati scoperti?

La soluzione più semplice è utilizzare un array booleano con una voce per vertice. In questo caso, dobbiamo inizializzare tutti gli elementi dell'array su falsee ciò richiede tempo Θ(|V|) . Questo vale per ogni grafico, anche se non ci sono bordi, quindi non possiamo assumere alcuna relazione tra |V||E|e otteniamo un tempo di esecuzione di O(|V|+|E|) .

Possiamo evitare di avere una struttura di dati con Θ(|V|) tempo di inizializzazione? Il nostro primo tentativo potrebbe essere quello di utilizzare un elenco collegato. Tuttavia, ora testare se è stato scoperto un vertice (riga 10) richiede un tempo lineare nel numero di vertici visitati, anziché tempo costante come prima. Ciò significa che il tempo di esecuzione diventa O(|V||E|) , che è molto peggio nel caso peggiore. (Nota che non vogliamo riscriverlo comeO(|E|2) dato che è anche peggio: potrebbe essere cattivo come|V|4 , mentre|V||E||V|3 )

L'uso di un array ridimensionato dinamicamente ci consentirebbe di mantenere ordinato l'elenco, quindi ora le ricerche richiederebbero solo il tempo O(log|V|) ma ciò fornisce comunque un tempo di esecuzione di solo O(|E|log|V|) , che è ancora peggio dello standard.

Infine, potremmo usare una tabella hash di dimensioni dinamiche: iniziare con una tabella di dimensioni costanti  c e raddoppiarla ogni volta che si riempie per metà. Ciò significa che la dimensione finale della tabella è al massimo il doppio del numero di vertici scoperti prima della fine dell'algoritmo e questo è al massimo |E|+1 perché non scopriamo mai nulla al di fuori del componente del vertice iniziale. Inoltre, la quantità totale di lavoro svolto copiando la tabella hash per espanderla è al massimo c+2c+4c++2|E|4|E|. Le ricerche e gli inserimenti nella tabella hash sono ammortizzati O(1) quindi otteniamo effettivamente un tempo di esecuzione di O(|E|) .

Quindi O(|E|) è possibile, ma vorresti farlo in una vera implementazione? Direi che probabilmente no. A meno che tu non abbia motivo di credere che i tuoi grafici di input avranno molti piccoli componenti, il sovraccarico di mantenere la tabella hash aggiungerà un notevole fattore costante al tempo di esecuzione. La crescita della tabella hash potrebbe richiedere del tempo 4|E|e le ricerche richiederanno di calcolare la funzione hash e, in media, guardare più di uno slot nella tabella. Le scarse prestazioni della cache delle tabelle hash potrebbero anche farti del male su un vero computer. Nella maggior parte dei casi con l'implementazione di array standard, O(|E|)parte è il termine dominante del tempo di esecuzione O(|V|+|E|) quindi non vale la pena usare una tabella hash per rimuovere il termine dominato, dato il costo pratico di farlo.


1
Penso che potrebbe essere troppo forte per affermare che le tabelle hash in pratica hanno scarse prestazioni della cache. Se implementato con concatenamento (ovvero elenchi collegati), sono d'accordo. Ma se implementato con un pezzo continuo di memoria e indirizzamento aperto, non tanto.
Juho,

Risposta meravigliosa davvero! Una nota marginale però, le tabelle hash di dimensioni dinamiche sono davvero una buona scelta non solo se ci sono molti piccoli componenti, ma anche se il valore di hash per qualsiasi vertice è delimitato da una costante ragionevole e questo accade spesso. Bella risposta!
Carlos Linares López,

1
David, ho avuto pensieri simili anni fa. Penso che la risposta stia nelle prospettive storiche.
Kelalaka,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.