Рубрики
Без рубрики

Демистифицирующее динамическое программирование

Автор оригинала: FreeCodeCapm Team.

Алайна Кафкесом

Как построить и код динамические алгоритмы программирования

Может быть, вы слышали об этом в подготовке к кодированию интервью. Может быть, вы боролись через это в курсе алгоритма. Может быть, вы пытаетесь научиться кодировать самостоятельно, и где-то рассказали, что важно понимать динамическое программирование. Использование динамического программирования (DP) для записи алгоритмов так же необходим, как он боялся.

А кто может обвинить тех, кто уклоняется от этого? Динамическое программирование кажется запугиванием, потому что оно плохо учили. Многие учебные пособия сосредоточены на результате – Объясняя алгоритм, вместо процесса – Найти алгоритм. Это поощряет запоминание, а не понимание.

Во время класса моих алгоритмов в этом году я собрал свой собственный процесс решения проблем, требующих динамического программирования. Части этого приходят из Мои алгоритмы профессор (Для кого большой кредит должен!) И запчасти из моего собственного рассечения динамических алгоритмов программирования.

Но прежде чем я делюсь своим процессом, давайте начнем с основы. Что такое динамическое программирование, в любом случае?

Динамическое программирование определено

Динамическое программирование составляет Разбиение задачи оптимизации в более простые подзаботы, а Хранение решения для каждой подзабойной проблемы Так что каждая подзадача решается только один раз.

Если честно, это определение может не составить полное смысл, пока вы не увидите пример подзабойной проблемы. Это нормально, он придет в следующем разделе.

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

В следующих двух разделах я объясню, что a Подзадача Есть, а затем мотивировать зачем хранение решений – техника, известная как мемузаризация – Вопросы в динамическом программировании.

Подзаботы по подпространениям по подзаготаниям

Подпространение являются меньшими версиями оригинальной проблемы. Фактически, суб-проблемы часто выглядят как релегированная версия оригинальной проблемы. Если правильно сформулирован, подпростоки опираются друг на друга, чтобы получить решение первоначальной проблемы.

Чтобы дать вам лучшее представление о том, как это работает, давайте найдем подпрофильную проблему в примере проблемы динамической программирования.

Притворись, что вы вернулись в 1950-х годах, работающих на компьютере IBM-650. Вы знаете, что это значит – Pundcards! Ваша работа – человек, или женщина, IBM-650 на день. Вам дано естественное число N Pundcards для бега. Каждая перфорация Я должно быть запущено при определенном заданном времени начала S_I и перестать работать в некотором заданном время окончания F_I Отказ Только одна перфорация может работать на IBM-650 одновременно. Каждая перфорация также имеет связанное значение V_I Основываясь на том, насколько это важно для вашей компании.

Проблема В качестве лица, отвечающего за IBM-650, вы должны определить оптимальное расписание ParkCards, которые максимизируют общую стоимость выполнения всех Partcards.

Поскольку я пройду этот пример значительно в этой статье, я только дразню вас с его подпрозонной проблемой:

Подзадача : Максимальный график значений для Pundcards Я через N Таким образом, что перфораторы отсортированы по времени начала.

Обратите внимание, как подзадача разбивает исходную проблему в компоненты, которые создают решение. С подзором вы можете найти максимальный график значений для Punkcards N-1 через N , а затем для Pundcards N-2 через N , и так далее. Находя решения для каждой отдельной подзабойной проблемы, вы можете затем решить саму оригинальную проблему: максимальный график значений для PartCards 1 через N Отказ Поскольку подпроформация выглядит как оригинальная проблема, подзаголовки могут быть использованы для решения исходной проблемы.

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

Мотивация воспоминания с числами фибоначчи

Когда он сказал реализовать алгоритм, который рассчитывает Фибоначчи ценность За любой данный номер, что бы вы сделали? Большинство людей, которых я знаю, выбрал бы Рекурсивный алгоритм Это выглядит что-то вроде этого в Python:

def fibonacciVal(n):  if n == 0:    return 0  elif n == 1:    return 1  else:    return fibonacciVal(n-1) + fibonacciVal(n-2)

Этот алгоритм достигает своей цели, но на огромный Стоимость. Например, давайте посмотрим на то, что этот алгоритм должен рассчитать, чтобы решить для (сокращенного как F (5)):

F(5)                      /      \                                     /        \                  /          \               F(4)          F(3)            /       \        /   \          F(3)     F(2)     F(2)  F(1)         /   \     /  \     /   \       F(2) F(1) F(1) F(0) F(1) F(0)       /  \     F(1) F(0)

Вышеемое дерево представляет каждое вычисление, которое необходимо сделать, чтобы найти значение FIBONACCI для. Обратите внимание, как решается подзадача для решена трижды. Для относительно небольшого примера), это много повторяющихся и потрачено впустую, вычисление!

Что если, вместо того, чтобы рассчитать значение FIBONACCI в течение трех раз, мы создали алгоритм, который вычисляет его один раз, сохраняет его значение и обращается к сохраненному значению FIBONACCI для каждого последующего вхождения? Это точно какая мемузация делает.

Имея в виду, я написал динамическое программирование решением задачи значения FIBONACCI:

def fibonacciVal(n):  memo = [0] * (n+1)  memo[0], memo[1] = 0, 1  for i in range(2, n+1):    memo[i] = memo[i-1] + memo[i-2]  return memo[n]

Обратите внимание, как решение возвращаемого значения исходит из массива Memoization Memo [], что итеративно заполняется циклом для цикла. «Итеративно», я имею в виду, что Memo [2] рассчитывается и хранится до памятки [3], Memo [4], … и Memo [ N . Поскольку MEMO [] заполняется в этом порядке, решение для каждой подпростоки) можно решить решениями ее предшествующими подпространениями и), поскольку эти значения уже хранялись в записке [] в более раннее время.

Воспоминание означает отсутствие повторных вычислений, что делает для более эффективного алгоритма. Таким образом, воспоминание гарантирует, что динамическое программирование эффективно, но он выбирает правую подпрофильную проблему, которая гарантирует, что динамическая программа проходит через все возможности, чтобы найти лучший.

Теперь, когда мы обратились к мемузаризации и подпространениям, пришло время изучать процесс динамического программирования. Пряжка в.

Мой процесс динамического программирования

Шаг 1: Определите подзагонку в словах.

Слишком часто программисты превратится в запись кода до критически думать о проблеме под рукой. Нехорошо. Одна стратегия для стрельбы вашего мозга перед тем, как прикоснуться к клавиатуре, использует слова, английский или иным образом, чтобы описать подпрофильную проблему, которую вы определили в пределах исходной проблемы.

Если вы решаете проблему, которая требует динамического программирования, захватите лист бумаги и подумайте о том, что вам нужно решить эту проблему. Выписывайте подзагонку с этим в виду.

Например, в проблеме Перэнткарда я заявил, что подпроставление может быть написано как «Максимальное значение значений для Partcards i через n Таким образом, что Partcards отсортированы по времени начала». Я нашел эту подзадачую проблему, полагая, что, чтобы определить максимальный график значений для Punkcards 1 через N Таким образом, что перфораторы отсортированы по времени начала, мне нужно было бы найти ответ на следующие подпроставления:

  • Максимальный график значений для Pundcards N-1 через N Таким, что перфораторы отсортированы по времени начала
  • Максимальный график значений для Pundcards N-2 через N Таким, что перфораторы отсортированы по времени начала
  • Максимальный график значений для Pundcards N-3 через N Таким, что перфораторы отсортированы по времени начала
  • (Et cetea)
  • Максимальное значение значений для Pundcards 2 через N Таким, что перфораторы отсортированы по времени начала

Если вы можете определить подпрофильную проблему, которая опирается на предыдущие под-проблемы, чтобы решить проблему под рукой, вы находитесь на правильном пути.

Шаг 2: Выписывайте подпрофилью как повторяющееся математическое решение.

Как только вы определили подзагонку в словах, пришло время писать его математически. Почему? Ну, математический Рецидив, Или повторное решение, которое вы найдете, в конечном итоге будут то, что вы вводите в свой код. Кроме того, выписывайте подпрофильную проблему, математически ветеринаруют вашу подзадачую проблему в словах с шага 1. Если трудно кодировать свою подзагонку с шага 1 в математике, то это может быть неверной подзабойной проблемой!

Есть два вопроса, которые я задаю себе каждый раз, когда пытаюсь найти рецидива:

  • Какое решение я делаю на каждом шаге?
  • Если мой алгоритм на шаге Я какая информация должна ли она решить, что делать на шаге Я + 1 ? (И иногда: если мой алгоритм на шаге I , какая информация должна была решить, что делать на шаге I-1 ?)

Давайте вернемся к проблеме PartCard и задайте эти вопросы.

Какое решение я делаю на каждом шаге? Предположим, что перфораторы отсортированы по времени начала, как упоминалось ранее. Для каждой перфорации, которая совместима с графиком до сих пор (время начала – после времени окончания PartCard, которая в данный момент работает), алгоритм должен выбрать между двумя вариантами: для запуска или не запускать PartCard.

Если мой алгоритм на шаге Я какая информация должна ли она решить, что делать на шаге Я + 1 ? Чтобы выбрать между двумя вариантами, алгоритм должен знать следующую совместимую перфорацию в порядке. Следующая совместимая PartCard для данной перфории P это перфорация Q Такое, что S_Q (заранее определенное время начала для Punchcard Q ) происходит после F_P (заранее определенное время отделки для Punchcard P ) И разница между S_Q и F_P минимизируется. Отказ от математика – говорящий, следующая совместимая перфорация – это тот, с самым ранним временем начала после завершения нынешней перфории заканчивается.

Если мой алгоритм на шаге Я какая информация должна была решить, что делать на шаге I-1 ? Алгоритм нужно знать о будущих решениях: те, которые сделаны для Pundcards Я через N Для того, чтобы решить запустить или не запустить PartCard I-1 Отказ

Теперь, когда мы ответили на эти вопросы, возможно, вы начали сформировать повторяющееся математическое решение в вашем уме. Если нет, это тоже хорошо, становится легче писать рецидивов, когда вы подвергаетесь воздействию более динамичных проблем программирования.

Без дальнейшего ADO вот наше рецидивирование:

OPT(i) = max(v_i + OPT(next[i]), OPT(i+1))

Этот математический рецидив требует некоторых объяснений, особенно для тех, кто еще не написал. Я использую opt ( i ), чтобы представлять максимальный график значений для Partcards Я через N Таким образом, что перфораторы отсортированы по времени начала. Звучит знакомо, верно? Opt (•) – наша подзадача от шага 1.

Чтобы определить значение opt ( i ), рассмотрим два варианта, и мы хотим взять Максимум Из этих вариантов для удовлетворения нашей цели: Максимум Расписание ценностей для всех Partcards. Как только мы выберем вариант, который дает максимальный результат на шаге Я Мы восполняем его ценность как opt ( I

Два варианта – бегать или не запустить PartCard Я – представлены математически следующим образом:

v_i + OPT(next[i])

Этот пункт представляет решение запустить PartCard Я Отказ Это добавляет значение, полученное из бегущей перфории Я Выбирать (Далее [ I ]), где дальше [ Я ] представляет следующую совместимую перфорирую после перфоратора Я Отказ Opt (Далее [ I ]) дает максимальный график значений для PartCards Next [ Я ] через N Таким образом, что перфораторы отсортированы по времени начала. Добавление этих двух значений вместе производит максимальный график значений для PartCards Я через N Такое, что перфораторы отсортированы по времени начала, если Pundcard Я бежит

OPT(i+1)

И наоборот, этот пункт представляет решение не запускать PartCard Я Отказ Если Pundcard Я не работает, его значение не получено. Opt ( i + 1 ) дает максимальный график значений для Pundcards Я + 1 через N Таким образом, что перфораторы отсортированы по времени начала. Итак, opt ( i + 1 ) дает максимальный график значений для Pundcards Я через N Такое, что перфораторы отсортированы по времени начала, если Pundcard Я не работает.

Таким образом, решение, принятое на каждом этапе проблем перфоратора, математически кодируется, чтобы отразить подпрофильную проблему на шаге 1.

Шаг 3: Решите исходную проблему, используя шаги 1 и 2.

На шаге 1 мы записали подзаголовую проблему для проблемы перфорации в словах. На шаге 2 мы записали повторяющееся математическое решение, которое соответствует этим подпространениям. Как мы можем решить оригинальную проблему с этой информацией?

OPT(1)

Это так просто. Поскольку подзабойная проблема, которую мы нашли на шаге 1, является максимальным графиком значений для PartCards Я через N Таким образом, что Pundcards отсортированы по времени начала, мы можем записать решение оригинальной задачи в качестве максимального графика значений для Pundcards 1 через N Таким образом, что перфораторы отсортированы по времени начала. Поскольку шаги 1 и 2 идут рука об руку, оригинальная проблема также может быть записана как opt (1).

Шаг 4: Определите размеры массива памяти и направления, в котором он должен быть заполнен.

Вы нашли шаг 3 обманчиво просто? Это, безусловно, кажется таким образом. Вы можете думать, как выбрать (1) быть решением нашей динамической программы, если она опирается на opt (2), выбрать (дальше [1]), и так далее?

Вы правильно заметить, что opt (1) полагается на решение для определения (2). Это следует напрямую с шага 2:

OPT(1) = max(v_1 + OPT(next[1]), OPT(2))

Но это не сокрушительная проблема. Думайте обратно к примеру мемузации FIBONACCI. Чтобы найти значение FIBONACCI для N Алгоритм полагается на то, что ценности фибоначчи для N , N , N , N и N были уже вомены. Если мы заполним нашу таблицу памяти в правильном порядке, зависимость от opt (1) в других подпространяемых проблемах не имеет большого значения.

Как мы можем определить правильное направление для заполнения таблицы памяти? В задаче перфоратора, поскольку мы знаем, opt (1), опирается на решения для определения (2) и выбрать (следующую [1]), а следующие Pundcards 2 и следующие [1] имеют время начала после первой карты 1 из-за сортировки, мы Можно сделать вывод, что нам нужно заполнить нашу таблицу памяти от opt ( n ), чтобы выбрать (1).

Как мы определяем размеры этого массива памяти? Вот трюк: размеры массива равны количеству и размеру переменных, на которые опираются (•) полагаются. В заданной проблеме у нас есть opt ( i ), что означает, что opt (•) только зависит от переменной Я , который представляет номер перфории. Это предполагает, что наша массив памяти будет одномерным и что его размер будет N Так как есть N Всего Pundcards.

Если мы знаем, что N Затем наша массив памяти может выглядеть так:

memo = [OPT(1), OPT(2), OPT(3), OPT(4), OPT(5)]

Однако, потому что многие языки программирования Начните массивы индексации в 0 , может быть удобнее создавать эту мемузаризацию, чтобы его индексы выровняли с номерами перфорации:

memo = [0, OPT(1), OPT(2), OPT(3), OPT(4), OPT(5)]

Шаг 5: Код это!

Чтобы кодировать нашу динамическую программу, мы собрали шаги 2-4. Единственная новая информация о том, что вам нужно написать динамическую программу, является базовым случаем, который вы можете найти так, как вы Tinker с вашим алгоритмом.

Динамическая программа для проблемы перфорации будет выглядеть что-то подобное:

def punchcardSchedule(n, values, next): # Initialize memoization array - Step 4  memo = [0] * (n+1)   # Set base case  memo[n] = values[n]   # Build memoization table from n to 1 - Step 2  for i in range(n-1, 0, -1):    memo[i] = max(v_i + memo[next[i]], memo[i+1])  # Return solution to original problem OPT(1) - Step 3  return memo[1]

Поздравляю с написанием вашей первой динамической программы! Теперь, когда вы сможете ноги, я пойду за собой другой тип динамической программы.

Парадокс выбора: несколько опций DP пример

Хотя предыдущий пример динамического программирования был двусторонним решением – для запуска или не запустить ParterCard – некоторые проблемы требуют, чтобы несколько вариантов были рассмотрены до того, как решение может быть сделано на каждом шаге.

Время для нового примера.

Притворяться, что вы продаете браслеты дружбы к N Клиенты, и стоимость этого продукта увеличивается монотонно. Это означает, что товар имеет цены { P_1 , …, P_N } такой, что p_i ≤ p_j Если клиент J заходит после клиента Я Отказ Эти N У клиентов есть ценности { v_1 , …, V_N } Данный клиент Я Куплю браслет дружбы по цене P_I Если и только если P_IV_I ; В противном случае доходы, полученные от этого клиента, это 0. Предположим, что цены являются натуральными числами.

Проблема : Вы должны найти набор цен, которые гарантируют вам максимально возможный доход от продажи браслетов дружбы.

Войдите секунду, чтобы подумать о том, как вы можете решить эту проблему, прежде чем смотреть на мои решения на шаги 1 и 2.

Шаг 1: Определите подзагонку в словах.

Подзадача : Максимальный доход, полученный от клиентов Я через N Такое, что цена для клиента I-1 был установлен на Q Отказ

Я нашел эту подзамучую проблему, повлияя, что определить максимальный доход для клиентов 1 через N Мне нужно было найти ответ на следующие подпроставления:

  • Максимальный доход, полученный от клиентов N-1 через N Такое, что цена для клиента N-2 был установлен на Q Отказ
  • Максимальный доход, полученный от клиентов N-2 через N Такое, что цена для клиента N-3 был установлен на Q Отказ
  • (Et cetea)

Обратите внимание, что я представил вторую переменную Q в подпростаю. Я сделал это, потому что, чтобы решить каждую подзамучую проблему, мне нужно знать цену, которую я установил для клиента до эта подзадача. Переменная Q обеспечивает монотонный характер набора цен и переменной Я Отслеживает текущий клиент.

Шаг 2: Выписывайте подпрофилью как повторяющееся математическое решение.

Есть два вопроса, которые я задаю себе каждый раз, когда пытаюсь найти рецидива:

  • Какое решение я делаю на каждом шаге?
  • Если мой алгоритм на шаге Я какая информация должна ли она решить, что делать на шаге Я + 1 ? (И иногда: если мой алгоритм на шаге I , какая информация должна ли она решить, что делать на шаге I-1 ?)

Вернемся к проблеме браслета дружбы и задать эти вопросы.

Какое решение я делаю на каждом шаге? Я решаю, по которой цена продает мой браслет дружбы к текущему клиенту. Поскольку цены должны быть натуральными числами, я знаю, что я должен установить свою цену за клиенту Я В диапазоне от Q – Цена набор для клиента I-1 – к V_I – максимальная цена, на которой клиент Я купит браслет дружбы.

Если мой алгоритм на шаге Я какая информация должна ли она решить, что делать на шаге Я + 1 ? Мой алгоритм должен знать цену набор для клиентов Я и стоимость клиента Я + 1 Для того, чтобы решить, какой естественный номер для установки цены на клиента Я + 1 Отказ

С этими знаниями я могу математически выписать рецидив:

OPT(i,q) = max~([Revenue(v_i, a) + OPT(i+1, a)])
such that max~ finds the maximum over all a in the range q ≤ a ≤ v_i

Еще раз, этот математический рецидив требует некоторых объяснений. Поскольку цена на клиента I-1 это Q для клиента Я , цена А Либо оставаться в целом Q или это меняется, чтобы быть каким-то целым числом между Q + 1 и V_I Отказ Чтобы найти общий доход, мы добавляем доход от клиента Я к максимальному доходу, полученному от клиентов Я + 1 через N Такое, что цена для клиента Я был установлен на А Отказ

Другими словами, чтобы максимизировать общий доход, алгоритм должен найти оптимальную цену для клиента Я Проверяя все возможные цены между Q и V_I Отказ Если V_IQ Тогда цена А должен остаться в Q Отказ

Как насчет других шагов?

Работа через шаги 1 и 2 является наиболее сложной частью динамического программирования. Как упражнение, я предлагаю вам работать через шаги 3, 4 и 5 самостоятельно, чтобы проверить ваше понимание.

Анализ выполнения динамических программ

Теперь для забавной части алгоритмов записи: анализ времени выполнения. Я буду использовать нотацию Big-O на протяжении всего этого обсуждения. Если вы еще не знакомы с Big-O, я предлагаю вам прочитать на нем здесь Отказ

Как правило, время выполнения динамической программы состоит из следующих функций:

  • Предварительная обработка
  • Сколько раз то для петли работает
  • Сколько времени требуется рецидивирование, чтобы запустить в один для итерации петли
  • Постобработка

В целом, время выполнения принимает следующую форму:

Pre-processing + Loop * Recurrence + Post-processing

Давайте выполним анализ выполнения задач Partcard, чтобы ознакомиться с Big-O для динамических программ. Вот главная программа Dynamic Partcard:

def punchcardSchedule(n, values, next): # Initialize memoization array - Step 4  memo = [0] * (n+1)   # Set base case  memo[n] = values[n]   # Build memoization table from n to 1 - Step 2  for i in range(n-1, 0, -1):    memo[i] = max(v_i + memo[next[i]], memo[i+1])  # Return solution to original problem OPT(1) - Step 3  return memo[1]

Давайте сломаем свое время выполнения:

  • Предварительная обработка: здесь это означает, что строит массив памяти. O ( n ).
  • Сколько раз то для петли работает: O ( n ).
  • Сколько времени требуется рецидив для запуска в один для итерации петли: рецидив занимает постоянное время для запуска, потому что он принимает решение между двумя вариантами в каждой итерации. O (1).
  • Почтовая обработка: нет здесь! O (1).

Общее время выполнения программы WARDCARD SPLEAL DIANDIC IS ( N ) O ( N ) * O (1) + O (1) или, в упрощенной форме, O ( n ).

Ты сделал это!

Ну, вот и это – ты один шаг ближе, чтобы стать динамическим мастером программирования!

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

Так что выйдите туда и возьмите интервью, классы и Жизнь (Конечно) с вашими новому динамическими программированными знаниями!

Большое спасибо за Стивен Беннетт , Клэр Дюранд и Prithaj Nath Для корректировки этого поста. Спасибо …| Профессор Hartline Для того, чтобы получить меня так взволнованным о динамическом программировании, которое я написал об этом по длине.

Наслаждайтесь тем, что вы читаете? Распространите любовь, понравив и делись этим произведением. Есть мысли или вопросы? Обратитесь к мне на Twitter или в комментариях ниже.