Вы находитесь на странице: 1из 2

Динамика

Основы
Понятие динамического программирования похоже на понятие индукции в
математике. Решить задачу - значит свести ее к похожим задачам, которые чуть-чуть
проще, и их решить тем же способом.
Самый простой пример: задача про кузнечика. Кузнечик стоит на 0 ступеньке,
за ход прыгает на 1 или 2 ступеньки, надо найти число способов дойти до ступеньки N.
Здесь нужно догадаться, что задача “сколько способов дойти до N” напрямую
сводится к задачам “сколько способов дойти до N-1” и “сколько способов дойти до N-
2”. Поэтому надо завести массив dp[i], где dp[i] - число способов дойти до ступеньки i.
Тогда dp[0] = 1, dp[1] = 1, и верна формула dp[i] = dp[i - 1] + dp[i - 2], так как любой путь
до i-й вершины последним ходом либо прыгает на один (таких путей до
предпоследней ступеньки dp[i - 1]), либо на два (таких dp[i - 2]).
Осталось просто завести массив, заполнить его начальные значения, и
посчитать слева направо по формуле все остальные значения до N:

dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
print(dp[n])

P. S. Вы заметили, что мы посчитали числа Фибоначчии?

То есть решение задачи на динамику сводится к 5 шагам:


1) придумать, ЧТО ХРАНИТЬ в массиве dp[i] (или dp[i][j], массив может быть
любой размерности) - часто самый сложный шаг
2) придумать, КАК СЧИТАТЬ значения в этом массиве через предыдущие, то есть
формулу - здесь очень помогает ручками нарисовать этот массив и поискать
закономерности
3) придумать, КАКИЕ НАЧАЛЬНЫЕ ЗНАЧЕНИЯ в этом массиве - это простой,
скорее технический шаг, про который нельзя забывать
4) придумать, КАК ОБОЙТИ МАССИВ, чтобы все посчиталось - чаще всего (но не
всегда) это просто естественный обход слева направо, сверху вниз
5) придумать, ГДЕ ЛЕЖИТ ОТВЕТ - чаще всего это просто одна из последних
ячеек, но не всегда

НВП
Возьмем такую задачу: надо найти наибольшую возрастающую
подпоследовательность (НВП) в массиве. Подпоследовательность - это то, что
остается, после зачеркивания некоторых значений в массиве.
Например, в массиве [4, 2, 3, 6, 8, 5, 6, 10, 7, 2, 1, 9] жирным выделена
максимальная возрастающая подпоследовательность.
Эта задача не решается динамикой за O(N), к сожалению, хотя сама задача и
про массив.
Попытаемся свести задачу к меньшей. Что, если мы выкинем последний
элемент массива и узнаем в оставшемся НВП? И если в конец добавляется последний
элемент (он больше последнего в НВП), то добавим его. Действительно, это работает.
Но если он меньше и он не добавляется, то что делать? Неясно.
Здесь мало что сделаешь без важной идеи динамики по последнему
элементу. То есть давайте в dp[i] хранить размер НВП, которая заканчивается в
клетке i. Это добавляет какой-то определенности.
Тогда как свести ее к предыдущим? Перебрать предыдущий элемент! Он
должен быть левее и меньше. За O(n) переберем его и выберем максимум dp[i] в таких
элементах. Так сделаем для n элементов. Заметьте, что ответ лежит не в dp[n], а в
максимуме dp[i] по всему массиву, потому что последний элемент может быть любым.

Но как восстановить эту подпоследовательность? Мы знаем только ее размер.


Здесь тоже нужна важная идея массива предыдущих элементов. Любое
восстановление ответа делается примерно одним способом - заводим массив prev,
prev[i] - это номер ячейки, откуда мы посчитали значение в dp[i]. То есть в данном
случае это номер предпоследнего элемента в НВП, заканчивающейся в i. Это номер
того самого максимума по элементам, которые слева и меньше.
Тогда, чтобы восстановить ответ, нужно взять ответ (максимум по всем dp[i]) и с
помощью цикла while пройтись по массиву prev и записать весь путь в массив path.
Там и будет нужная там НВП. Ну, правда, в перевернутом виде, но просто выведем ее
с конца. Вот и все.
P.S. Решение за O(N^2)

НОП
Другая задача: есть две строки, надо найти их наибольшую общую
подпоследовательность (НОП). Например, у abacab и aabcba это строка abca (aaca
тоже подходит).
Здесь нужна уже двумерная динамика. Здесь задача решается без
премудростей - сводим задачу к предыдущей тривиально: просто dp[i][j] - это размер
НОП первых i символов первой строки и первых j символов второй строки.
Тогда заметим, что dp[0][k]=dp[k][0]=0.
И заметим, что dp[i][j] = dp[i-1][j-1], если s1[i] == s2[j], то есть если символы
равны, то они по-любому входят в НОП.
А если они не равны, то один из них по-любому не входит в НОП: dp[i][j] =
max(dp[i - 1][j], dp[i][j - 1]), если s1[i] != s2[j].
Вот и всё! Осталось восстановить ответ с помощью prev[i][j] старым способом и
получить перевернутый ответ. Удачи!