rev4: Un commento molto eloquente da parte dell'utente Sammaron ha notato che, forse, questa risposta precedentemente confusa top-down e bottom-up. Mentre originariamente questa risposta (rev3) e altre risposte dicevano che "dal basso verso l'alto è la memorizzazione" ("assume i sottoproblemi"), potrebbe essere l'inverso (cioè "dall'alto verso il basso" potrebbe essere "assumere i sottoproblemi" e " bottom-up "può essere" componi i sottoproblemi "). In precedenza, ho letto che la memoizzazione è un diverso tipo di programmazione dinamica rispetto a un sottotipo di programmazione dinamica. Stavo citando quel punto di vista nonostante non mi abbonassi ad esso. Ho riscritto questa risposta per essere agnostico della terminologia fino a quando nella letteratura non si trovano riferimenti adeguati. Ho anche convertito questa risposta in un wiki della comunità. Preferisci le fonti accademiche. Lista di referenze:} {Letteratura: 5 }
Ricapitolare
La programmazione dinamica consiste nell'ordinare i tuoi calcoli in modo da evitare di ricalcolare il lavoro duplicato. Hai un problema principale (la radice dell'albero dei tuoi sottoproblemi) e dei sottoproblemi (sottotipi). I sottoproblemi si ripetono e si sovrappongono in genere .
Ad esempio, considera il tuo esempio preferito di Fibonnaci. Questo è l'albero pieno dei sottoproblemi, se abbiamo fatto un'ingenua chiamata ricorsiva:
TOP of the tree
fib(4)
fib(3)...................... + fib(2)
fib(2)......... + fib(1) fib(1)........... + fib(0)
fib(1) + fib(0) fib(1) fib(1) fib(0)
fib(1) fib(0)
BOTTOM of the tree
(In alcuni altri rari problemi, questo albero potrebbe essere infinito in alcuni rami, rappresentando la non terminazione, e quindi il fondo dell'albero potrebbe essere infinitamente grande. Inoltre, in alcuni problemi potresti non sapere come appare l'intero albero davanti a tempo. Pertanto, potrebbe essere necessario una strategia / algoritmo per decidere quali sottoproblemi rivelare.)
Memoization, Tabulazione
Esistono almeno due tecniche principali di programmazione dinamica che non si escludono a vicenda:
Memoization - Questo è un approccio laissez-faire: supponi di aver già calcolato tutti i sottoproblemi e di non avere idea di quale sia l'ordine di valutazione ottimale. In genere, esegui una chiamata ricorsiva (o qualche equivalente iterativo) dalla radice e speri di avvicinarti all'ordine di valutazione ottimale o ottenere una prova che ti aiuterà a arrivare all'ordine di valutazione ottimale. Assicureresti che la chiamata ricorsiva non ricalcoli mai un sottoproblema perché memorizzi nella cache i risultati e quindi i sottoalberi duplicati non vengono ricalcolati.
- esempio: se stai calcolando la sequenza di Fibonacci
fib(100)
, chiameresti semplicemente questo, e chiamerebbe fib(100)=fib(99)+fib(98)
, che chiamerebbe fib(99)=fib(98)+fib(97)
, ... ecc ..., che chiamerebbe fib(2)=fib(1)+fib(0)=1+0=1
. Quindi alla fine si risolverebbe fib(3)=fib(2)+fib(1)
, ma non è necessario ricalcolarlo fib(2)
, perché lo abbiamo memorizzato nella cache.
- Questo inizia nella parte superiore dell'albero e valuta i sottoproblemi dalle foglie / sottotitoli verso la radice.
Tabulazione - Puoi anche pensare alla programmazione dinamica come a un algoritmo di "riempimento della tabella" (sebbene di solito multidimensionale, questa "tabella" può avere una geometria non euclidea in casi molto rari *). Questo è come la memoizzazione ma più attivo e comporta un ulteriore passaggio: devi scegliere in anticipo l'ordine esatto in cui eseguirai i tuoi calcoli. Ciò non dovrebbe implicare che l'ordine debba essere statico, ma che tu abbia molta più flessibilità della memoizzazione.
- Esempio: Se si esegue Fibonacci, è possibile scegliere di calcolare i numeri in questo ordine:
fib(2)
, fib(3)
, fib(4)
... cache ogni valore in modo da poter calcolare le successive più facilmente. Puoi anche pensarlo come riempire una tabella (un'altra forma di memorizzazione nella cache).
- Personalmente non sento molto la parola "tabulazione", ma è un termine molto decente. Alcune persone considerano questa "programmazione dinamica".
- Prima di eseguire l'algoritmo, il programmatore considera l'intero albero, quindi scrive un algoritmo per valutare i sottoproblemi in un ordine particolare verso la radice, generalmente compilando una tabella.
- * Nota: a volte il 'tavolo' non è un tavolo rettangolare con connettività simile a una griglia, di per sé. Piuttosto, potrebbe avere una struttura più complicata, come un albero o una struttura specifica per il dominio del problema (ad es. Città a distanza di volo su una mappa), o persino un diagramma a traliccio che, sebbene a griglia, non ha una struttura di connettività up-down-left-right, ecc. Ad esempio, user3290797 ha collegato un esempio di programmazione dinamica di ricerca dell'insieme indipendente massimo in un albero , che corrisponde al riempimento degli spazi vuoti in un albero.
(Al massimo, in un paradigma di "programmazione dinamica", direi che il programmatore considera l'intero albero, quindiscrive un algoritmo che implementa una strategia per la valutazione dei sottoproblemi in grado di ottimizzare le proprietà desiderate (di solito una combinazione di complessità temporale e complessità spaziale). La tua strategia deve iniziare da qualche parte, con qualche sottoproblema particolare, e forse potrebbe adattarsi in base ai risultati di tali valutazioni. Nel senso generale della "programmazione dinamica", si potrebbe provare a memorizzare nella cache questi sottoproblemi e, più in generale, provare a evitare di rivisitare i sottoproblemi con una sottile distinzione, forse come nel caso dei grafici in varie strutture di dati. Molto spesso, queste strutture di dati sono al loro centro come matrici o tabelle. Le soluzioni ai sottoproblemi possono essere eliminate se non ne abbiamo più bisogno.)
[In precedenza, questa risposta faceva una dichiarazione sulla terminologia top-down vs bottom-up; ci sono chiaramente due approcci principali chiamati Memoization and Tabulation che potrebbero essere in biiezione con quei termini (anche se non del tutto). Il termine generale che la maggior parte delle persone usa è ancora "Programmazione dinamica" e alcune persone dicono "Memoizzazione" per riferirsi a quel particolare sottotipo di "Programmazione dinamica". Questa risposta rifiuta di dire quale sia dall'alto verso il basso e dal basso verso l'alto fino a quando la comunità non troverà riferimenti adeguati nei documenti accademici. In definitiva, è importante capire la distinzione piuttosto che la terminologia.]
Pro e contro
Facilità di codifica
La memoizzazione è molto semplice da codificare (generalmente puoi * scrivere un'annotazione "memoizer" o una funzione wrapper che lo fa automaticamente per te) e dovrebbe essere la tua prima linea di approccio. L'aspetto negativo della tabulazione è che devi inventare un ordine.
* (questo è in realtà facile solo se si sta scrivendo la funzione da soli e / o si sta codificando in un linguaggio di programmazione impuro / non funzionale ... ad esempio se qualcuno ha già scritto una fib
funzione precompilata , effettua necessariamente chiamate ricorsive a se stesso e non puoi memorizzare magicamente la funzione senza assicurarti che quelle chiamate ricorsive chiamino la tua nuova funzione memorizzata (e non la funzione originale non modificata))
ricorsività
Si noti che sia il top-down che il bottom-up possono essere implementati con la ricorsione o il riempimento iterativo della tabella, anche se potrebbe non essere naturale.
Preoccupazioni pratiche
Con la memoizzazione, se l'albero è molto profondo (ad es. fib(10^6)
), Si esaurirà lo spazio dello stack, poiché ogni calcolo ritardato deve essere messo nello stack e ne avrai 10 ^ 6.
ottimalità
Entrambi gli approcci potrebbero non essere ottimali nel tempo se l'ordine in cui si verificano (o si tenta di) visitare i sottoproblemi non è ottimale, in particolare se esiste più di un modo per calcolare un sottoproblema (normalmente la memorizzazione nella cache risolverebbe questo problema, ma è teoricamente possibile che la memorizzazione nella cache possa non in alcuni casi esotici). La memoizzazione di solito aumenta la complessità del tempo nella complessità dello spazio (ad es. Con la tabulazione hai più libertà di buttare via i calcoli, come usare la tabulazione con Fib ti consente di usare lo spazio O (1), ma la memoizzazione con Fib usa O (N) spazio pila).
Ottimizzazioni avanzate
Se stai anche riscontrando problemi estremamente complicati, potresti non avere altra scelta che fare la tabulazione (o almeno assumere un ruolo più attivo nel guidare la memoizzazione dove vuoi che vada). Inoltre, se ti trovi in una situazione in cui l'ottimizzazione è assolutamente critica e devi ottimizzare, la tabulazione ti consentirà di fare ottimizzazioni che la memoizzazione non ti farebbe altrimenti in modo sano. Secondo la mia modesta opinione, nella normale ingegneria del software, nessuno di questi due casi si presenta mai, quindi userei solo la memoizzazione ("una funzione che memorizza le risposte nella cache") a meno che qualcosa (come lo spazio dello stack) renda necessaria la tabulazione ... sebbene tecnicamente per evitare uno scoppio dello stack puoi 1) aumentare il limite di dimensioni dello stack in lingue che lo consentono, o 2) consumare un fattore costante di lavoro extra per virtualizzare il tuo stack (ick),
Esempi più complicati
Qui elenchiamo esempi di particolare interesse, che non sono solo problemi generali di DP, ma distinguono in modo interessante la memoizzazione e la tabulazione. Ad esempio, una formulazione potrebbe essere molto più semplice dell'altra o potrebbe esserci un'ottimizzazione che in sostanza richiede tabulazione:
- l'algoritmo per calcolare la modifica della distanza [ 4 ], interessante come esempio non banale di un algoritmo di riempimento di tabelle bidimensionale