Dynamische Programmierung mit vielen Teilproblemen

11

Dynamische Programmierung mit vielen Teilproblemen. Ich versuche also, dieses Problem in der Interview Street zu lösen:

Gitter Gehen (50 Punkte) ,
Sie sind in einem befindet N -dimensionalen Gitter an der Position (x1,x2,,xN) . Die Abmessungen des Gitters sind (D1,D2,,DN ). In einem Schritt können Sie in einer der N Dimensionen einen Schritt vorwärts oder rückwärts gehen. (Es sind also immer 2N verschiedene Bewegungen möglich). Auf wie viele Arten können Sie M nehmenMSchritte, bei denen Sie das Raster zu keinem Zeitpunkt verlassen? Sie verlassen das Gitter, wenn für xi entweder xi0 oder xi>Di .

Mein erster Versuch war diese auswendig gelernte rekursive Lösung:

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

Große Überraschung: Es schlägt bei einer großen Anzahl von Schritten und / oder Dimensionen aufgrund von Speichermangel fehl.

Der nächste Schritt ist also, meine Lösung durch dynamische Programmierung zu verbessern. Aber bevor ich anfange, sehe ich ein großes Problem mit dem Ansatz. Das Argument starting_pointist ein n Tupel, wobei n so groß wie 10 . So in der Tat könnte die Funktion sein , number_of_ways(steps, x1, x2, x3, ... x10)mit 1xi100 .

Die dynamischen Programmierprobleme, die ich in Lehrbüchern gesehen habe, haben fast alle zwei Variablen, so dass nur eine zweidimensionale Matrix benötigt wird. In diesem Fall wäre eine zehndimensionale Matrix erforderlich. Also Zellen insgesamt.10010

Bei 2D-Matrizen in der dynamischen Programmierung wird normalerweise nur die vorherige Berechnungsreihe für die nächste Berechnung benötigt, wodurch die räumliche Komplexität von auf min ( m , n ) verringert wird . Ich bin mir nicht sicher, wie ich das in diesem Fall tun würde. Die Visualisierung einer Tabelle ist nicht möglich, daher müsste die Antwort direkt aus der obigen Rekursion stammen.mnmin(m,n)

AKTUALISIEREN

Verwenden Sie die Vorschläge von Peter Shor und nehmen Sie einige kleinere Korrekturen vor, insbesondere die Notwendigkeit, die Position in der -Funktion zu verfolgen und nicht nur die Dimensionen in zwei Sätze A und B aufzuteilen, sondern die Aufteilung rekursiv und effektiv durchzuführen Eine Divide-and-Conquer-Methode, bis ein Basisfall erreicht ist, in dem sich nur eine Dimension in der Menge befindet.W(i,ti)

Ich habe die folgende Implementierung entwickelt, die alle Tests unterhalb der maximalen Ausführungszeit bestanden hat:

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
Alexandre
quelle
2
"es schlägt für eine große Anzahl von Schritten und / oder Dimensionen fehl" - was bedeutet "fehlschlagen" hier?
Raphael
1
Herzlich willkommen! Ich habe Ihre Frage bearbeitet, um a) die richtige Markdown- und LaTeX-Formatierung zu verwenden (bitte selbst in Zukunft) und b) überflüssige Dachrinnen zu entfernen. Wir interessieren uns nicht für Klappentexte von C-Code; Bitte beschränken Sie sich auf Ideen , das ist Pseudocode der zentralen Dinge.
Raphael
Fehlgeschlagen bedeutet, dass der gesamte verfügbare Systemspeicher durch Auffüllen des mem[]Wörterbuchs erschöpft wird . Und danke, dass du meine Antwort aufgeräumt hast. Nicht allzu vertraut mit LaTeX, wird sich aber beim nächsten Mal anstrengen.
Alexandre
Hilfe zu Markdown finden Sie neben dem Editorfeld. siehe hier für eine Grundierung auf LaTeX.
Raphael

Antworten:

14

Die verschiedenen Dimensionen sind unabhängig . Was Sie tun können, ist für jede Dimension j zu berechnen, wie viele verschiedene Spaziergänge es in genau dieser Dimension gibt, die Schritte ausführen. Nennen wir diese Nummer W ( j , t ) . Aus Ihrer Frage wissen Sie bereits, wie Sie diese Zahlen mit dynamischer Programmierung berechnen können.tW(j,t)

Jetzt ist es einfach , die Anzahl der Schichten zu zählen , die nehmen in Dimension Schritte i . Sie haben ( N.tiiWegen der Dimensionenso dass die Gesamtzahl der Schritte interspersing in Dimensioniistti, und für jede dieser Möglichkeitenwie Sie habenΠN1W(i,ti)geht. Summiere diese, um t1+t2++tN=M(M.(Nt1,t2,,tM)itiΠ1NW(i,ti) Jetzt ist der Speicher unter Kontrolle, da Sie sich nur noch die WerteW(j,t)merken müssen. Die Zeit wächst für großeNsuperpolynomiell, aber die meisten Computer haben viel mehr Zeit als Speicher.

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

Sie können es noch besser machen. Teilen Sie die Dimensionen rekursiv in zwei Teilmengen, und B , und berechnen Sie, wie viele Spaziergänge es gibt, indem Sie nur die Dimensionen in Teilmenge A und nur die in B verwenden . Nennen Sie diese Nummern W A ( t ) bzw. W B ( t ) . Sie erhalten die Gesamtzahl der Spaziergänge vorbeiABABWA(t)WB(t)

t1+t2=M(Mt1)WA(t1)WB(t2).
Peter Shor
quelle
Hallo Peter. Okay, das war die fehlende Einsicht. Jetzt habe ich nur noch einen Zweifel. Die äußere Summe iteriert über alle möglichen Kombinationen von t1, t2, ... tn dieser Summe zu M. Leider ist die Anzahl solcher Kombinationen C (M + 1, N-1), was so hoch wie C (300) sein kann +1, 10-9). Sehr große Anzahl ... :(
Alexandre
1
@Alexandre: Mein zweiter Algorithmus (beginnend mit "Du kannst es noch besser machen") hat dieses Problem nicht. Ich habe den ersten Algorithmus in meiner Antwort belassen, weil es der erste ist, den ich mir ausgedacht habe, und weil ich denke, dass es viel einfacher ist, den zweiten Algorithmus als eine Variante des ersten zu erklären, als ihn nur ohne Motivation zu geben.
Peter Shor
Ich habe den zweiten Algorithmus implementiert. Es ist schneller, aber immer noch zu niedrig für die größten Grenzen. Das Problem mit dem ersten bestand darin, alle Möglichkeiten von t1, t2, t3, ... tn zu iterieren, die zu M summiert wurden. Der zweite Algorithmus iteriert nur über Lösungen zu t1 + t2 = M. Aber dann muss dasselbe für Wa getan werden (t1), Iteration über Lösungen zu t1 '+ t2' = t1. Und so weiter rekursiv. Hier ist die Implementierung, falls Sie insterested sind: pastebin.com/e1BLG7Gk . Und im zweiten Algorithmus sollte das Multinom M über t1, t2 nein sein?
Alexandre
Keine Ursache! Ich habe es gelöst! Musste Memoization auch in der Funktion set_ways verwenden. Hier ist die endgültige Lösung, die blitzschnell ist! pastebin.com/GnkjjpBN Vielen Dank für Ihren Einblick Peter. Sie haben beide wichtigen Beobachtungen gemacht: Problemunabhängigkeit und Teilen und Erobern. Ich empfehle den Leuten, sich meine Lösung anzusehen, da es einige Dinge gibt, die in der obigen Antwort nicht enthalten sind, wie zum Beispiel die W (i, ti) -Funktion, die ein drittes Argument benötigt, nämlich die Position. Dies muss für Wertekombinationen von i, ti und Position berechnet werden. Wenn Sie können, fügen Sie in Ihrem zweiten Algorithmus auch t2 das Multinom hinzu.
Alexandre
4

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)

Hier sind ein paar Ideen.

  • s=ks<k
  • sx1=ix1i2x2x1=ix1=ix2j2x2=j erreicht ist - und so weiter.
  • 2NxiM>0xi+M<Dii(2N)MMN
  • 0MN2Mx (beachten Sie, dass er aufgrund von Pfaden, die das Gitter verlassen, verbeult wird).

Das sollte ausreichen, um die Speichernutzung recht gering zu halten.

Raphael
quelle
Hallo Raphael, sagen wir, unser Ziel ist jetzt (3, 3, 3, 3) in einem Raster von 5 x 5 x 5. Unter Verwendung der dynamischen Programmierung und der von Ihnen vorgeschlagenen Lexreihenfolge würden wir jetzt (0, 0, 0, 0), dann (0, 0, 0, 1), ... jetzt (0, 5, 5, 5) berechnen. An welchem ​​Punkt könnten wir jetzt verwerfen (0, 0, 0, 0) (was mehr als ein Radius von eins von (5, 5, 5) entfernt ist, da wir es jetzt brauchen werden, um jetzt zu berechnen (1, 0, 0) , 0), jetzt (1, 0, 0, 1) usw.? Sie haben M << N ein paar Mal erwähnt, aber die Grenzen sind 1 <= M <= 300 und 1 <= N <= 10. Also ,
Alexandre
(2,0,0,0)(0,\*,\*,\*)(0,0,0,0)(1,0,0,0)MNMNM=1N=10
1
Die 1) Kugel verstehe ich. Das reduziert die räumliche Komplexität von M * D ^ N auf D ^ N, aber D ^ N ist immer noch zu viel. Ich sehe allerdings nicht ganz, wie die 2) Kugel funktioniert. Können Sie das Beispiel in meinem Kommentar verwenden, um es zu veranschaulichen?
Alexandre
D.maxi=1,,N.D.ichD.N.- -1D.N.- -2ich=1N.D.ichich=2N.D.ich und so weiter.)
Raphael
Ich verstehe nicht ganz, wie es geht ... Nehmen wir an, ich habe es verstanden und die räumliche Komplexität auf D reduziert. Müssen M * D ^ N-Teilprobleme nicht grundsätzlich noch gelöst werden? Wird nicht eine zusätzliche Eigenschaft benötigt, um das Problem polynomisch zu machen?
Alexandre