Programmazione dinamica con un gran numero di sottoproblemi


11

Programmazione dinamica con un gran numero di sottoproblemi. Quindi sto cercando di risolvere questo problema da Interview Street:

Grid Walking (segna 50 punti)
Ti trovi in ​​una griglia dimensionale in posizione . Le dimensioni della griglia sono ). In un passo, puoi camminare un passo avanti o indietro in una qualsiasi delle dimensioni. (Quindi ci sono sempre possibili mosse diverse). In quanti modi puoi fare passi in modo tale da non lasciare la griglia in nessun punto? Si lascia la griglia se per qualsiasi , o o .N(x1,x2,,xN)(D1,D2,,DNN2NMxixi0xi>Di

Il mio primo tentativo è stato questa soluzione ricorsiva memorizzata:

def number_of_ways(steps, starting_point):
    global n, dimensions, mem
    #print steps, starting_point
    if (steps, tuple(starting_point)) in mem:
        return mem[(steps, tuple(starting_point))]
    val = 0
    if steps == 0:
        val = 1
    else:
        for i in range(0, n):
            tuple_copy = starting_point[:]
            tuple_copy[i] += 1
            if tuple_copy[i] <= dimensions[i]:
                val += number_of_ways(steps - 1, tuple_copy)
            tuple_copy = starting_point[:]
            tuple_copy[i] -= 1
            if tuple_copy[i] > 0:
                val += number_of_ways(steps - 1, tuple_copy)
    mem[(steps, tuple(starting_point))] = val
    return val

Grande sorpresa: fallisce per un gran numero di passaggi e / o dimensioni a causa della mancanza di memoria.

Quindi il prossimo passo è migliorare la mia soluzione usando la programmazione dinamica. Ma prima di iniziare, sto riscontrando un grave problema con l'approccio. L'argomento starting_pointè una -tupla, dove è grande quanto . Quindi, in effetti, la funzione potrebbe essere con .n 10 1 x i100nn10number_of_ways(steps, x1, x2, x3, ... x10)1xi100

I problemi di programmazione dinamica che ho visto nei libri di testo hanno quasi tutti variabili twp, quindi è necessaria solo una matrice bidimensionale. In questo caso, sarebbe necessaria una matrice tridimensionale. Quindi celle in totale.10010

Con le matrici 2D nella programmazione dinamica, di solito è necessaria solo la riga precedente di calcoli per il calcolo successivo, riducendo quindi la complessità spaziale da a . Non sono sicuro di come farei lo stesso in questo caso. Visualizzare una tabella non è fattibile, quindi la risposta dovrebbe provenire direttamente dalla ricorsione sopra.mnmin(m,n)

AGGIORNARE

Utilizzando i suggerimenti di Peter Shor e apportando alcune correzioni minori, in particolare la necessità di tenere traccia della posizione nella funzione e piuttosto di dividere solo le dimensioni in due insiemi A e B, facendo la scissione ricorsivamente, efficacemente usando un metodo divide-and-conquer, fino a quando non viene raggiunto un caso base in cui solo una dimensione è nell'insieme.W(i,ti)

Mi è venuta in mente la seguente implementazione, che ha superato tutti i test al di sotto del tempo massimo di esecuzione:

def ways(di, offset, steps):
    global mem, dimensions
    if steps in mem[di] and offset in mem[di][steps]:
        return mem[di][steps][offset]
    val = 0
    if steps == 0:
        val = 1
    else:
        if offset - 1 >= 1:
            val += ways(di, offset - 1, steps - 1)
        if offset + 1 <= dimensions[di]:
            val += ways(di, offset + 1, steps - 1)
    mem[di][steps][offset] = val
    return val


def set_ways(left, right, steps):
    # must create t1, t2, t3 .. ti for steps
    global mem_set, mem, starting_point
    #print left, right
    #sleep(2)
    if (left, right) in mem_set and steps in mem_set[(left, right)]:
        return mem_set[(left, right)][steps]
    if right - left == 1:
        #print 'getting steps for', left, steps, starting_point[left]
        #print 'got ', mem[left][steps][starting_point[left]], 'steps'
        return mem[left][steps][starting_point[left]]
        #return ways(left, starting_point[left], steps)
    val = 0
    split_point =  left + (right - left) / 2 
    for i in xrange(steps + 1):
        t1 = i
        t2 = steps - i
        mix_factor = fact[steps] / (fact[t1] * fact[t2])
        #print "mix_factor = %d, dimension: %d - %d steps, dimension %d - %d steps" % (mix_factor, left, t1, split_point, t2)
        val += mix_factor * set_ways(left, split_point, t1) * set_ways(split_point, right, t2)
    mem_set[(left, right)][steps] = val
    return val

import sys
from time import sleep, time

fact = {}
fact[0] = 1
start = time()
accum = 1
for k in xrange(1, 300+1):
    accum *= k
    fact[k] = accum
#print 'fact_time', time() - start

data = sys.stdin.readlines()
num_tests = int(data.pop(0))
for ignore in xrange(0, num_tests):
    n_and_steps = data.pop(0)
    n, steps = map(lambda x: int(x), n_and_steps.split())
    starting_point = map(lambda x: int(x), data.pop(0).split())
    dimensions = map(lambda x: int(x), data.pop(0).split())
    mem = {}
    for di in xrange(n):
        mem[di] = {}
        for i in xrange(steps + 1):
            mem[di][i] = {}
            ways(di, starting_point[di], i)
    start = time()
    #print 'mem vector is done'
    mem_set = {}
    for i in xrange(n + 1):
        for j in xrange(n + 1):
            mem_set[(i, j)] = {}
    answer = set_ways(0, n, steps)
    #print answer
    print answer % 1000000007
    #print time() - start

2
"fallisce per un gran numero di passaggi e / o dimensioni" - cosa significa "fallimento" qui?
Raffaello

1
Benvenuto! Ho modificato la tua domanda per a) utilizzare la formattazione Markdown e LaTeX corretta (per favore, in futuro) eb) rimuovere la grondaia superflua. Non ci preoccupiamo per gli sfocature del codice C; per favore, limitati alle idee , cioè allo pseudo codice delle cose centrali.
Raffaello

Fallisce significa che esaurisce tutta la memoria di sistema disponibile riempiendo il mem[]dizionario. E grazie per aver ripulito la mia risposta. Non familiarità con LaTeX ma farà uno sforzo la prossima volta.
Alexandre,

Puoi trovare aiuto su Markdown accanto alla casella dell'editor; guarda qui per un'anteprima su LaTeX.
Raffaello

Risposte:


14

Le diverse dimensioni sono indipendenti . Quello che puoi fare è calcolare, per ogni dimensione j , quante diverse camminate ci sono proprio in quella dimensione che fa passi. Chiamiamo quel numero . Dalla tua domanda, sai già come calcolare questi numeri con la programmazione dinamica.tW(j,t)

Ora è facile contare il numero di camminate che compiono passi nella dimensione . Hai modi di intervallare le dimensioni in modo tale che il numero totale di passi fatti nella dimensione sia , e per ognuno di questi modi hai . Sommati su questi per ottenere Ora, la memoria è sotto controllo, poiché devi solo ricordare i valori . Il tempo aumenta in modo superpolinomiale per grandi dimensioni , ma la maggior parte dei computer ha molto più tempo della memoria.tii(Nt1,t2,,tM)itiΠ1NW(i,ti)

t1+t2++tN=M(Mt1,t2,,tN) Πi=1NW(i,ti).
W(j,t)N

Puoi fare ancora meglio. Ricorsivamente dividere le dimensioni in due sottoinsiemi, e , e calcolare quante passeggiate ci sono utilizzando solo le dimensioni in sottoinsieme , e solo quelli in . Chiama questi numeri e , rispettivamente. Ottieni il numero totale di passeggiateABABWA(t)WB(t)

t1+t2=M(Mt1)WA(t1)WB(t2).

Ciao Peter. Bene, quella era la visione mancante. Ora mi resta solo un dubbio. La somma esterna scorre su tutte le possibili combinazioni di t1, t2, ... tn che somma a M. Sfortunatamente, il numero di tali combinazioni è C (M + 1, N-1), che può essere alto quanto C (300 +1, 10-9). Numero molto grande ... :(
Alexandre

1
@Alexandre: il mio secondo algoritmo (che inizia con "Puoi fare ancora meglio") non ha questo problema. Ho lasciato il primo algoritmo nella mia risposta perché è il primo con cui mi sono inventato e perché penso che sia molto più facile spiegare il secondo algoritmo come una variante del primo piuttosto che darlo senza alcuna motivazione.
Peter Shor,

Ho implementato il secondo algoritmo. È più veloce, ma ancora troppo basso per i limiti più grandi. Il problema con il primo stava iterando su tutte le possibilità di t1, t2, t3, ... tn sommate a M. Il secondo algoritmo itera solo sulle soluzioni a t1 + t2 = M. Ma lo stesso deve essere fatto per Wa (t1), ripetendo le soluzioni a t1 '+ t2' = t1. E così via in modo ricorsivo. Ecco l'implementazione nel caso in cui tu sia interessato: pastebin.com/e1BLG7Gk . E nel secondo algoritmo, il multinomiale dovrebbe essere M su t1, t2 no?
Alexandre,

Non importa! Risolto! Ho dovuto usare la memoization anche nella funzione set_ways. Ecco la soluzione finale, che è velocissima! pastebin.com/GnkjjpBN Grazie per la tua comprensione Peter. Hai fatto entrambe le osservazioni chiave: indipendenza dal problema e divisione e conquista. Consiglio alle persone di vedere la mia soluzione perché ci sono alcune cose che non sono nella risposta sopra, come la funzione W (i, ti) che necessita di un terzo argomento, che è la posizione. Deve essere calcolato per le combinazioni di valori di i, ti e position. Se puoi, aggiungi anche t2 al multinomiale nel tuo secondo algoritmo.
Alexandre,

4

Estraiamo una formula per dal tuo codice (per una cella interna, che ignora i casi limite):now(s,x1,,xn)

now(s,x1,,xn)=+i=0nnow(s1,x1,,xi1,xi+1,xi+1,,xn)+i=0nnow(s1,x1,,xi1,xi1,xi+1,,xn)

Ecco alcune idee.

  • Vediamo che una volta calcolati tutti i valori per , è possibile eliminare tutti i valori calcolati per .s=ks<k
  • Per una fissa , è necessario calcolare le voci della tabella in ordine lessicografico (solo perché è semplice). Quindi, nota che ogni cella ha bisogno solo di tali celle all'interno di un "raggio di una", cioè nessuna coordinata può essere più lontana di una. Pertanto, una volta che l'iterazione raggiunge , è possibile eliminare tutti i valori per . Se questo non è sufficiente, fare lo stesso per - per fisso , valori di perdita con e quando è raggiunto - e così via.sx1=ix1i2x2x1=ix1=ix2j2x2=j
  • Nota che "quindi ci sono sempre possibili mosse diverse" vale solo nel mezzo della griglia, cioè se e per tutti . Ma questo significa anche che la risposta è facile in mezzo: è solo . Se avessi una recidiva di programmazione dinamica funzionante, questo da solo ti permetterebbe di radere via la maggior parte della tabella (se ).2NxiM>0xi+M<Dii(2N)MMN
  • Un'altra cosa da notare è che non è necessario calcolare l'intera tabella; la maggior parte dei valori verrà comunque riempita con (se ). Puoi limitarti al cubo (iper) di lunghezza del bordo intorno a (nota che verrà ammaccato a causa dei percorsi che escono dalla griglia).0MN2Mx

Ciò dovrebbe essere sufficiente per mantenere l'utilizzo della memoria piuttosto basso.


Ciao Raffaele, diciamo che il nostro obiettivo è ora (3, 3, 3, 3), su una griglia 5x5x5. Usando la programmazione dinamica e usando l'ordine lex come hai suggerito, calcoleremmo ora (0, 0, 0, 0), quindi (0, 0, 0, 1), ... ora (0, 5, 5, 5). A che punto ora potremmo scartare (0, 0, 0, 0) (che è più di un raggio di uno lontano da (5, 5, 5), poiché ora avremo bisogno di farlo per calcolare ora (1, 0, 0 , 0), ora (1, 0, 0, 1), ecc. Hai menzionato M << N un paio di volte, ma i limiti sono 1 <= M <= 300 e 1 <= N <= 10. Quindi , agli estremi, non sembra che 1 << 300.
Alexandre

1) Cosa non è chiaro nel mio secondo proiettile? Non appena si calcola , è possibile scartare . Questo non è il primo punto in cui puoi scartare ; l'ultima cella per cui ti serve è . 2) Non sono troppo preoccupato per i tuoi valori specifici per e , a dire il vero. Preferirei guardare al problema generale. Se non hai , gli ultimi due proiettili non ti aiuteranno molto. e dovrebbero bastare per notare l'effetto, e nessuna delle due strategie fa male. (2,0,0,0)(0,\*,\*,\*)(0,0,0,0)(1,0,0,0)MNMNM=1N=10
Raffaello

1
Il 1) proiettile ho capito. Ciò riduce la complessità spaziale da M * D ^ N a D ^ N, ma D ^ N è ancora troppo. Non vedo come funziona il 2) proiettile. Puoi usare l'esempio nel mio commento per illustrarlo?
Alexandre,

@Alexandre l'ho fatto nel mio commento precedente. Se leggo come significato , l'applicazione del secondo punto elenco riduce una volta la complessità dello spazio in , la seconda volta in e presto. (Più precisamente, va da a e così via.)Dmaxi=1,,NDiDN1DN2i=1NDii=2NDi
Raffaello

Non capisco bene come farlo ... Diciamo che ho capito e che ho ridotto la complessità spaziale a D. Fondamentalmente, i sottoproblemi M * D ^ N non dovranno ancora essere risolti? Non è necessaria una proprietà aggiuntiva per rendere il problema polinomiale?
Alexandre,
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.