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 xi≤0 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 1≤xi≤100 .

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⁡(s−1,x1,…,xi−1,xi+1,xi+1,…,xn)+∑i=0nnow⁡(s−1,x1,…,xi−1,xi−1,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=ix1≤i−2x2x1=ix1=ix2≤j−2x2=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 ).2Nxi−M>0xi+M<Dii(2N)MM≪N
  • 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ę).0M≪N2Mx

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)MNM≪NM=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,…,NDiDN−1DN−2∏i=1NDi∏i=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.