Cosa farebbe sì che un algoritmo abbia complessità O (log log n)?


Risposte:


218

I termini O (log log n) possono essere visualizzati in una varietà di luoghi diversi, ma in genere ci sono due percorsi principali che arriveranno a questo runtime.

Restringersi da una radice quadrata

Come menzionato nella risposta alla domanda collegata, un modo comune per un algoritmo di avere complessità temporale O (log n) è che quell'algoritmo funzioni riducendo ripetutamente la dimensione dell'input di qualche fattore costante ad ogni iterazione. Se questo è il caso, l'algoritmo deve terminare dopo O (log n) iterazioni, perché dopo aver eseguito O (log n) divisioni per una costante, l'algoritmo deve ridurre la dimensione del problema fino a 0 o 1. Questo è il motivo, ad esempio , la ricerca binaria ha complessità O (log n).

È interessante notare che esiste un modo simile per ridurre le dimensioni di un problema che produce tempi di esecuzione della forma O (log log n). Invece di dividere l'input a metà su ogni livello, cosa succede se prendiamo la radice quadrata della dimensione su ogni livello?

Ad esempio, prendiamo il numero 65.536. Quante volte dobbiamo dividerlo per 2 finché non arriviamo a 1? Se lo facciamo, otteniamo

  • 65.536 / 2 = 32.768
  • 32.768 / 2 = 16.384
  • 16.384 / 2 = 8.192
  • 8.192 / 2 = 4.096
  • 4.096 / 2 = 2.048
  • 2.048 / 2 = 1.024
  • 1.024 / 2 = 512
  • 512/2 = 256
  • 256/2 = 128
  • 128/2 = 64
  • 64/2 = 32
  • 32/2 = 16
  • 16/2 = 8
  • 8/2 = 4
  • 4/2 = 2
  • 2/2 = 1

Questo processo richiede 16 passaggi ed è anche il caso che 65.536 = 2 16 .

Ma se prendiamo la radice quadrata a ogni livello, otteniamo

  • √65.536 = 256
  • √256 = 16
  • √16 = 4
  • √4 = 2

Notare che sono necessari solo quattro passaggi per arrivare fino a 2. Perché?

Innanzitutto, una spiegazione intuitiva. Quante cifre ci sono nei numeri ne √n? Ci sono approssimativamente log n cifre nel numero n, e approssimativamente log (√n) = log (n 1/2 ) = (1/2) log n cifre in √n. Ciò significa che, ogni volta che prendi una radice quadrata, stai approssimativamente dimezzando il numero di cifre nel numero. Poiché puoi solo dimezzare una quantità k O (log k) volte prima che scenda a una costante (diciamo 2), questo significa che puoi prendere solo radici quadrate O (log log n) volte prima di ridurre il numero verso il basso a qualche costante (diciamo 2).

Ora, facciamo un po 'di matematica per renderlo rigoroso. Le'ts riscrive la sequenza precedente in termini di potenze di due:

  • √65.536 = √2 16 = (2 16 ) 1/2 = 2 8 = 256
  • √256 = √2 8 = (2 8 ) 1/2 = 2 4 = 16
  • √16 = √2 4 = (2 4 ) 1/2 = 2 2 = 4
  • √4 = √2 2 = (2 2 ) 1/2 = 2 1 = 2

Si noti che abbiamo seguito la sequenza 2 16 → 2 8 → 2 4 → 2 2 → 2 1 . Ad ogni iterazione, dimezziamo l'esponente della potenza di due. È interessante, perché si ricollega a ciò che già sappiamo: puoi solo dividere il numero k per metà O (log k) volte prima che scenda a zero.

Quindi prendi un numero qualsiasi n e scrivilo come n = 2 k . Ogni volta che si prende la radice quadrata di n, si dimezza l'esponente in questa equazione. Pertanto, possono essere applicate solo radici quadrate O (log k) prima che k scenda a 1 o inferiore (nel qual caso n scende a 2 o inferiore). Poiché n = 2 k , ciò significa che k = log 2 n, e quindi il numero di radici quadrate prese è O (log k) = O (log log n). Pertanto, se è presente un algoritmo che funziona riducendo ripetutamente il problema a un sottoproblema di dimensione che è la radice quadrata della dimensione del problema originale, tale algoritmo terminerà dopo O (log log n) passaggi.

Un esempio nel mondo reale di questo è l' albero di van Emde Boas(vEB-tree) struttura dati. Un vEB-tree è una struttura dati specializzata per memorizzare interi nell'intervallo 0 ... N - 1. Funziona come segue: il nodo radice dell'albero ha puntatori √N, dividendo l'intervallo 0 ... N - 1 in √N bucket ciascuno contenente un intervallo di circa √N numeri interi. Questi bucket vengono quindi suddivisi internamente in √ (√ N) bucket, ciascuno dei quali contiene all'incirca √ (√ N) elementi. Per attraversare l'albero, si inizia dalla radice, si determina a quale bucket si appartiene, quindi si prosegue ricorsivamente nella sottostruttura appropriata. A causa del modo in cui è strutturato l'albero vEB, puoi determinare in O (1) tempo in quale sottostruttura scendere, e così dopo O (log log N) passi raggiungerai il fondo dell'albero. Di conseguenza, le ricerche in un albero vEB richiedono solo tempo O (log log N).

Un altro esempio è l' algoritmo della coppia di punti più vicina di Hopcroft-Fortune . Questo algoritmo tenta di trovare i due punti più vicini in una raccolta di punti 2D. Funziona creando una griglia di bucket e distribuendo i punti in tali bucket. Se in qualsiasi punto dell'algoritmo viene trovato un bucket che contiene più di √N punti, l'algoritmo elabora ricorsivamente quel bucket. La profondità massima della ricorsione è quindi O (log log n) e utilizzando un'analisi dell'albero di ricorsione si può dimostrare che ogni strato dell'albero funziona O (n). Pertanto, il tempo di esecuzione totale dell'algoritmo è O (n log log n).

O (log n) Algoritmi su piccoli ingressi

Esistono altri algoritmi che raggiungono tempi di esecuzione O (log log n) utilizzando algoritmi come la ricerca binaria su oggetti di dimensione O (log n). Ad esempio, la struttura dati x-fast trie esegue una ricerca binaria sui livelli dell'albero di altezza O (log U), quindi il runtime per alcune delle sue operazioni è O (log log U). Il trie y-fast correlato ottiene alcuni dei suoi tempi di esecuzione O (log log U) mantenendo BST bilanciati di O (log U) nodi ciascuno, consentendo alle ricerche in quegli alberi di essere eseguite nel tempo O (log log U). L' albero di tango e le relative strutture di dati dell'albero multisplay finiscono con un termine O (log log n) nelle loro analisi perché mantengono alberi che contengono O (log n) elementi ciascuno.

Altri esempi

Altri algoritmi raggiungono il runtime O (log log n) in altri modi. La ricerca per interpolazione prevedeva che il runtime O (log log n) trovasse un numero in un array ordinato, ma l'analisi è abbastanza coinvolta. In definitiva, l'analisi funziona mostrando che il numero di iterazioni è uguale al numero k tale che n 2 -k ≤ 2, per cui log log n è la soluzione corretta. Alcuni algoritmi, come l' algoritmo MST Cheriton-Tarjan , arrivano a un runtime che coinvolge O (log log n) risolvendo un complesso problema di ottimizzazione vincolata.

Spero che questo ti aiuti!


5
@ JonathonReinhart- Questo è accaduto per essere qualcosa che pensavo fosse (a) davvero, davvero interessante e (b) non molto conosciuto. Sono sempre felice di condividere cose come questa! :-)
templatetypedef


solo indovinare quali altre implicazioni potrebbe avere "Rispondi alla tua domanda" nella parte inferiore della pagina Fai domanda o abilita semplicemente la casella di testo della risposta nella pagina della domanda.
Mahesha999

1
Riga importante: Pertanto, se esiste un algoritmo che funziona riducendo ripetutamente il problema a un sottoproblema di dimensione che è la radice quadrata della dimensione del problema originale, tale algoritmo terminerà dopo O (log log n) passaggi.
Gun2sh

3

Un modo per vedere il fattore O (log log n) nella complessità temporale è per divisione come cose spiegate nell'altra risposta, ma c'è un altro modo per vedere questo fattore, quando vogliamo fare uno scambio tra tempo e spazio / tempo e approssimazione / tempo e durezza / ... di algoritmi e abbiamo qualche iterazione artificiale sul nostro algoritmo.

Ad esempio SSSP (Single source shortest path) ha un algoritmo O (n) sui grafi planari, ma prima di quel complicato algoritmo c'era un algoritmo molto più semplice (ma comunque piuttosto difficile) con tempo di esecuzione O (n log log n), il la base dell'algoritmo è la seguente (solo una descrizione molto approssimativa, e mi offrirei di saltare la comprensione di questa parte e leggere l'altra parte della risposta):

  1. dividere il grafico nelle parti di dimensione O (log n / (log log n)) con qualche restrizione.
  2. Supponiamo che ciascuna delle parti menzionate sia un nodo nel nuovo grafo G ', quindi calcola SSSP per G' nel tempo O (| G '| * log | G' |) ==> qui perché | G '| = O (| G | * log log n / log n) possiamo vedere il fattore (log log n).
  3. Calcola SSSP per ogni parte: di nuovo perché abbiamo la parte O (| G '|) e possiamo calcolare SSSP per tutte le parti nel tempo | n / logn | * | log n / log logn * log (logn / log log n).
  4. aggiornare i pesi, questa parte può essere eseguita in O (n). per maggiori dettagli queste note di lezione sono buone.

Ma il punto è che qui scegliamo che la divisione sia di dimensione O (log n / (log log n)). Se scegliamo altre divisioni come O (log n / (log log n) ^ 2) che potrebbe essere più veloce e portare un altro risultato. Voglio dire, in molti casi (come in algoritmi di approssimazione o algoritmi randomizzati, o algoritmi come SSSP come sopra), quando iteriamo su qualcosa (sottoproblemi, possibili soluzioni, ...), scegliamo il numero di iterazione corrispondente al commercio di quello abbiamo (tempo / spazio / complessità dell'algoritmo / fattore costante dell'algoritmo, ...). Quindi forse vediamo cose più complicate di "log log n" in algoritmi di lavoro reali.

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.