Programowanie dynamiczne z dużą liczbą podproblemów


11

Programowanie dynamiczne z dużą liczbą podproblemów. Więc próbuję rozwiązać ten problem z ulicy wywiadu:

Grid Walking (Zdobądź 50 punktów)
Znajdujesz się w siatce N wymiarowej na pozycji (x1,x2,,xN) . Wymiary siatki to (D1,D2,,DN ). W jednym kroku możesz przejść jeden krok do przodu lub do tyłu w dowolnym z N wymiarów. (Więc zawsze są 2N możliwe różne ruchy). Na ile sposobów możesz wziąć Mkroki takie, że w żadnym momencie nie opuszczasz siatki? Opuszczasz siatkę, jeśli dla dowolnego xi , albo xi0 albo xi>Di .

Moją pierwszą próbą było zapamiętane rozwiązanie rekurencyjne:

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

Wielka niespodzianka: zawodzi w przypadku dużej liczby kroków i / lub wymiarów z powodu braku pamięci.

Następnym krokiem jest ulepszenie mojego rozwiązania za pomocą programowania dynamicznego. Ale przed rozpoczęciem widzę poważny problem z tym podejściem. Argumentem starting_pointjest n -tuple, gdzie n jest tak duże jak 10 . Tak więc w rzeczywistości funkcja może być number_of_ways(steps, x1, x2, x3, ... x10)z 1xi100 .

Problemy z programowaniem dynamicznym, które widziałem w podręcznikach, prawie wszystkie mają zmienne TWP, więc potrzebna jest tylko dwuwymiarowa macierz. W takim przypadku potrzebna byłaby dziesięciowymiarowa matryca. Więc komórek w sumie.10010

mnmin(m,n)

AKTUALIZACJA

Korzystając z sugestii Petera Shora i wprowadzając kilka drobnych poprawek, zwłaszcza potrzebę śledzenia pozycji w funkcji , a nie tylko dzielenie wymiarów na dwa zestawy A i B, dzielenie rekurencyjne, efektywne przy użyciu metoda „dziel i rządź”, dopóki nie zostanie osiągnięty przypadek podstawowy, gdy w zestawie znajduje się tylko jeden wymiar.W(i,ti)

Wymyśliłem następującą implementację, która przeszła wszystkie testy poniżej maksymalnego czasu wykonania:

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
„nie udaje się w przypadku dużej liczby kroków i / lub wymiarów” - co oznacza tutaj „awaria”?
Raphael

1
Witamy! Zredagowałem twoje pytanie, aby: a) użyć właściwego formatowania Markdown i LaTeX (proszę, abyś sam to zrobił w przyszłości) oraz b) usunąć zbędną rynnę. Nie dbamy o bluki kodu C; ogranicz się do pomysłów , czyli pseudo-kodu najważniejszych rzeczy.
Raphael

Fails oznacza, że ​​wyczerpuje całą dostępną pamięć systemową, wypełniając mem[]słownik. I dziękuję za oczyszczenie mojej odpowiedzi. Niezbyt zaznajomiony z LaTeX, ale dołoży starań następnym razem.
Alexandre

Pomoc znajdziesz na Markdown obok pola edytora; zobacz tutaj podkład dla LaTeX.
Raphael

Odpowiedzi:


14

Różne wymiary są niezależne . To, co możesz zrobić, to obliczyć, dla każdego wymiaru j , ile różnych spacerów jest w tym wymiarze, który wykonuje kroków. Nazwijmy ten numer . Z pytania już wiesz, jak obliczyć te liczby za pomocą programowania dynamicznego.tW(j,t)

Teraz łatwo policzyć liczbę spacerów, które wykonują kroki w wymiarze . Trzeba sposoby wymiary przeplatając tak, że całkowita liczba kroków podjętych w wymiarze jest , a dla każdego z tych sposobów masz spacery. Zsumuj je, aby uzyskać Teraz pamięć jest pod kontrolą, ponieważ wystarczy zapamiętać wartości . Czas rośnie wielobiegunowo dla dużych , ale większość komputerów ma znacznie więcej czasu niż pamięć.tii(Nt1,t2,,tM)itiΠ1NW(i,ti)

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

Możesz zrobić jeszcze lepiej. Rekursywnie podzielić wymiary na dwa podzbiory, i , i obliczyć, ile idzie tam używasz tylko wymiary w podzbioru , i tylko te, w . Nazwij te numery odpowiednio i . Otrzymujesz łączną liczbę spacerówABABWA(t)WB(t)

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

Cześć Piotr. W porządku, to był brakujący wgląd. Teraz pozostała mi tylko jedna wątpliwość. Suma zewnętrzna iteruje wszystkie możliwe kombinacje t1, t2, ... tn tej sumy do M. Niestety liczba takich kombinacji wynosi C (M + 1, N-1), która może być tak wysoka jak C (300 +1, 10-9). Bardzo duża liczba ... :(
Alexandre,

1
@Alexandre: Mój drugi algorytm (zaczynający się od „Możesz zrobić jeszcze lepiej”) nie ma tego problemu. Pierwszy algorytm zostawiłem w swojej odpowiedzi, ponieważ jest to pierwszy, który wymyśliłem, i ponieważ myślę, że dużo łatwiej jest wytłumaczyć drugi algorytm jako wariant pierwszego niż podanie go bez żadnej motywacji.
Peter Shor,

Zaimplementowałem drugi algorytm. Jest szybszy, ale wciąż zbyt niski dla największych granic. Problem z pierwszym polegał na iteracji wszystkich możliwości t1, t2, t3, ... tn, które zostały zsumowane do M. Drugi algorytm iteruje tylko rozwiązania t1 + t2 = M. Ale wtedy to samo należy zrobić dla Wa (t1), iteracja po rozwiązaniach do t1 '+ t2' = t1. I tak dalej rekurencyjnie. Oto implementacja na wypadek zainteresowania: pastebin.com/e1BLG7Gk . A w drugim algorytmie wielomian powinien być M ponad t1, t2 nie?
Alexandre,

Nieważne! Rozwiązałem to! Musiałem również użyć zapamiętywania w funkcji set_ways. Oto ostateczne rozwiązanie, które płonie szybko! pastebin.com/GnkjjpBN Dziękujemy za wgląd Peter. Poczyniłeś oba kluczowe spostrzeżenia: problem niezależności i podziału i podboju. Polecam, aby ludzie spojrzeli na moje rozwiązanie, ponieważ są pewne rzeczy, których nie ma w powyższej odpowiedzi, takie jak funkcja W (i, ti) wymagająca trzeciego argumentu, którym jest pozycja. Należy to obliczyć dla kombinacji wartości i, ti i pozycji. Jeśli możesz, dodaj również t2 wielomian w drugim algorytmie.
Alexandre,

4

formułę dla nazwa z kodu (dla komórki wewnętrznej, która ignoruje przypadki graniczne):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)

Oto kilka pomysłów.

  • Widzimy, że po obliczeniu wszystkich wartości dla można usunąć wszystkie obliczone wartości dla .s=ks<k
  • Dla stałych należy obliczyć pozycje tabeli w porządku leksykograficznym (tylko dlatego, że jest to proste). Następnie zauważ, że każda komórka potrzebuje tylko takich komórek w „promieniu jednego”, to znaczy żadna współrzędna nie może być dalej niż jedna. Dlatego gdy twoja iteracja osiągnie , możesz usunąć wszystkie wartości dla . Jeśli to nie wystarczy, zrób to samo dla - dla ustalonego , upuść wartości z oraz gdy osiągnięta zostanie - i tak dalej.sx1=ix1i2x2x1=ix1=ix2j2x2=j
  • Zauważ, że „więc zawsze są możliwe różne ruchy” zachowuje się tylko w środku siatki, to znaczy, jeśli i dla wszystkich . Ale to oznacza również, że odpowiedź jest prosta w środku: to tylko . Jeśli miałeś działające cykliczne powtarzanie programowania, to samo pozwoliłoby ci zgolić większość tabeli (jeśli ).2NxiM>0xi+M<Dii(2N)MMN
  • Kolejną rzeczą do odnotowania jest to, że nie musisz obliczać całego stołu; większość wartości i tak zostanie wypełniona (jeśli ). Możesz ograniczyć się do (hiper) sześcianu o długości wokół (zwróć uwagę, że zostanie on wgnieciony z powodu ścieżek opuszczających siatkę).0MN2Mx

To powinno wystarczyć do utrzymania niskiego zużycia pamięci.


Cześć Raphael, powiedzmy, że naszym celem jest teraz (3, 3, 3, 3) na siatce 5x5x5. Używając programowania dynamicznego i porządku leksykalnego, jak zasugerowałeś, obliczylibyśmy teraz (0, 0, 0, 0), a następnie (0, 0, 0, 1), ... teraz (0, 5, 5, 5). W którym momencie moglibyśmy odrzucić teraz (0, 0, 0, 0) (co jest więcej niż promieniem jednego z dala od (5, 5, 5), ponieważ teraz będziemy musieli to teraz obliczyć (1, 0, 0 , 0), teraz (1, 0, 0, 1) itd. Wspomniałeś M << N kilka razy, ale granice wynoszą 1 <= M <= 300 i 1 <= N <= 10. Więc , w skrajnościach, nie wydaje się, że 1 << 300.
Alexandre

1) Co jest niejasne w mojej drugiej kuli? Jak tylko , możesz odrzucić . Nie jest to jednak najwcześniejszy punkt, w którym możesz odrzucić ; ostatnia komórka, której potrzebujesz, to . 2) Nie jestem zbytnio zaniepokojony swoimi wartościami specyficznych dla i , aby być uczciwym. Wolę spojrzeć na ogólny problem. Jeśli nie masz , dwa ostatnie pociski niewiele ci pomogą. i powinny wystarczyć, aby zauważyć efekt, i żadna strategia nigdy nie boli. (2,0,0,0)(0,\*,\*,\*)(0,0,0,0)(1,0,0,0)MNMNM=1N=10
Raphael

1
1) kula, którą rozumiem. Zmniejsza to złożoność przestrzenną z M * D ^ N do D ^ N, ale D ^ N to wciąż za dużo. Nie do końca rozumiem, jak działa 2) kula. Czy możesz zilustrować ten przykład w moim komentarzu?
Alexandre,

@Alexandre zrobiłem w moim wcześniejszym komentarzu. Jeśli przeczytam w znaczeniu , to zastosowanie drugiego pocisku raz zmniejszy złożoność przestrzeni do , drugi raz do i wkrótce. (Dokładniej rzecz biorąc, przechodzi z do i tak dalej.)Dmaxi=1,,NDiDN1DN2i=1NDii=2NDi
Raphael

Nie do końca rozumiem, jak to zrobić ... Powiedzmy, że zrozumiałem i że zredukowałem złożoność przestrzenną do D. Zasadniczo, czy podproblemy M * D ^ N wciąż nie będą musiały zostać rozwiązane? Czy dodatkowa właściwość nie jest potrzebna, aby problem był wielomianowy?
Alexandre,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.