Динамическое программирование — это когда у нас есть задача, которую непонятно как решать, и мы разбиваем ее на меньшие задачи, которые тоже непонятно как решать.
Рассмотрим такую задачу: требуется найти -ое число Фибоначчи , определяемое как
Записав определение выше как функцию, задачу можно решить рекурсивно:
int f(int n) {
if (n == 0)
return 0;
if (n == 1)
return 1;
return f(n - 2) + f(n - 1);
}
Однако для , превосходящих ~40, такое решение будет работать очень долго. Попытаемся грубо оценить время работы .
Чтобы посчитать , нам нужно рекурсивно посчитать и :
Для подсчета нужно не меньше действий, чем для . Объединяя с предыдущим, это значит, что нужно хотя бы в два раза больше действий, чем для : А значит, время работы растет экспоненциально:Упражнение. Какое точное суммарное количество рекурсивных вызовов будет при подсчете ?
Запоминание результатов
Основная идея — сводить задачу к меньшим, и в какой-то момент к базовым — совершенно правильная.
Однако нужно следить за тем, чтобы число этих задач не росло экспоненциально. Для этого проще всего не совершать никаких лишних действий: если мы один раз посчитали , то давайте запомним, чему оно равно, и в следующий раз, когда оно нам понадобится, будем использовать его сразу.
Удобнее всего завести массив для всех нужных значений и, вместо рекурсивной функции, инициализировать его базовые значения, а затем пройтись в цикле по всем остальным и посчитать их, используя уже известные после предыдущих итераций и :
const int n = 100;
int f[n];
f[0] = 0;
f[1] = 1;
for (int i = 2; i < n; i++)
f[i] = f[i - 2] + f[i - 1];
(Для больших в последней строчке будет происходить переполнение, поэтому часто в подобных задачах все значения рассматривают по модулю.)
Динамическим программированием (англ. dynamic programming, ДП, динамика) называется как раз приведенный выше подход, обычно заключающийся в следующих общих шагах:
- Свести задачу для к задаче для чисел, меньших, чем — то есть найти какую-то рекурсивную формулу.
- Создать массив (или какую-нибудь другую структуру данных) для хранения ответов на подзадачи.
- Заполнить начало массива вручную (базовые случае, для которых формула не работает).
- Обойти массив от известных значений в неизвестные и заполнить ответы по формуле.
- Вывести ответ — обычно просто записанный в последней ячейке массиве.
Чтобы решить задачу динамическим программированием, вы должны ответить на 5 вопросов:
- Что лежит в массиве? (чаще всего самый важный вопрос)
- Как инициализировать начало массива?
- Как обходить массив? (чаще всего слева направо, но не всегда)
- Какой формулой считать элементы массива?
- Где в массиве лежит ответ?
Этим приемом можно решать большое количество важных задач, и в этом разделе мы в этом убедимся.