Динамічне програмування з великою кількістю підпрограм


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- -tuple, де дорівнює . Тож насправді функція може бути з .n 10 1 x i100nn10number_of_ways(steps, x1, x2, x3, ... x10)1xi100

Проблеми з динамічним програмуванням, які я бачив у підручниках, майже у всіх мають змінні twp, тому потрібна лише двовимірна матриця. У цьому випадку знадобиться десятивимірна матриця. Тож клітин усього.10010

Для двовимірних матриць у динамічному програмуванні зазвичай потрібен лише попередній ряд обчислень для наступного обчислення, отже, зменшення просторової складності з до . Я не впевнений, як би я зробив те саме в цій справі. Візуалізація таблиці неможлива, тому відповідь повинна була б бути безпосередньо з рекурсії вище.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 (будь ласка, так, щоб у майбутньому) і b) видалити зайвий жолоб. Ми не дбаємо про розмивання 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, що підсумовується до М. Другий алгоритм лише ітераціює рішення щодо t1 + t2 = M. Але тоді те ж саме потрібно зробити для Wa (t1), ітерація над рішеннями t1 '+ t2' = t1. І так далі рекурсивно. Ось реалізація у випадку, якщо вас не зацікавить: pastebin.com/e1BLG7Gk . А у другому алгоритмі мультином має бути M над t1, t2 ні?
Олександр

Не звертай уваги! Вирішили це! Доводилося також використовувати пам'ятку у функції set_ways. Ось остаточне рішення, яке швидко палає! pastebin.com/GnkjjpBN Дякую за вашу проникливість Пітер. Ви зробили обидва ключові спостереження: незалежність проблеми та розділення та перемогу. Я рекомендую людям переглядати моє рішення, оскільки є деякі речі, на які немає відповіді вище, наприклад функція W (i, ti), яка потребує третього аргументу, який є позицією. Це має бути обчислено для комбінацій значень i, ti та позиції. Якщо ви можете, також додайте 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. Використовуючи динамічне програмування та використовуючи порядок lex, як ви запропонували, ми обчислимо зараз (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 все ще вирішувати? Чи не потрібна додаткова властивість, щоб зробити проблему поліномом?
Олександр
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.