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