rev4: очень красноречивый комментарий пользователя Sammaron отметил, что, возможно, этот ответ ранее путал сверху вниз и снизу вверх.Хотя первоначально в этом ответе (rev3) и других ответах говорилось, что «снизу вверх - это запоминание» («примите подзадачи»), он может быть обратным (то есть «сверху вниз» может означать «принять подзадачи» и «снизу вверх "может быть" составить подзадачи ").Ранее я читал о запоминании как о другом типе динамического программирования, в отличие от подтипа динамического программирования.Я цитировал эту точку зрения, хотя и не подписывался на нее.Я переписал этот ответ, чтобы быть независимым от терминологии, пока в литературе не будут найдены надлежащие ссылки.Я также преобразовал этот ответ в вики сообщества.Пожалуйста, предпочитайте академические источники.Список литературы: {Интернет: 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
(В некоторых других редких проблемах это дерево может быть бесконечным в некоторых ветвях, что означает отсутствие завершения и, следовательно,дерева может быть бесконечно большим. Более того, в некоторых задачах вы можете заранее не знать, как выглядит полное дерево. Таким образом, вам может потребоваться стратегия / алгоритм, чтобы решить, какие подзадачи раскрыть.)
Памятка, табуляция
Существует, по крайней мере, два основных метода динамического программирования, которые не являются взаимоисключающими:
Памятка - это подход laissez-faire:Вы предполагаете, что уже вычислили все подзадачи и не знаете, каков оптимальный порядок оценки.Как правило, вы выполняете рекурсивный вызов (или некоторый итерационный эквивалент) из корня и либо надеетесь, что вы приблизитесь к оптимальному порядку оценки, либо получите доказательство того, что вы поможете достичь оптимального порядка оценки.Вы должны убедиться, что рекурсивный вызов никогда не пересчитывает подзадачу, потому что вы кэшируете результаты, и, следовательно, дублированные поддеревья не пересчитываются.
- пример: Если вы вычисляете последовательность Фибоначчи
fib(100)
, вы просто вызовете ее, и она вызовет fib(100)=fib(99)+fib(98)
, что вызовет fib(99)=fib(98)+fib(97)
, ... и т. Д. ..., что вызовет fib(2)=fib(1)+fib(0)=1+0=1
.Затем он, наконец, разрешит fib(3)=fib(2)+fib(1)
, но ему не нужно пересчитывать fib(2)
, потому что мы его кешируем. - Это начинается в верхней части дерева и оценивает подзадачи из листьев / поддеревьев назад.вверх к корню.
Табуляция - Вы также можете думать о динамическом программировании как о алгоритме «заполнения таблицы» (хотя обычно многомерный, эта «таблица» может иметь неЕвклидова геометрия в очень редких случаях *).Это похоже на запоминание, но более активное и включает в себя еще один шаг: вы должны заблаговременно выбрать точный порядок, в котором вы будете выполнять вычисления.Это не должно подразумевать, что заказ должен быть статическим, но что у вас гораздо больше гибкости, чем в памятке.
- пример: Если вы выполняете Фибоначчи, вы можете рассчитатьчисла в следующем порядке:
fib(2)
, fib(3)
, fib(4)
... кэшируют каждое значение, чтобы вам было легче вычислять следующие.Вы также можете думать об этом как о заполнении таблицы (еще одна форма кэширования). - Лично я не часто слышу слово «табуляция», но это очень приличный термин.Некоторые люди считают это «динамическим программированием».
- Перед запуском алгоритма программист рассматривает все дерево, а затем пишет алгоритм для оценки подзадач в определенном порядке по направлению к корню, обычно заполняя таблицу.
- * сноска: иногда таблица'не является прямоугольным столом с сетчатым соединением, как таковым.Скорее, он может иметь более сложную структуру, такую как дерево, или структуру, специфичную для проблемной области (например, города в пределах расстояния полета на карте), или даже решетчатую диаграмму, которая, хотя и имеет вид сетки, не имеетструктура соединения «вверх-вниз-влево-вправо» и т. д. Например, пользователь 3290797 связал пример динамического программирования для нахождения максимального независимого набора в дереве , который соответствует заполнению пробелов в дереве.
(В общем, в парадигме «динамического программирования» я бы сказал, что программист рассматривает все дерево, , а затем пишет алгоритм, который реализуетстратегия для оценки подзадач, которая может оптимизировать любые свойства, которые вы хотите (обычно это сочетание сложности времени и сложности пространства). Ваша стратегия должна начинаться где-то с какой-то конкретной подзадачи и, возможно, может адаптироваться на основе результатов этих оценок.В общем смысле «динамическое программирование», вы можете попробоватьдля кэширования этих подзадач и, в более общем смысле, постарайтесь не пересматривать подзадачи с тонким отличием, возможно, в случае графов в различных структурах данных.Очень часто эти структуры данных в своей основе, как массивы или таблицы.Решения подзадач можно отбросить, если они нам больше не нужны.)
[Ранее в этом ответе делалось утверждение о терминологии «сверху вниз» и «снизу вверх»;ясно, что есть два основных подхода, называемых Мемоизация и табуляция, которые могут быть в биографии с этими терминами (хотя и не полностью).Общий термин, который используют большинство людей, - это «Динамическое программирование», а некоторые говорят «Мемоизация» для обозначения этого конкретного подтипа «Динамическое программирование».В этом ответе отказывается указывать, что является «сверху вниз» и «снизу вверх», пока сообщество не сможет найти надлежащие ссылки в научных статьях.В конечном счете, важно понимать различие, а не терминологию.]
Плюсы и минусы
Простота кодирования
Запоминание кода очень просто (выобычно может * написать аннотацию "memoizer" или функцию-обертку, которая автоматически сделает это за вас), и должна быть вашей первой линией подхода.Недостатком табулирования является то, что вам нужно придумать порядок.
* (на самом деле это легко сделать, только если вы пишете функцию самостоятельно и / или программируете на нечистом / не функциональном языке программирования... например, если кто-то уже написал предварительно скомпилированную функцию fib
, она обязательно делает рекурсивные вызовы сама себе, и вы не можете волшебным образом запоминать функцию, не гарантируя, что эти рекурсивные вызовы вызовут вашу новую запомненную функцию (а не оригинальную немемозированную функцию))
Рекурсивность
Обратите внимание, что как сверху вниз, так и снизу вверх могут быть реализованы с помощью рекурсии или итеративного заполнения таблиц, хотя это может и не быть естественным.
Практические проблемы
При наличии заметки, если дерево очень глубокое (например, fib(10^6)
), вам не хватит места в стеке, потому что каждое отложенное вычисление должно быть помещено в стек, и у вас будет 10 ^ 6 из них.
Оптимальность
Любой подход не может быть оптимальным по времени, если вы выполняете заказ (или пытаетесь)Посещение подзадач не является оптимальным, в частности, если существует несколько способов вычисления подзадачи (обычно это может решить кэширование, но теоретически возможно, что в некоторых экзотических случаях кэширование может быть невозможным).Мемоизация, как правило, добавляет сложность времени к сложности пространства (например, при табулировании у вас больше свободы в отбрасывании вычислений, например, при использовании табуляции в Fib позволяет использовать пространство O (1), но при запоминании в Fib используется O (N).пространство стека).
Расширенные возможности оптимизации
Если вы также решаете чрезвычайно сложные проблемы, у вас может не быть иного выбора, кроме как выполнять табулирование (или, по крайней мере, играть более активную роль в управлении мемоизацией, куда вы хотите ее направить),Кроме того, если вы находитесь в ситуации, когда оптимизация абсолютно необходима, и вы должны оптимизировать, табулирование позволит вам выполнить оптимизацию, которую в противном случае запоминание не позволило бы вам сделать разумным способом.По моему скромному мнению, в обычной разработке программного обеспечения ни один из этих двух случаев никогда не встречался, поэтому я бы просто использовал памятку («функция, которая кэширует свои ответы»), если что-то (например, пространство стека) не делает необходимым табулирование ... хотяТехнически, чтобы избежать выброса стека, вы можете: 1) увеличить предельный размер стека в языках, которые позволяют это, или 2) потреблять постоянный фактор дополнительной работы для виртуализации вашего стека (ick), или 3) программы в стиле передачи продолжения, которыйв действительности также виртуализирует ваш стек (не уверен, что это сложно, но в основном вы будете эффективно извлекать цепочку отложенных вызовов из стека размера N и фактически вставлять ее в N последовательных вложенных функций thunk ... хотя в некоторых языках безДля оптимизации хвостового вызова вам, возможно, придется батутить вещи, чтобы избежать выброса из стека.)
Более сложные примеры
Здесь мы приводим примеры особого интереса, которые не являются общими проблемами DP, но интересно различить памятные запискии табулирование.Например, одна формулировка может быть намного проще, чем другая, или может быть оптимизация, которая в основном требует табуляции:
- алгоритм для вычисления расстояния редактирования [ 4 ],интересен как нетривиальный пример двумерного алгоритма заполнения таблиц