Qual'è la differenza tra bottom-up e top-down?


177

L' approccio dal basso verso l'alto (alla programmazione dinamica) consiste nel considerare prima i sottoproblemi "più piccoli", quindi risolvere i sottoproblemi più grandi usando la soluzione ai problemi più piccoli.

Il top-down consiste nel risolvere il problema in modo "naturale" e verificare se in precedenza è stata calcolata la soluzione al sottoproblema.

Sono un po 'confuso. Qual è la differenza tra questi due?


Risposte:


247

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 fibfunzione 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

3
@ coder000001: per esempi di Python, puoi cercare su Google python memoization decorator; alcune lingue ti permetteranno di scrivere una macro o un codice che incapsuli il modello di memoization. Il modello di memoization non è altro che "piuttosto che chiamare la funzione, cercare il valore da una cache (se il valore non è presente, calcolarlo e aggiungerlo prima alla cache)".
ninjagecko,

16
Non vedo nessuno menzionarlo, ma penso che un altro vantaggio di Top down sia che creerai scarsamente solo la tabella di ricerca / cache. (ad esempio, inserisci i valori in cui ne hai effettivamente bisogno). Quindi questo potrebbe essere il vantaggio oltre alla semplice codifica. In altre parole, dall'alto in basso potresti risparmiare il tempo di esecuzione effettivo poiché non calcoli tutto (potresti avere un tempo di esecuzione tremendamente migliore ma lo stesso tempo di esecuzione asintotico). Tuttavia richiede memoria aggiuntiva per mantenere i frame di stack aggiuntivi (di nuovo, il consumo di memoria "può" (solo può) raddoppiare ma asintoticamente è lo stesso.
Informato

2
Ho l'impressione che gli approcci top-down che memorizzano nella cache le soluzioni ai sottoproblemi sovrapposti sia una tecnica chiamata memoization . Una tecnica dal basso verso l'alto che riempie una tabella ed evita anche di ricalcolare i sottoproblemi sovrapposti viene definita tabulazione . Queste tecniche possono essere impiegate quando si utilizza la programmazione dinamica , che si riferisce alla risoluzione di sottoproblemi per risolvere un problema molto più grande. Ciò sembra in contraddizione con questa risposta, in cui questa risposta utilizza la programmazione dinamica anziché la tabulazione in molti luoghi. Chi ha ragione?
Sammaron,

1
@Sammaron: hmm, fai un buon punto. Forse avrei dovuto controllare la mia fonte su Wikipedia, che non trovo. Dopo aver verificato un po 'cstheory.stackexchange, ora sono d'accordo che "bottom-up" implicherebbe che il bottom è noto in anticipo (tabulazione), e "top-down" è assumere la soluzione ai sottoproblemi / subtrees. All'epoca ho trovato il termine ambiguo e ho interpretato le frasi nella doppia vista ("dal basso verso l'alto" si assume una soluzione ai sottoproblemi e si memorizza, "dall'alto verso il basso" si conoscono i sottoproblemi di cui si parla e si può tabulare). Tenterò di risolverlo in una modifica.
ninjagecko,

1
@mgiuffrida: lo spazio dello stack viene talvolta trattato in modo diverso a seconda del linguaggio di programmazione. Ad esempio in Python, il tentativo di eseguire una fib ricorsiva memoizzata non funzionerà fib(513). La terminologia sovraccarica che sento si sta mettendo in mezzo qui. 1) Puoi sempre buttare via i sottoproblemi che non ti servono più. 2) Puoi sempre evitare di calcolare i sottoproblemi che non ti servono. 3) 1 e 2 potrebbero essere molto più difficili da codificare senza una struttura dati esplicita in cui archiviare i sottoproblemi, O, più difficile se il flusso di controllo deve intrecciarsi tra chiamate di funzione (potrebbe essere necessario stato o continuazioni).
ninjagecko,

76

La DP top-down e bottom-up sono due modi diversi di risolvere gli stessi problemi. Considera una soluzione di programmazione memoized (top down) vs dynamic (bottom up) per il calcolo dei numeri di fibonacci.

fib_cache = {}

def memo_fib(n):
  global fib_cache
  if n == 0 or n == 1:
     return 1
  if n in fib_cache:
     return fib_cache[n]
  ret = memo_fib(n - 1) + memo_fib(n - 2)
  fib_cache[n] = ret
  return ret

def dp_fib(n):
   partial_answers = [1, 1]
   while len(partial_answers) <= n:
     partial_answers.append(partial_answers[-1] + partial_answers[-2])
   return partial_answers[n]

print memo_fib(5), dp_fib(5)

Personalmente trovo la memorizzazione molto più naturale. Puoi assumere una funzione ricorsiva e memorizzarla mediante un processo meccanico (prima ricerca risposta nella cache e restituzione se possibile, altrimenti calcola ricorsivamente e poi prima di tornare, salvi il calcolo nella cache per un uso futuro), mentre fai il bottom up la programmazione dinamica richiede di codificare un ordine in cui vengono calcolate le soluzioni, in modo tale che nessun "grande problema" venga calcolato prima del problema minore da cui dipende.


1
Ah, ora vedo cosa significano "top-down" e "bottom-up"; si tratta infatti solo di memoization vs DP. E pensare che sono stato io a modificare la domanda per menzionare DP nel titolo ...
ninjagecko,

qual è il tempo di esecuzione della normale fibra ricorsiva fib v / s memorizzata?
Siddhartha,

esponenziale (2 ^ n) per coz normale è un albero di ricorsione credo.
Siddhartha,

1
Sì, è lineare! Ho tirato fuori l'albero di ricorsione e ho visto quali chiamate potevano essere evitate e ho realizzato che le chiamate memo_fib (n - 2) sarebbero state evitate dopo la prima chiamata, e quindi tutti i rami giusti dell'albero di ricorsione sarebbero stati tagliati e ridurrò a lineare.
Siddhartha,

1
Poiché DP implica essenzialmente la creazione di una tabella dei risultati in cui ogni risultato viene calcolato al massimo una volta, un modo semplice per visualizzare il tempo di esecuzione di un algoritmo DP è vedere quanto è grande la tabella. In questo caso, ha dimensioni n (un risultato per valore di input) quindi O (n). In altri casi, potrebbe essere una matrice n ^ 2, risultante in O (n ^ 2), ecc.
Johnson Wong

22

Una caratteristica chiave della programmazione dinamica è la presenza di sottoproblemi sovrapposti . Cioè, il problema che si sta tentando di risolvere può essere suddiviso in sottoproblemi e molti di questi sottoproblemi condividono sottoproblemi. È come "Dividi e conquista", ma finisci per fare la stessa cosa molte, molte volte. Un esempio che ho usato dal 2003 quando ho insegnato o spiegato queste questioni: puoi calcolare i numeri di Fibonacci in modo ricorsivo.

def fib(n):
  if n < 2:
    return n
  return fib(n-1) + fib(n-2)

Usa la tua lingua preferita e prova a eseguirla fib(50). Ci vorrà molto, molto tempo. Circa il tempo fib(50)stesso! Tuttavia, viene svolto molto lavoro inutile. fib(50)chiamerà fib(49)e fib(48), ma poi entrambi finiranno per chiamare fib(47), anche se il valore è lo stesso. In effetti, fib(47)verrà calcolato tre volte: da una chiamata diretta da fib(49), da una chiamata diretta da fib(48), e anche da una chiamata diretta da un'altra fib(48), quella che è stata generata dal calcolo di fib(49)... Quindi vedete, abbiamo sottoproblemi sovrapposti .

Grandi notizie: non è necessario calcolare più volte lo stesso valore. Dopo averlo calcolato una volta, memorizza nella cache il risultato e la volta successiva usa il valore memorizzato nella cache! Questa è l'essenza della programmazione dinamica. Puoi chiamarlo "top-down", "memoization" o come vuoi. Questo approccio è molto intuitivo e molto facile da implementare. Scrivi prima una soluzione ricorsiva, testala su piccoli test, aggiungi memoizzazione (memorizzazione nella cache di valori già calcolati) e --- bingo! --- hai fatto.

Di solito è anche possibile scrivere un programma iterativo equivalente che funziona dal basso verso l'alto, senza ricorsione. In questo caso questo sarebbe l'approccio più naturale: passa da 1 a 50 calcolando tutti i numeri di Fibonacci mentre procedi.

fib[0] = 0
fib[1] = 1
for i in range(48):
  fib[i+2] = fib[i] + fib[i+1]

In qualsiasi scenario interessante la soluzione dal basso verso l'alto è generalmente più difficile da capire. Tuttavia, una volta che lo capisci, di solito otterrai un'immagine molto più chiara di come funziona l'algoritmo. In pratica, quando risolvo problemi non banali, raccomando prima di scrivere l'approccio top-down e testarlo su piccoli esempi. Quindi scrivi la soluzione dal basso verso l'alto e confronta i due per assicurarti di ottenere la stessa cosa. Idealmente, confronta automaticamente le due soluzioni. Scrivi una piccola routine che genererebbe molti test, idealmente - tuttopiccoli test fino a determinate dimensioni --- e convalidano che entrambe le soluzioni danno lo stesso risultato. Dopodiché usa la soluzione bottom-up in produzione, ma mantieni il codice top-bottom, commentato. Ciò renderà più facile per gli altri sviluppatori capire cosa stai facendo: il codice bottom-up può essere abbastanza incomprensibile, anche se lo hai scritto e anche se sai esattamente cosa stai facendo.

In molte applicazioni l'approccio dal basso verso l'alto è leggermente più veloce a causa del sovraccarico di chiamate ricorsive. Lo overflow dello stack può anche essere un problema in alcuni problemi e notare che questo può dipendere molto dai dati di input. In alcuni casi potresti non essere in grado di scrivere un test causando un overflow dello stack se non capisci abbastanza bene la programmazione dinamica, ma un giorno potrebbe ancora succedere.

Ora, ci sono problemi in cui l'approccio top-down è l'unica soluzione possibile perché lo spazio del problema è così grande che non è possibile risolvere tutti i sottoproblemi. Tuttavia, il "caching" funziona ancora in un tempo ragionevole perché il tuo input ha bisogno solo di una frazione dei sottoproblemi per essere risolto --- ma è troppo complicato definire esplicitamente quali sottoproblemi devi risolvere, e quindi scrivere un bottom- soluzione. D'altra parte, ci sono situazioni in cui sai che dovrai risolvere tutti i sottoproblemi. In questo caso, andare avanti e utilizzare dal basso verso l'alto.

Personalmente userei top-bottom per l'ottimizzazione del paragrafo, ovvero il problema dell'ottimizzazione dell'involucro di Word (cercare gli algoritmi di interruzione di linea Knuth-Plass; almeno TeX lo utilizza e alcuni software di Adobe Systems utilizzano un approccio simile). Vorrei utilizzare dal basso verso l'alto per la trasformata di Fourier veloce .


Ciao!!! Voglio determinare se le seguenti proposizioni sono giuste. - Per un algoritmo di programmazione dinamica, il calcolo di tutti i valori con bottom-up è asintoticamente più veloce dell'uso della ricorsione e della memoizzazione. - Il tempo di un algoritmo dinamico è sempre Ο (Ρ) dove Ρ è il numero di sottoproblemi. - Ogni problema in NP può essere risolto in tempo esponenziale.
Mary Star,

Cosa potrei dire delle proposizioni di cui sopra? Hai un'idea? @osa
Mary Star,

@evinda, (1) ha sempre torto. È uguale o asintoticamente più lento (quando non hai bisogno di tutti i sottoproblemi, la ricorsione può essere più veloce). (2) è giusto solo se riesci a risolvere ogni sottoproblema in O (1). (3) è un po 'giusto. Ogni problema in NP può essere risolto in tempo polinomiale su una macchina non deterministica (come un computer quantistico, che può fare più cose contemporaneamente: avere la sua torta e contemporaneamente mangiarla e tracciare entrambi i risultati). Quindi, in un certo senso, ogni problema in NP può essere risolto in tempo esponenziale su un normale computer. Nota: tutto in P è anche in NP. Ad esempio aggiungendo due numeri interi
osa

19

Prendiamo la serie di fibonacci come esempio

1,1,2,3,5,8,13,21....

first number: 1
Second number: 1
Third Number: 2

Un altro modo per dirlo,

Bottom(first) number: 1
Top (Eighth) number on the given sequence: 21

In caso di primi cinque numeri di fibonacci

Bottom(first) number :1
Top (fifth) number: 5 

Ora diamo un'occhiata all'algoritmo ricorsivo della serie Fibonacci come esempio

public int rcursive(int n) {
    if ((n == 1) || (n == 2)) {
        return 1;
    } else {
        return rcursive(n - 1) + rcursive(n - 2);
    }
}

Ora se eseguiamo questo programma con i seguenti comandi

rcursive(5);

se esaminiamo attentamente l'algoritmo, per generare il quinto numero sono necessari il 3 ° e il 4 ° numero. Quindi la mia ricorsione in realtà inizia dall'alto (5) e poi arriva fino ai numeri inferiori / inferiori. Questo approccio è in realtà un approccio dall'alto verso il basso.

Per evitare di fare lo stesso calcolo più volte, utilizziamo le tecniche di programmazione dinamica. Memorizziamo il valore precedentemente calcolato e lo riutilizziamo. Questa tecnica si chiama memoization. Ci sono molti altri nella programmazione dinamica oltre alla memoizzazione che non è necessaria per discutere del problema attuale.

Dall'alto al basso

Consente di riscrivere il nostro algoritmo originale e aggiungere tecniche memorizzate.

public int memoized(int n, int[] memo) {
    if (n <= 2) {
        return 1;
    } else if (memo[n] != -1) {
        return memo[n];
    } else {
        memo[n] = memoized(n - 1, memo) + memoized(n - 2, memo);
    }
    return memo[n];
}

Ed eseguiamo questo metodo come segue

   int n = 5;
    int[] memo = new int[n + 1];
    Arrays.fill(memo, -1);
    memoized(n, memo);

Questa soluzione è ancora top-down poiché l'algoritmo inizia dal valore più alto e scende in fondo ogni passaggio per ottenere il nostro valore più alto.

Dal basso verso l'alto

Ma, la domanda è: possiamo iniziare dal basso, come dal primo numero di fibonacci, quindi procedere verso l'alto. Consente di riscriverlo utilizzando queste tecniche,

public int dp(int n) {
    int[] output = new int[n + 1];
    output[1] = 1;
    output[2] = 1;
    for (int i = 3; i <= n; i++) {
        output[i] = output[i - 1] + output[i - 2];
    }
    return output[n];
}

Ora, se esaminiamo questo algoritmo, in realtà inizia da valori più bassi, quindi va in cima. Se ho bisogno del 5 ° numero di fibonacci, sto effettivamente calcolando il 1 °, quindi il secondo e il terzo fino al 5 ° numero. Queste tecniche in realtà chiamate tecniche dal basso verso l'alto.

Ultimi due, gli algoritmi soddisfano i requisiti di programmazione dinamica. Ma uno è dall'alto in basso e un altro è dal basso verso l'alto. Entrambi gli algoritmi hanno una complessità spaziale e temporale simile.


Possiamo dire che l'approccio dal basso verso l'alto è spesso implementato in modo non ricorsivo?
Lewis Chan,

No, puoi convertire qualsiasi logica di loop in ricorsione
Ashvin Sharma,

3

La programmazione dinamica è spesso chiamata Memoization!

1. La modellazione è la tecnica top-down (inizia a risolvere il problema dato scomponendolo) e la programmazione dinamica è una tecnica bottom-up (inizia a risolvere dal banale sotto-problema, verso il problema dato)

2.DP trova la soluzione partendo dal caso o dai casi base e procede verso l'alto. DP risolve tutti i problemi secondari, perché lo fa dal basso verso l'alto

A differenza della Memoization, che risolve solo i sotto-problemi necessari

  1. DP ha il potenziale per trasformare soluzioni di forza bruta a tempo esponenziale in algoritmi a tempo polinomiale.

  2. DP può essere molto più efficiente perché è iterativo

Al contrario, la Memoization deve pagare le spese generali (spesso significative) dovute alla ricorsione.

Per essere più semplici, Memoization utilizza l'approccio top-down per risolvere il problema, ovvero inizia con il problema principale (principale), quindi lo suddivide in sotto-problemi e risolve questi sotto-problemi in modo simile. In questo approccio lo stesso sotto-problema può verificarsi più volte e consumare più cicli della CPU, quindi aumentare la complessità del tempo. Mentre nella programmazione dinamica lo stesso sotto-problema non verrà risolto più volte ma il risultato precedente verrà utilizzato per ottimizzare la soluzione.


4
non è vero, la memoization utilizza una cache che ti aiuterà a salvare la complessità temporale allo stesso di DP
Informato

3

Semplicemente dicendo che l'approccio top down usa la ricorsione per chiamare ripetutamente i problemi Sub
dove l'approccio bottom up usa il singolo senza chiamare nessuno e quindi è più efficiente.


1

Di seguito è riportata la soluzione basata su DP per il problema Modifica distanza che è dall'alto verso il basso. Spero che possa anche aiutare a comprendere il mondo della programmazione dinamica:

public int minDistance(String word1, String word2) {//Standard dynamic programming puzzle.
         int m = word2.length();
            int n = word1.length();


     if(m == 0) // Cannot miss the corner cases !
                return n;
        if(n == 0)
            return m;
        int[][] DP = new int[n + 1][m + 1];

        for(int j =1 ; j <= m; j++) {
            DP[0][j] = j;
        }
        for(int i =1 ; i <= n; i++) {
            DP[i][0] = i;
        }

        for(int i =1 ; i <= n; i++) {
            for(int j =1 ; j <= m; j++) {
                if(word1.charAt(i - 1) == word2.charAt(j - 1))
                    DP[i][j] = DP[i-1][j-1];
                else
                DP[i][j] = Math.min(Math.min(DP[i-1][j], DP[i][j-1]), DP[i-1][j-1]) + 1; // Main idea is this.
            }
        }

        return DP[n][m];
}

Puoi pensare alla sua implementazione ricorsiva a casa tua. È abbastanza bello e stimolante se non hai risolto qualcosa di simile prima.


1

Top-Down : tenere traccia del valore calcolato fino ad ora e restituire il risultato quando viene soddisfatta la condizione di base.

int n = 5;
fibTopDown(1, 1, 2, n);

private int fibTopDown(int i, int j, int count, int n) {
    if (count > n) return 1;
    if (count == n) return i + j;
    return fibTopDown(j, i + j, count + 1, n);
}

Bottom-Up : il risultato attuale dipende dal risultato del suo sotto-problema.

int n = 5;
fibBottomUp(n);

private int fibBottomUp(int n) {
    if (n <= 1) return 1;
    return fibBottomUp(n - 1) + fibBottomUp(n - 2);
}
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.