rev4: Очень красноречивый комментарий пользователя Sammaron отметил, что, возможно, в этом ответе ранее путались нисходящие и восходящие. Хотя первоначально в этом ответе (rev3) и в других ответах говорилось, что «снизу вверх - это мемоизация» («предполагать подзадачи»), это может быть обратное (то есть «сверху вниз» может быть «предполагать подзадачи» и « снизу вверх "может быть" составить подзадачи "). Раньше я читал о мемоизации как о другом виде динамического программирования в отличие от подтипа динамического программирования. Я цитировал эту точку зрения, хотя и не подписывался под ней. Я переписал этот ответ, чтобы не зависеть от терминологии, пока в литературе не будут найдены соответствующие ссылки. Я также преобразовал этот ответ в вики сообщества. Пожалуйста, отдайте предпочтение академическим источникам. Список ссылок: {Web: 1, 2} {Литература: 5}
Резюме
Суть динамического программирования заключается в упорядочивании вычислений таким образом, чтобы избежать повторного вычисления дублирующей работы. У вас есть основная проблема (корень вашего дерева подзадач) и подзадачи (поддеревья). Подзадачи обычно повторяются и перекрываются.
Например, рассмотрим ваш любимый пример Фибонначи. Это полное дерево подзадач, если мы сделали наивный рекурсивный вызов:
TOP of the tree
fib(4)
fib(3)...................... + fib(2)
fib(2)......... + fib(1) fib(1)........... + fib(0)
fib(1) + fib(0) fib(1) fib(1) fib(0)
fib(1) fib(0)
BOTTOM of the tree
(В некоторых других редких задачах это дерево может быть бесконечным в некоторых ветвях, представляя незавершенность, и, таким образом, нижняя часть дерева может быть бесконечно большой. Кроме того, в некоторых задачах вы можете не знать, как выглядит полное дерево перед времени. Таким образом, вам может потребоваться стратегия / алгоритм, чтобы решить, какие подзадачи выявить.)
Мемоизация, табуляция
Существует как минимум два основных метода динамического программирования, которые не исключают друг друга:
Мемоизация - это подход невмешательства: вы предполагаете, что уже вычислили все подзадачи и что вы не знаете, каков оптимальный порядок оценки. Как правило, вы выполняете рекурсивный вызов (или некоторый итерационный эквивалент) из корня и либо надеетесь, что приблизитесь к оптимальному порядку оценки, либо получите доказательство того, что вы поможете вам достичь оптимального порядка оценки. Вы можете гарантировать, что рекурсивный вызов никогда не будет повторно вычислять подзадачу, потому что вы кешируете результаты, и, таким образом, повторяющиеся поддеревья не пересчитываются.
- example: If you are calculating the Fibonacci sequence
fib(100)
, you would just call this, and it would call fib(100)=fib(99)+fib(98)
, which would call fib(99)=fib(98)+fib(97)
, ...etc..., which would call fib(2)=fib(1)+fib(0)=1+0=1
. Then it would finally resolve fib(3)=fib(2)+fib(1)
, but it doesn't need to recalculate fib(2)
, because we cached it.
- Это начинается с вершины дерева и оценивает подзадачи от листьев / поддеревьев до корня.
Табулирование - вы также можете думать о динамическом программировании как об алгоритме «заполнения таблицы» (хотя обычно эта «таблица» многомерна, в очень редких случаях эта «таблица» может иметь неевклидову геометрию *). Это похоже на мемоизацию, но более активную и включает в себя один дополнительный шаг: вы должны заранее выбрать точный порядок, в котором вы будете выполнять свои вычисления. Это не должно означать, что порядок должен быть статическим, но у вас гораздо больше гибкости, чем у мемоизации.
- example: If you are performing fibonacci, you might choose to calculate the numbers in this order:
fib(2)
,fib(3)
,fib(4)
... caching every value so you can compute the next ones more easily. You can also think of it as filling up a table (another form of caching).
- Я лично не часто слышу слово «табуляция», но это очень приличный термин. Некоторые считают это «динамическим программированием».
- Перед запуском алгоритма программист рассматривает все дерево, затем пишет алгоритм для оценки подзадач в определенном порядке по направлению к корню, обычно заполняя таблицу.
- * сноска: иногда «таблица» не является прямоугольной таблицей с сеточной связью как таковой. Скорее, он может иметь более сложную структуру, такую как дерево, или структуру, специфичную для проблемной области (например, города в пределах дальности полета на карте), или даже решетчатую диаграмму, которая, хотя и похожа на сетку, не имеет структура подключения вверх-вниз-влево-вправо и т. д. Например, user3290797 связал пример динамического программирования поиска максимальный независимый набор в дереве, который соответствует заполнению пробелов в дереве.
(В самом общем случае, в парадигме «динамического программирования», я бы сказал, что программист рассматривает все дерево целиком, затем пишет алгоритм, реализующий стратегию оценки подзадач, которая может оптимизировать любые свойства, которые вы хотите ( обычно сочетание временной сложности и пространственной сложности). Ваша стратегия должна где-то начинаться с какой-то конкретной подзадачи и, возможно, может адаптироваться на основе результатов этих оценок. В общем смысле "динамического программирования" вы можете попробовать чтобы кэшировать эти подзадачи и, в более общем плане, старайтесь избегать повторения подзадач с тонкими различиями, возможно, в случае графов в различных структурах данных. Очень часто эти структуры данных лежат в основе своей, как массивы или таблицы. Решения подзадач можно выбросить. если они нам больше не нужны.)
[Ранее в этом ответе говорилось о нисходящей и восходящей терминологии; очевидно, что существует два основных подхода, называемых мемоизацией и табулированием, которые могут противоречить этим терминам (хотя и не полностью). Общий термин, который большинство людей использует по-прежнему, - «динамическое программирование», а некоторые люди говорят «мемоизация», имея в виду этот конкретный подтип «динамического программирования». В этом ответе не говорится, какой из них идет сверху вниз или снизу вверх, до тех пор, пока сообщество не найдет подходящие ссылки в научных статьях. В конце концов, важно понимать различие, а не терминологию.]
За и против
Легкость кодирования
Мемоизацию очень легко кодировать (обычно вы * можете написать аннотацию или функцию-оболочку «мемоизатора», которая автоматически сделает это за вас), и она должна быть вашим первым подходом. Обратной стороной табуляции является то, что вам нужно придумать порядок.
* (на самом деле это просто, если вы сами пишете функцию и / или кодируете на нечистом / нефункциональном языке программирования ... например, если кто-то уже написал предварительно скомпилированную fib
функцию, он обязательно выполняет рекурсивные вызовы к себе, и вы не можете волшебным образом запоминать функцию без обеспечения того, чтобы эти рекурсивные вызовы вызывали вашу новую запомненную функцию (а не исходную немомизированную функцию))
Рекурсивность
Обратите внимание, что как сверху вниз, так и снизу вверх можно реализовать рекурсию или итеративное заполнение таблицы, хотя это может быть неестественным.
Практические проблемы
С мемоизацией, если дерево очень глубокое (например, fib(10^6)
), у вас закончится пространство стека, потому что каждое отложенное вычисление должно быть помещено в стек, и у вас будет 10 ^ 6 из них.
Оптимальность
Любой из подходов может быть неоптимальным по времени, если порядок, в котором вы происходите (или пытаетесь) посещать подзадачи, не является оптимальным, в частности, если существует более одного способа вычисления подзадачи (обычно кэширование решает эту проблему, но теоретически возможно, что кеширование может не в каких-то экзотических случаях). Мемоизация обычно добавляет вашу временную сложность к вашей пространственной сложности (например, с табуляцией у вас больше свободы отбрасывать вычисления, например, использование табуляции с Fib позволяет вам использовать пространство O (1), но мемоизация с Fib использует O (N) пространство стека).
Расширенная оптимизация
Если вы также решаете чрезвычайно сложные задачи, у вас может не быть другого выбора, кроме как делать табуляции (или, по крайней мере, играть более активную роль в управлении мемоизацией, куда вы хотите). Кроме того, если вы находитесь в ситуации, когда оптимизация абсолютно критична и вы должны оптимизировать, табуляция позволит вам выполнить оптимизацию, которую мемоизация в противном случае не позволила бы вам сделать разумным способом. По моему скромному мнению, в обычной разработке программного обеспечения ни один из этих двух случаев никогда не возникает, поэтому я бы просто использовал мемоизацию («функцию, которая кэширует свои ответы»), если что-то (например, пространство стека) не делает табуляцию необходимой ... хотя технически, чтобы избежать выброса стека, вы можете 1) увеличить предел размера стека на языках, которые это позволяют, или 2) съесть постоянный фактор дополнительной работы для виртуализации вашего стека (ick), или 3) программу в стиле с продолжением передачи, что по сути, также виртуализирует ваш стек (не уверен в сложности этого, но в основном вы эффективно берете цепочку отложенных вызовов из стека размера N и де-факто вставляете ее в N последовательно вложенных функций преобразователя ... хотя на некоторых языках без оптимизация хвостового вызова, вам, возможно, придется прыгать с трамплина, чтобы избежать выброса стека).
Более сложные примеры
Здесь мы перечисляем примеры, представляющие особый интерес, которые не являются просто общими проблемами DP, но интересно различают мемоизацию и табуляцию. Например, одна формулировка может быть намного проще, чем другая, или может быть оптимизация, которая в основном требует табуляции:
- алгоритм вычисления расстояния редактирования [4], интересный как не -тривиальный пример двумерного алгоритма заполнения таблицы
person
Community
schedule
29.05.2011