Questo è un bel problema di programmazione dinamica.
Il modo di pensare di programmazione dinamica è quella di porsi due domande:
- Esiste una versione banale di questo problema se riduco uno (o più) delle variabili da zero (o simili)?
- Esiste un modo semplice per calcolare la risposta a un problema di dimensione
n+1
se conosco le risposte a tutti i problemi della dimensione n
? (Qui, "dimensione" è specifica del problema, e devi trovare la nozione giusta di dimensioni che aiuti con il problema in mano.)
Per questo problema, quale sarebbe una versione banale? Bene, supponiamo che il numero di giorni sia stato 1. Quindi sarebbe stato facile: ho fermato la macchina e ho trattato il maggior numero di pazienti possibile. Non ha senso fare altro.
Ora, se consideriamo il numero di giorni rimasti come nozione di dimensione, otteniamo anche una risposta alla seconda domanda. Supponiamo di sapere tutte le risposte a tutti i problemi in cui ci sono n
giorni rimanenti. Scriviamo maxTreat(days, running)
per il numero massimo che potremmo trattare se ci fossero days
giorni rimanenti e se la macchina fosse stata inizialmente in esecuzione per running
giorni.
Ora ci sono n+1
giorni rimanenti; e la macchina ha funzionato finora per i giorni k
. Abbiamo due opzioni: (1) fermare la macchina; (2) non fermarlo.Se interrompiamo la macchina, possiamo trattare alcuni pazienti oggi (possiamo calcolare il numero basato su k
), e successivamente possiamo trattare i pazienti maxTreat(n, 1)
, perché ci sono ancora n
giorni, e domani la macchina sarà di nuovo in esecuzione per un solo giorno. Se non fermiamo la macchina, non possiamo trattare chiunque oggi, ma da allora in poi saremo in grado di trattare i pazienti maxTreat(n,k+1)
, perché domani la macchina sarà in corso da k+1
giorni.
Vi lascio a elaborare i dettagli precisi, ma per risolverlo in modo efficiente, creiamo una matrice multidimensionale, basata sul numero di giorni rimasti e il numero di giorni per i quali la macchina ha funzionato finora. Quindi, iteriamo attraverso l'array, risolvendo tutti i possibili problemi, a partire dal banale (un giorno lasciato) e lavorando all'indietro (due giorni, poi tre giorni e così via). In ogni fase, il problema che stiamo risolvendo è o banale (quindi possiamo semplicemente scrivere la risposta in), o qualcosa che possiamo calcolare dalle voci che abbiamo scritto nell'array nel passaggio precedente.
La cosa veramente interessante della programmazione dinamica è che stiamo creando una cache di tutti i risultati. Quindi, per problemi in cui un approccio ricorsivo finirà per dover calcolare più volte la risposta a un sotto-problema, con la programmazione dinamica non risolviamo mai un sotto-problema più di una volta.
Ulteriori commenti ora che ho visto l'implementazione:
Per uno, io non sono troppo sorpreso che inizia a rallentare quando si colpisce 10.000 o giù di lì. L'algoritmo è O(n^2)
, perché a ogni iterazione è necessario riempire l'array con un massimo di n
voci prima di passare al livello successivo. Sono abbastanza certo che O(n^2)
è la migliore complessità asintotica che otterrai per questo puzzle, però.
Se si desidera accelerare ulteriormente, è possibile osservare un approccio dall'alto verso il basso. Attualmente stai facendo una programmazione dinamica bottom-up: risolvendo i casi di dimensione 0, quindi di dimensione 1 e così via. Ma puoi anche farlo al contrario. Essenzialmente questo è lo stesso algoritmo come se si stesse scrivendo una soluzione ricorsiva orribilmente inefficiente che calcola le soluzioni ai sotto-problemi al volo, eccetto che si memorizza un risultato in cache ogni volta che lo si calcola. Quindi assomiglia a questo:
- Imposta il tuo array bidimensionale per contenere soluzioni a problemi secondari. Pre-riempirlo con -1 per ogni caso. Un valore di -1 indicherà che non hai ancora risolto il problema secondario.
- Scrivere una routine che risolva
maxTreat(days, running)
in termini di risposte a problemi secondari al livello successivo. Quando vuoi le risposte ai sotto-problemi, guarda nell'array. Se c'è un -1 lì, non lo hai ancora risolto, quindi lo risolvi in modo ricorsivo e poi metti la risposta nell'array. Se c'è qualcosa di diverso da -1, puoi semplicemente usare il valore che trovi lì, perché lo hai già calcolato. (È inoltre possibile utilizzare un anziché l'array multidimensionale.)
Questo è meglio in un modo e peggio in un altro. È peggio perché hai overheads associati alla ricorsione e perché alla fine finirai con lo stack con le chiamate ricorsive. Potrebbe essere necessario aumentare la dimensione dello stack con un parametro della riga di comando sulla JVM.
Ma è meglio in un aspetto fondamentale: non si calcolano le risposte a tutti i sotto-problemi, ma solo quelli a cui è necessario conoscere le risposte. Per alcuni problemi, questa è una grande differenza e per alcuni non lo è. È difficile avere l'intuizione giusta, ma penso che potrebbe fare una grande differenza qui. Dopotutto, ogni risposta dipende solo da due sotto-problemi della riga precedente.
La soluzione definitiva (non provarlo fino a quando non si ottiene prima quello ricorsivo dall'alto verso il basso!) È farlo dall'alto verso il basso ma senza ricorsione. Ciò eviterà il problema dello spazio di stack. Per fare questo, crei una serie di problemi secondari (usa uno ArrayDeque
) che devono essere risolti e continui a portarli fuori dalla coda finché non ne rimane nessuno. La prima cosa è spingere in pila il grosso problema per il quale è necessaria una soluzione. Ora, si espellono in modo iterativo i problemi dallo stack fino a quando non è vuoto. Pop one off, e chiamalo P
. Poi:
- guardare nel tuo array o
HashMap
per vedere se P
è stato risolto. In tal caso, restituire la risposta.
- In caso contrario, verificare se i problemi secondari per
P
sono già stati risolti. In tal caso, è possibile risolvere P
e memorizzare la risposta nella cache. Se lo stack è vuoto, hai risolto il problema finale e hai generato la risposta per P
.
- Se i problemi secondari non sono stati tutti risolti, premere nuovamente
P
nello stack. Quindi spingere uno dei problemi secondari di P
che non sono ancora stati risolti nello stack.
Quello che succederà mentre si procede è che lo stack crescerà inizialmente quando si spinge il problema principale, i suoi sotto-problemi e quindi i suoi sotto-problemi, nello stack. Quindi inizierai a risolvere le istanze più piccole e a mettere i risultati nella cache, finché alla fine avrai tutto ciò che ti serve per risolvere il problema principale.
Non utilizza molto meno memoria rispetto all'approccio top-down ricorsivo, ma utilizza lo spazio heap anziché lo spazio JVM dello stack e ciò significa che si adatta meglio perché lo stack JVM è molto più piccolo dell'heap.
Questo è abbastanza difficile, però. Per lo meno, mantieni la soluzione di lavoro prima di iniziare a codificare la versione più difficile!
È possibile calcolare tutti i risultati possibili e decidere quale ha il numero più alto di pazienti trattati. – Smutje
E i pazienti, se non vengono trattati il giorno in cui fanno il check-in, muoiono? In tal caso, significa che devono essere trattati il giorno del check-in, altrimenti è necessario tenere traccia del numero di pazienti non trattati. – Rerito
Esiste un vincolo/una scadenza che un paziente non può più essere trattato? – Hannes