Подсчет действительных последовательностей с динамическим программированием - PullRequest
4 голосов
/ 24 сентября 2019

Я довольно новичок в динамическом программировании, но я пытаюсь стать лучше.У меня есть упражнение из книги, которое задает мне следующий вопрос (слегка сокращенный):

Вы хотите построить последовательность длины N из чисел из набора {1, 2, 3, 4, 5, 6}.Однако вы не можете разместить число i (i = 1, 2, 3, 4, 5, 6) более чем A [i] раз подряд, где A - заданный массив.С учетом длины последовательности N (1 <= N <= 10 ^ 5) и массива ограничений A (1 <= A [i] <= 50), сколько последовательностей возможно? </p>

Например, если A= {1, 2, 1, 2, 1, 2} и N = 2, это будет означать, что вы можете иметь только одну последовательную 1, две последовательных 2, одну последовательную 3 и т. Д. Здесь что-то вроде «11» недопустимо, посколькуу него есть два последовательных 1, тогда как что-то вроде «12» или «22» оба действительны.Оказывается, что фактический ответ для этого случая - 33 (всего 36 двузначных последовательностей, но «11», «33» и «55» недопустимы, что дает 33).


Кто-то сказал мне, что одним из способов решения этой проблемы является использование динамического программирования с тремя состояниями.Более конкретно, они говорят, чтобы сохранить 3d-массив dp (i, j, k), где i представляет текущую позицию, в которой мы находимся в последовательности, j представляет элемент, помещенный в позицию i - 1, и k представляет число раз, когдаэтот элемент был повторен в блоке.Они также сказали мне, что для переходов мы можем поместить в положение i каждый элемент, отличный от j, и мы можем вставить только j, если A [j]> k.

Это все имеет смысл для меня в теории, но я боролся с реализацией этого.Я понятия не имею, как начать с фактической реализации, кроме инициализации матрицы dp.Как правило, большинство других упражнений имели своего рода «базовый случай», который был установлен вручную в матрице, а затем для заполнения других записей использовался цикл.

Я думаю, что я особенно запутался, потому что этоявляется 3D-массивом.

Ответы [ 2 ]

4 голосов
/ 24 сентября 2019

На мгновение давайте просто не будем заботиться о массиве.Давайте реализуем это рекурсивно.Пусть dp(i, j, k) будет числом последовательностей длиной i, последним элементом j и k последовательных вхождений j в конце массива.

Теперь возникает вопрос, как сделатьмы пишем решение dp(i, j, k) рекурсивно.

Хорошо, мы знаем, что мы добавляем j kth время, поэтому мы должны взять каждую последовательность длиной i - 1, и j встречается k - 1 раз, и добавить еще однуj к этой последовательности.Обратите внимание, что это просто dp(i - 1, j, k - 1).

Но что, если k == 1?Если это так, мы можем добавить один случай j к каждой последовательности длины i - 1, которая не заканчивается j.По сути, нам нужна сумма всех dp(i, x, k), таких как A[x] >= k и x != j.

Это дает наше рекуррентное соотношение:

def dp(i, j, k):

    # this is the base case, the number of sequences of length 1
    # one if k is valid, otherwise zero
    if i == 1: return int(k == 1)

    if k > 1:
        # get all the valid sequences [0...i-1] and add j to them
        return dp(i - 1, j, k - 1)

    if k == 1:
        # get all valid sequences that don't end with j
        res = 0
        for last in range(len(A)):
            if last == j: continue
            for n_consec in range(1, A[last] + 1):
                res += dp(i - 1, last, n_consec)
        return res

Мы знаем, что наш ответ будет всеми действительными подпоследовательностями длины N, поэтому наш окончательный ответ будет sum(dp(N, j, k) for j in range(len(A)) for k in range(1, A[j] + 1))

Хотите верьте, хотите нет, но это основа динамического программирования.Мы просто разбили нашу главную проблему на множество подзадач.Конечно, сейчас наше время экспоненциально из-за рекурсии.У нас есть два способа уменьшить это:

  1. Кэширование, мы можем просто отслеживать результат каждого (i, j, k) и затем выплевывать то, что мы первоначально вычислили, когда он называетсяочередной раз.

  2. Использовать массив.Мы можем переопределить эту идею с помощью восходящего dp и получить массив dp[i][j][k].Все наши вызовы функций просто становятся обращениями к массиву в цикле for.Обратите внимание, что использование этого метода заставляет нас перебирать массив в топологическом порядке, что может быть сложно.

1 голос
/ 24 сентября 2019

Существует 2 вида подхода dp: сверху вниз и снизу вверх

Внизу вверх вы заполняете корпуса терминалов в таблице dp, а затем используете циклы for для построения из этого.Давайте рассмотрим восходящий алгоритм для генерации последовательности Фибоначчи.Мы устанавливаем dp[0] = 1 и dp[1] = 1 и запускаем цикл for из i = 2 to n.

. При подходе сверху вниз мы начинаем с "верхнего" взгляда на проблему и спускаемся оттуда.Рассмотрим рекурсивную функцию для получения n-го числа Фибоначчи:

def fib(n):
    if n <= 1:
        return 1
    if dp[n] != -1:
        return dp[n]
    dp[n] = fib(n - 1) + fib(n - 2)
    return dp[n]

Здесь мы не заполняем полную таблицу, а только случаи, с которыми мы сталкиваемся.

Почему я говорю об этих двух типах, потому что, когда вы начинаете изучать дп, часто бывает трудно придумать подходы снизу вверх (как вы пытаетесь).Когда это происходит, сначала вы хотите придумать нисходящий подход, а затем попытаться получить из этого восходящее решение.

Итак, давайте сначала создадим рекурсивную функцию dp:

# let m be size of A
# initialize dp table with all values -1
def solve(i, j, k, n, m):
    # first write terminal cases
    if k > A[j]:
        # this means sequence is invalid. so return 0
        return 0
    if i >= n:
        # this means a valid sequence.
        return 1
    if dp[i][j][k] != -1:
        return dp[i][j][k]
    result = 0
    for num = 1 to m:
        if num == j:
            result += solve(i + 1, num, k + 1, n)
        else:
            result += solve(i + 1, num, 1, n)
    dp[i][j][k] = result
    return dp[i][j][k]

Итак, мы знаем, что такое терминальные случаи.Мы создаем таблицу dp размером dp [n + 1] [m] [50].Инициализируйте его всеми значениями 0, а не -1. ​​

Таким образом, мы можем сделать восходящее как:

# initially all values in table are zero. With loop below, we set the valid endings as 1.
# So any state trying to reach valid terminal states will get 1, but invalid states will
# return the values 0
for num = 1 to m:
    for occour = 1 to A[num]:
        dp[n][num][occour] = 1

# now to build up from bottom, we start by filling n-1 th position
for i = n-1 to 1:
    for num = 1 to m:
        for occour = 1 to A[num]:
            for next_num = 1 to m:
                if next_num != num:
                    dp[i][num][occour] += dp[i + 1][next_num][1]
                else:
                    dp[i][num][occour] += dp[i + 1][num][occour + 1]

Ответ будет:

sum = 0
for num = 1 to m:
    sum += dp[1][num][1]

IЯ уверен, что должно быть какое-то более элегантное решение дп, но я верю, что это отвечает на ваш вопрос.Обратите внимание, что я считал, что k - это количество раз, которое j-е число повторялось последовательно, исправьте меня, если я ошибаюсь.

Редактировать:

С учетом ограничений размертаблица в худшем случае будет 10 ^ 5 * 6 * 50 = 3e7.Это будет> 100 МБ.Это работоспособно, но может считаться слишком большим использованием пространства (я думаю, что некоторые ядра не позволяют так много места в стеке процессу).Один из способов уменьшить это - использовать hash-map вместо массива с подходом сверху вниз, так как сверху вниз не посещает все состояния.В этом случае это будет в основном верно, например, если A [1] равен 2, то все остальные состояния, в которых 1 произошло более двух раз, не должны сохраняться.Конечно, это не сэкономит много места, если A [i] имеет большие значения, скажем, [50, 50, 50, 50, 50, 50].Другим подходом было бы немного изменить наш подход.На самом деле нам не нужно хранить размерность k, то есть времена j появлялись последовательно:

dp[i][j] = no of ways from i-th position if (i - 1)th position didn't have j and i-th position is j.

Затем нам нужно изменить наш алгоритм так:

def solve(i, j):
    if i == n:
        return 1
    if i > n:
        return 0
    if dp[i][j] != -1
        return dp[i][j]
    result = 0
    # we will first try 1 consecutive j, then 2 consecutive j's then 3 and so on
    for count = 1 to A[j]:
        for num = 1 to m:
            if num != j:
                result += solve(i + count, num)
    dp[i][j] = result
    return dp[i][j]

Этот подход уменьшит нашу пространственную сложность до O (10 ^ 6) ~ = 2 Мб, а временная сложность останется прежней: O (N * 6 * 50)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...