Что такое динамическое программирование?

33

Извините заранее, если этот вопрос звучит глупо ...

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

  1. выразить проблему как рекуррентное отношение;
  2. Реализуйте рекуррентную связь либо через памятку, либо через восходящий подход.

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

Итак, что особенного в динамическом программировании? Что это дает вам, кроме расплывчатого метода решения определенных проблем?

эй эй
источник
11
Исторический фактоид (этот комментарий вам не поможет, но Беллман на самом деле является хорошим лидером, если вы хотите углубиться в теорию динамического программирования): когда Беллман придумал то, что сейчас известно как динамическое программирование, он назвал идею «динамическое программирование». «потому что чисто теоретическая работа не работала в то время с его работодателем, поэтому ему нужно было что-то более модное, которое нельзя было бы использовать в уничижительной форме» .
Г. Бах
3
Насколько я знаю, именно эти два момента вы упоминаете. Он становится особенным, когда избегает экспоненциального взрыва из-за перекрывающихся подзадач. Вот и все. Ах, кстати, мой профессор предпочитает «алгоритмическую парадигму», а не «расплывчатый метод».
Хендрик Ян
«Динамическое программирование», по-видимому, в основном модное слово (с тех пор оно утратило свою популярность). Это не значит, что это не полезно, конечно.
user253751
3
Не заслуживает ответа, но для меня динамическое программирование определенно «то, что вы используете, когда пытаетесь рекурсивно решить проблему, но в конечном итоге вы тратите время на повторное рассмотрение одних и тех же подзадач».
Хоббс
@hobbs: Точно, но умение находить этот первоначальный способ тратить время;)
j_random_hacker

Ответы:

27

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

Методы памяти и восходящие методы дают вам правило / метод для преобразования рекуррентных отношений в код. Мемуализация - относительно простая идея, но лучшие идеи часто бывают!

Динамическое программирование дает вам структурированный способ думать о времени выполнения вашего алгоритма. Время выполнения в основном определяется двумя числами: количеством подзадач, которые вы должны решить, и временем, которое требуется для решения каждой подзадачи. Это обеспечивает удобный и простой способ думать о проблеме разработки алгоритма. Когда у вас есть рекуррентное отношение кандидата, вы можете посмотреть на него и очень быстро понять, каким может быть время выполнения (например, вы часто можете очень быстро определить, сколько будет подзадач, что является нижней границей для время выполнения: если вы решаете экспоненциально много подзадач, то повторение, вероятно, не будет хорошим подходом). Это также поможет вам исключить возможные декомпозиции подзадач. Например, если у нас есть строкаS [ 1 .. i ] S [ j . , n ] S [ i . , j ] n S nS[1..n]определение подзадачи с помощью префикса или суффикса или подстроки может быть разумным (количество подзадач является полиномиальным по ), но определение подзадачи по подпоследовательности вряд ли будет хорошим подходом (число подзадач экспоненциально по ). Это позволяет вам обрезать «пространство поиска» возможных повторений.S[1..i]S[j..n]S[i..j]nSn

Динамическое программирование дает вам структурированный подход для поиска возможных рекуррентных отношений. Эмпирически этот подход часто эффективен. В частности, есть некоторые эвристические / общие шаблоны, которые вы можете распознать для общих способов определения подзадач в зависимости от типа ввода. Например:

  • Если входные данные являются положительным целым числом , один из возможных способов определения подзадачи состоит в замене меньшим целым числом (st ).п п ' 0 N 'пnnn0nn

  • Если входные данные представляют собой строку , некоторые возможные способы определения подзадачи включают: заменить префиксом ; заменить суффиксом ; замените подстрокой . (Здесь подзадача определяется выбором .)S [ 1 .. n ] S [ 1 .. i ] S [ 1 .. n ] S [ j . , n ] S [ 1 .. n ] S [ i . , j ] i , jS[1..n]S[1..n]S[1..i]S[1..n]S[j..n]S[1..n]S[i..j]i,j

  • Если вход представляет собой список , сделайте то же самое, что и для строки.

  • Если входные данные представляют собой дерево , один из возможных способов определения подзадачи состоит в том, чтобы заменить любым поддеревом (т. Е. Выбрать узел и заменить поддеревом, корнем которого является ; подзадача определяется выбором ).Т Т х Т х хTTTxTxx

  • Если вход представляет собой пару , то рекурсивно посмотрите на тип и тип чтобы определить способ выбора подзадачи для каждого. Другими словами, одним из возможных способов определения подзадачи является замена на где - это подзадача для а - это подзадача для . (Вы также можете рассмотреть подзадачи вида или .)x y ( x , y ) ( x , y ) x x y y ( x , y ) ( x , y )(x,y)xy(x,y)(x,y)xxyy(x,y)(x,y)

И так далее. Это дает вам очень полезную эвристику: просто взглянув на сигнатуру метода, вы можете найти список возможных способов определения подзадач. Другими словами, просто взглянув на постановку задачи - рассматривая только типы входных данных - вы можете найти несколько возможных способов определения подзадачи.

Это часто очень полезно. Он не говорит вам, что такое рекуррентное отношение, но когда у вас есть конкретный выбор, как определить подзадачу, часто не так уж сложно разработать соответствующее рекуррентное отношение. Таким образом, он часто превращает разработку алгоритма динамического программирования в структурированный опыт. На бумаге вы записываете список возможных способов определения подзадач (используя эвристику выше). Затем для каждого кандидата вы пытаетесь записать рекуррентное отношение и оцените его время выполнения путем подсчета количества подзадач и времени, затраченного на каждую подзадачу. Испытав каждого кандидата, вы оставляете лучшего, которого смогли найти. Предоставление некоторой структуры процессу разработки алгоритма является основной помощью, так как в противном случае разработка алгоритма может быть пугающей (

DW
источник
Таким образом, вы подтверждаете, что динамическое программирование не обеспечивает конкретных «процедур» для подражания. Это просто «способ думать», как вы сказали. Обратите внимание, что я не утверждаю, что DP бесполезен (наоборот!), Я просто пытаюсь понять, есть ли что-то, чего мне не хватает, или мне просто нужно больше практиковаться.
эй, эй,
@ хейхей, ну да ... и нет. Смотрите мой пересмотренный ответ для более подробной информации. Это не серебряная пуля, но она предоставляет некоторые полубетонные процедуры, которые часто полезны (не гарантированно работают, но часто оказываются полезными).
DW
Большое спасибо! Практикуя, я все больше и больше знакомлюсь с некоторыми из тех «полубетонных процедур», которые вы описываете.
эй, эй,
«если есть экспоненциально много подзадач, которые вы должны решить, то повторение, вероятно, не будет хорошим подходом». Для многих задач не существует известного алгоритма полиномиального времени. Почему это должно быть критерием для использования DP?
Чиль тен Бринке
@Chiel, это не критерий для использования DP. Если у вас возникла проблема, когда вы были бы довольны алгоритмами экспоненциального времени, вы можете проигнорировать это конкретное замечание в скобках. Это просто пример, чтобы попытаться проиллюстрировать общую мысль, которую я высказал, - не то, к чему вы должны относиться слишком серьезно или интерпретировать как жесткое правило.
DW
9

Ваше понимание динамического программирования является правильным ( afaik ), и ваш вопрос оправдан.

Я думаю, что дополнительное пространство проектирования, которое мы получаем из вида рецидивов, которые мы называем «динамическим программированием», лучше всего видно по сравнению с другими схемами рекурсивных подходов.

Давайте представим, что наши входные данные - это массивы чтобы выделить понятия.A[1..n]

  1. Индуктивный подход

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

    f(A)=g(f(A[1..nc]),A)

    с функция / алгоритм, который переводит решение.g

    Пример: поиск суперзвезд за линейное время

  2. Разделяй и властвуй

    Разделите входные данные на несколько более мелких частей, решите проблему для каждой и объедините. Схематично (на две части),

    f(A)=g(f(A[1..c]),f(A[c+1..n]),A) .

    Примеры: Merge- / Quicksort, Наименьшее попарное расстояние на плоскости

  3. Динамическое программирование

    Рассмотрите все способы разделения проблемы на более мелкие проблемы и выберите лучший. Схематично (на две части),

    f(A)=best{g(f(A[1..c]),f(A[c+1..n]))|1cn1} .

    Примеры: Редактировать расстояние, Проблема изменения

    Важное примечание: динамическое программирование - это не грубая сила ! Применение на каждом этапе значительно сокращает пространство поиска.best

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

Урок изучения динамического программирования является то , что это нормально , чтобы попробовать все возможные разбиения (ну, это необходимо для корректности) , потому что он все еще может быть эффективным использованием запоминания.

Рафаэль
источник
«Обрезанное динамическое программирование» (когда оно применяется) доказывает, что для правильности не требуется использовать все возможности.
Бен Фойгт
@BenVoigt Конечно. Я оставался намеренно расплывчатым в том, что означает «все способы разделения»; конечно, вы хотите исключить как можно больше! (Однако, даже если вы попробуете все способы разделения, вы не получите грубую силу, поскольку вы только когда-либо исследуете комбинации оптимальных решений для подзадач, тогда как грубая сила будет исследовать все комбинации всех решений.)
Рафаэль
5

Динамическое программирование позволяет вам обменять память на время вычислений. Рассмотрим классический пример Фибоначчи.

Фибоначчи определяется повторением . Если вы решите использовать эту рекурсию, вы в конечном итоге сделаете вызовов , поскольку дерево рекурсии представляет собой двоичное дерево с высотой .Fib(n)=Fib(n1)+Fib(n2)O(2n)Fib()n

Вместо этого вы хотите вычислить , затем использовать это, чтобы найти , использовать это, чтобы найти и т. Д. Это займет всего время.Fib(2)Fib(3)Fib(4)O(n)

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

Kittsil
источник
1
Вы говорите только о той части запоминания, которая упускает суть вопроса.
Рафаэль
1
«Динамическое программирование позволяет вам обменять память на время вычислений» - это не то, что я слышал, когда учился в старших классах, и это отличный способ взглянуть на эту тему. Это интуитивный ответ с кратким примером.
TruShot
@trueshot: За исключением того, что иногда динамическое программирование (и, в частности, «Динамическое программирование с отсечением») способно уменьшить как временные, так и пространственные требования.
Бен Фойгт
@ Я не говорил, что это сделка один на один. Вы также можете обрезать рекуррентное дерево. Я полагаю, что я ответил на вопрос: «Что нам дает DP?» Это дает нам более быстрые алгоритмы, торгуя пространством для времени. Я согласен, что принятый ответ является более тщательным, но это также верно.
Китцил
2

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

Позвольте мне взять в качестве примера проблему нахождения числа возрастающих подпоследовательностей длины в массиве длины . Полезно разбить множество всех подпоследовательностей на классы эквивалентности так, чтобы две подпоследовательности принадлежали одному и тому же классу, если и только если они имеют одинаковую длину и заканчиваются в одном и том же индексе. Все возможных подпоследовательностей принадлежат ровно одному из классов эквивалентности. Такое разбиение сохраняет достаточно информации, чтобы мы могли определить рекуррентное соотношение для размеров классов. Если дает число подпоследовательностей, которые заканчиваются индексом и имеют длинуkAn2nO(n2)f(i,)iтогда имеем:

f(i,)=j<i such thatA[j]<A[i]f(j,1)
f(i,1)=1 for all i=1n

Это повторение решает проблему во времени .O(n2k)

jnalanko
источник