Динамическое программирование с большим количеством подзадач

11

Динамическое программирование с большим количеством подзадач. Поэтому я пытаюсь решить эту проблему с улицы Интервью:

Ходьба по сетке (оценка 50 баллов)
Вы находитесь в мерной сетке в позиции . Размеры сетки: ). За один шаг вы можете идти на один шаг вперед или назад в любом из измерений. (Так что всегда есть возможных разных ходов). Сколько способов вы можете сделать шагов, чтобы не покинуть сетку ни в одной точке? Вы выходите из сетки, если для любого , либо либо .N(x1,x2,,xN)(D1,D2,,DNN2NMxixi0xi>Di

Моей первой попыткой было это запомненное рекурсивное решение:

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

Большой сюрприз: он не подходит для большого количества шагов и / или измерений из-за недостатка памяти.

Так что следующий шаг , чтобы улучшить свое решение с помощью динамического программирования. Но прежде чем начать, я вижу большую проблему с подходом. Аргумент starting_pointявляется -кратным, где таким же, как . Таким образом , в самом деле, функция может быть с .n 10 1 x i100nn10number_of_ways(steps, x1, x2, x3, ... x10)1xi100

Проблемы динамического программирования, которые я видел в учебниках, почти все имеют переменные twp, так что нужна только двумерная матрица. В этом случае потребуется десятимерная матрица. Итак, всего клеток.10010

С 2-D матрицами в динамическом программировании, как правило , только предыдущий ряд вычислений требуются для следующего расчета, следовательно , уменьшение пространственной сложности от до . Я не уверен, как бы я поступил так же в этом случае. Визуализация таблицы неосуществима, поэтому ответ должен прийти непосредственно из приведенной выше рекурсии.mnmin(m,n)

ОБНОВИТЬ

Используя предложения Питера Шора и внося некоторые незначительные исправления, в частности, необходимость отслеживать положение в функции , а не только измерения на два набора A и B, выполняя разбиение рекурсивно, эффективно используя метод «разделяй и властвуй», пока не будет достигнут базовый случай, когда в наборе есть только одно измерение.W(i,ti)

Я придумал следующую реализацию, которая прошла все тесты ниже максимального времени выполнения:

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
«это терпит неудачу для большого количества шагов и / или измерений» - что означает здесь «неудача»?
Рафаэль
1
Добро пожаловать! Я отредактировал ваш вопрос следующим образом: а) используйте правильное форматирование Markdown и LaTeX (пожалуйста, сделайте это сами в будущем) и б) удалите лишний желоб. Мы не заботимся о недостатках C-кода; пожалуйста, ограничьте себя идеями , то есть псевдокодом центральных вещей.
Рафаэль
Сбой означает, что он исчерпывает всю доступную системную память, заполняя mem[]словарь. И спасибо, что убрали мой ответ. Не слишком знаком с LaTeX, но постараюсь в следующий раз.
Александр
Вы можете найти помощь по Markdown рядом с окном редактора; см. здесь для начинающих на LaTeX.
Рафаэль

Ответы:

14

Различные размеры независимы . Что вы можете сделать, так это вычислить, для каждого измерения j , сколько различных шагов есть только в этом измерении, которые предпринимают шагов. Назовем это число . Из вашего вопроса вы уже знаете, как вычислить эти числа с помощью динамического программирования.tW(j,t)

Теперь легко подсчитать количество прогулок, которые делают шагов в измерении . У вас есть способов вкрапления измерений, так что общее количество шагов, сделанных в измерении равно , и для каждого из этих способов у вас есть . Суммируйте их, чтобы получить Теперь память находится под контролем, так как вам нужно только запомнить значения . Время увеличивается сверхполиномиально для больших , но у большинства компьютеров гораздо больше времени, чем памяти.tii(Nt1,t2,,tM)itiΠ1NW(i,ti)

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

Вы можете сделать еще лучше. Рекурсивный разделить размеры на два подмножества, и , и вычислить , сколько ходит там , используя только размеры в подмножество , и только те , в . Назовите эти номера и соответственно. Вы получаете общее количество прогулок поABABWA(t)WB(t)

t1+t2=M(Mt1)WA(t1)WB(t2).
Питер Шор
источник
Привет Питер. Хорошо, это было недостающее понимание. Теперь у меня осталось только одно сомнение. Внешняя сумма повторяется по всем возможным комбинациям t1, t2, ... tn, которые суммируются с M. К сожалению, количество таких комбинаций равно C (M + 1, N-1), которое может достигать C (300 +1, 10-9). Очень большое количество ... :(
Александр
1
@Alexandre: Мой второй алгоритм (начиная с «Вы можете сделать еще лучше») не имеет этой проблемы. Я оставил первый алгоритм в своем ответе, потому что это первый, который я придумал, и потому что я думаю, что гораздо проще объяснить второй алгоритм как вариант первого, чем просто дать его без какой-либо мотивации.
Питер Шор
Я реализовал второй алгоритм. Это быстрее, но все еще слишком мало для самых больших границ. Проблема с первым состояла в итерации по всем возможностям t1, t2, t3, ... tn, которые суммировались с M. Второй алгоритм только перебирает решения t1 + t2 = M. Но тогда то же самое должно быть сделано для Wa (t1), перебирая решения t1 '+ t2' = t1. И так далее, рекурсивно. Вот реализация на случай, если вы заинтересуетесь: pastebin.com/e1BLG7Gk . И во втором алгоритме, там должно быть многочлен M над t1, t2 нет?
Александр
Ничего! Решил это! Пришлось также использовать памятку в функции set_ways. Вот окончательное решение, которое очень быстро! pastebin.com/GnkjjpBN Спасибо за ваше понимание, Питер. Вы сделали оба ключевых наблюдения: независимость проблемы и разделяй и властвуй. Я рекомендую людям взглянуть на мое решение, потому что есть некоторые вещи, которых нет в ответе выше, например, функция W (i, ti), нуждающаяся в третьем аргументе, который является позицией. Это должно быть рассчитано для комбинаций значений i, ti и position. Если вы можете, также добавьте t2 к многочлену во втором алгоритме.
Александр
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)

Вот несколько идей.

  • Мы видим, что как только вы вычислили все значения для , вы можете отбросить все вычисленные значения для .s=ks<k
  • Для фиксированных значений вы должны вычислять записи таблицы в лексикографическом порядке (просто потому, что это просто). Затем обратите внимание, что каждая ячейка нуждается только в таких ячейках внутри «радиуса одного», то есть никакая координата не может быть дальше, чем единица. Поэтому, как только ваша итерация достигнет , вы можете отбросить все значения для . Если этого недостаточно, сделайте то же самое для - для фиксированного , отбросьте значения с и когда достигается - и так далее.sx1=ix1i2x2x1=ix1=ix2j2x2=j
  • Обратите внимание, что «так что всегда есть возможных различных ходов» выполняется только в середине сетки, то есть если и для всех . Но это также означает , что ответ прост в середине: это просто . Если бы у вас был рабочий рецидив динамического программирования, это само по себе позволило бы сбрить большую часть таблицы (если ).2NxiM>0xi+M<Dii(2N)MMN
  • Следует также отметить, что вам не нужно вычислять всю таблицу; большинство значений будут заполнены любом случае (если ). Вы можете ограничиться (гипер) кубом с длиной ребра вокруг (обратите внимание, что он будет помят из-за путей, выходящих из сетки).0MN2Mx

Этого должно быть достаточно, чтобы сохранить использование памяти достаточно низким.

Рафаэль
источник
Привет Рафаэль, скажем, наша цель сейчас (3, 3, 3, 3), на сетке 5x5x5. Используя динамическое программирование и используя лексический порядок, как вы предложили, мы вычислим сейчас (0, 0, 0, 0), затем (0, 0, 0, 1), ... сейчас (0, 5, 5, 5). В какой -то момент мы могли бы отбросить сейчас (0, 0, 0, 0) (что больше , чем радиус одного от (5, 5, 5), так как мы сейчас это нужно, чтобы вычислить сейчас (1, 0, 0 , 0), сейчас (1, 0, 0, 1) и т. Д. Вы упомянули M << N пару раз, но границы составляют 1 <= M <= 300 и 1 <= N <= 10. Итак, , в крайности, не кажется , что 1 << 300.
Александр
1) То , что неясно , в моей второй пуле? Как только вы вычислить , вы можете отказаться . Это не самый ранний момент , где вы можете отказаться , хотя; последняя ячейка вам это нужно есть . 2) Я не слишком обеспокоен ваших конкретных значений и , если честно. Я бы лучше посмотрел на общую проблему. Если вы не имеете , последние две пули не помогут вам много. и должно быть достаточно , чтобы заметить эффект, хотя и ни стратегия никогда не помешает. (2,0,0,0)(0,\*,\*,\*)(0,0,0,0)(1,0,0,0)MNMNM=1N=10
Рафаэль
1
1) пулю я понимаю. Это уменьшает пространственную сложность с M * D ^ N до D ^ N, но D ^ N все еще слишком много. Я не совсем понимаю, как работает 2) пуля. Вы можете использовать пример моего комментария , чтобы проиллюстрировать это?
Александр
@Alexandre Я сделал в своем предыдущем комментарии. Если я читаю как значение , то применение второго один раз уменьшает сложность пространства до , второй раз до и скоро. (Точнее, он идет от к и т. Д.)Dmaxi=1,,NDiDN1DN2i=1NDii=2NDi
Рафаэль
Не совсем понимаю, как это сделать ... Допустим, я понял, и что я уменьшил пространственную сложность до D. По сути, не нужно ли еще решать подзадачи M * D ^ N? Не нужно ли дополнительное свойство, чтобы сделать задачу полиномиальной?
Александр