Динамическое программирование и сходства "разделяй и властвуй"
На данный момент я могу сказать, что динамическое программирование является расширением парадигмы «разделяй и властвуй» .
Я бы не стал относиться к ним как к чему-то совершенно другому. Потому что они оба работают, рекурсивно разбивая проблему на две или более подзадач одного и того же или родственного типа, пока они не станут достаточно простыми для непосредственного решения. Затем решения подзадач объединяются, чтобы дать решение исходной проблемы.
Так почему же тогда у нас все еще есть разные названия парадигм и почему я назвал динамическое программирование расширением. Это связано с тем, что подход динамического программирования может быть применен к проблеме только в том случае, если проблема имеет определенные ограничения или предпосылки . И после этого динамическое программирование расширяет подход «разделяй и властвуй» с помощью техники запоминания или табуляции .
Пошагово…
Предпосылки / ограничения динамического программирования
Как мы только что обнаружили, есть два ключевых атрибута, которые должны иметь проблема «разделяй и властвуй», чтобы динамическое программирование было применимо:
Оптимальная подструктура - оптимальное решение может быть построено из оптимальных решений ее подзадач
Перекрывающиеся подзадачи - проблема может быть разбита на подзадачи, которые используются повторно несколько раз, или рекурсивный алгоритм для проблемы решает одну и ту же подзадачу снова и снова, а не всегда создает новые подзадачи.
Как только эти два условия выполнены, мы можем сказать, что эта проблема «разделяй и властвуй» может быть решена с использованием подхода динамического программирования.
Расширение динамического программирования для Divide and Conquer
Подход динамического программирования расширяет подход «разделяй и властвуй» с помощью двух методов ( запоминания и табулирования ), цель которых заключается в сохранении и повторном использовании решений подзадач, которые могут значительно улучшить производительность. Например, наивная рекурсивная реализация функции Фибоначчи имеет временную сложность, в O(2^n)
которой решение DP делает то же самое только со O(n)
временем.
Мемоизация (заполнение кэша сверху вниз) относится к технике кэширования и повторного использования ранее вычисленных результатов. Таким образом, мемоизированная fib
функция будет выглядеть так:
memFib(n) {
if (mem[n] is undefined)
if (n < 2) result = n
else result = memFib(n-2) + memFib(n-1)
mem[n] = result
return mem[n]
}
Табулирование (заполнение кеша снизу вверх) аналогично, но фокусируется на заполнении записей кеша. Вычисление значений в кеше проще всего выполнять итеративно. Версия таблицы fib
будет выглядеть так:
tabFib(n) {
mem[0] = 0
mem[1] = 1
for i = 2...n
mem[i] = mem[i-2] + mem[i-1]
return mem[n]
}
Вы можете прочитать больше о запоминании и сравнении таблиц здесь .
Основная идея, которую вы должны здесь усвоить, заключается в том, что, поскольку наша проблема «разделяй и властвуй» имеет перекрывающиеся подзадачи, становится возможным кеширование решений подзадач и, таким образом, на сцену выходит мемоизация / табуляция.
Так в чем же разница между DP и DC?
Поскольку теперь мы знакомы с предпосылками DP и его методологиями, мы готовы объединить все, что было упомянуто выше, в одну картину.
Если вы хотите увидеть примеры кода, вы можете взглянуть на более подробное объяснение здесь, где вы найдете два примера алгоритмов: двоичный поиск и минимальное расстояние редактирования (расстояние Левенштейна), которые иллюстрируют разницу между DP и DC.