Сайт переезжает. Большинство статей уже перенесено на новую версию.
Скоро добавим автоматические переходы, но пока обновленную версию этой статьи можно найти там.
Динамическое программирование — рюкзак, НВП, НОП
Предполагается, что вы уже знакомы с базовыми понятиями динамического программирования и помните бинарный поиск.
- Задача о рюкзаке
- НВП
- НОП
- Динамика по префиксу и значению последнего элемента
- Ленивая динамика
Задача о рюкзаке
0-1 Рюкзак
В самой простой форме задача о рюкзаке формулируется так: > Даны предметов с весами . Определите, на какой максимальный вес можно набрать предметов в рюкзак вместимости .
Для решения этой задачи воспользуемся динамическим программированием. Обозначим за состояние, когда мы рассмотрели первые предметов и набрали ими веса. , если такая ситуация возможна, иначе .
Для каждого состояния , которое возможно получить, мы можем либо взять предмет номер и попробовать обновить ответ из состояния , либо не брать его и обновить ответ из . Очевидно, что мы можем получить 0 веса, рассмотрев 0 предметов.
dp[0][0] = True
for i in range(1, n + 1):
for j in range(0, W + 1):
dp[i][j] = dp[i - 1][j]
if a[i] <= j and dp[i - 1][j - a[i]]:
dp[i][j] = True
Ответом будет максимальное , такое что . Таким образом, мы получили решение за
0-1 Рюкзак со стоимостями
Немного усложним задачу. Пусть, теперь предметы имеют не только веса, но и стоимости . Соответственно, надо набрать предметов с наибольшей суммарной стоимостью, но весом не превосходящим . Теперь в будем хранить не просто возможно ли получить из первых предметов набор веса , а максимальную суммарную стоимость такого набора. Если же такой набор получить невозможно, то по-прежнему . Переходы такие же как и в прошлой задаче. Изначально заполнено 0, так как для любого непустого набора мы пока не знаем, как его получить, а путой набор имеет стоимость 0.
for i in range(1, n + 1):
for j in range(0, W + 1):
dp[i][j] = dp[i - 1][j]
if a[i] <= j:
dp[i][j] = max(dp[i][j], dp[i - 1][j - a[i]] + c[i])
Ответом на задачу будет максимальное . Такое решение тоже работает за .
Если так получилось, что веса большие, а стоимости маленькие, можно поменять их местами и считать минимальный вес при данной набранной стоимости. Поменять местами значение динамики и параметр — довольно распространенный трюк в динамическом программировании.
Рюкзак с ограниченным числом предметов
Пусть, теперь предметов каждого типа может быть несколько, то есть даны не только веса и стоимости, но и максимальные количества каждого из предметов . Тогда для каждого состояния переберем, сколько мы взяли предметов такого типа и сделаем переход из каждого соответствующего состояния. Понятно, что мы не сможем взять более, чем предметов каждого типа.
for i in range(1, n + 1):
for j in range(0, W + 1):
for cnt in range(min(k[i], W // a[i]) + 1):
if a[i] * cnt <= j:
dp[i][j] = max(dp[i][j], dp[i - 1][j - a[i] * cnt] + c[i] * cnt)
Такое решение работает за , так как могут быть очень большими, а .
Можете попробовать решить эту задачу за , где — максимальное из .
Рюкзак с неограниченным числом предметов
Пусть, теперь каждого предмета будет не , а вообще бесконечно. Оказывается, задача стала только проще. Вернемся к обычному рюкзаку с весами и стоимостями. Единственное отличие будет в том, что теперь мы можем делать второй переход не из предыдущей строки, а прямо из текущей. При этом заметим, что для каждого состояния достаточно рассмотреть взятие только одного предмета данного типа, поскольку взятие двух и более будет рассмотрено одновременно.
for i in range(1, n + 1):
for j in range(0, W + 1):
dp[i][j] = dp[i - 1][j]
if a[i] <= j:
dp[i][j] = max(dp[i][j], dp[i][j - a[i]] + c[i])
Такое решение работает за .
Если велико, а достаточно малы, можно построить решение за , где — максимальное из . Заметим, что если достаточно большое, то большая часть рюкзака будет занята предметами одного и того же типа с максимальной удельной стоимостью. Можно доказать, что одним и тем же предметом будет занято не менее веса. Таким образом, можно за выбрать предмет с максимальным удельным весом, а для оставшихся предметов запустить предыдущий алгоритм, который сработает за , так как имеет смысл рассматривать не более предметов, а максимальный вес .
Восстановление ответа
Во всех рассмотренных задачах восстановление ответа делается стандартным образом: нужно из ответа пройтись обратно до начала.
- первый способ - это построить массив prev, где для каждой ячейки хранить, берем мы предмет i, или не берем предмет .
- второй способ - это определять это на ходу, смотря, какое из значений или больше.
Наибольшая возрастающая подпоследовательность
Пусть, дана последовательность из чисел . Требуется найти длину ее наибольшей возрастающей подпоследовательности (НВП), то есть длину такой наибольшей последовательности индексов , что .
Пример: в последовательности наибольшей возрастающей подпоследовательность является : она имеет длину . Возрастающих подпоследовательностей длины 5 здесь нет.
НВП за
Давайте решать наивно через динамческое программирование - то есть хранить в ровно то, что нам надо найти - длину НВП для первых чисел.
. Но как найти формулу, выражающую через предыдущин значения?
Ну, есть два варианта: * -ое число не входит в НВП. Тогда * -ое число входит в НВП. Тогда , где - индекс предыдущего числа в этой НВП. Так давайте просто его переберем. При этом надо учесть, что должно быть меньше, чем !
Итогвоая формула получается такая:
Этот алгоритм работает за : у нас состояний динамики, и каждое из них мы считаем за действий, пока ищем этот максимум.
Ответ восстанавливается тем же способом: для каждого состояния нужно сохранить, где был этот максимум - там и есть предыдущее число в НВП.
НВП за
Решим эту задачу чуть более нестандартным динамическим программированием, где будет обозначать минимальное число, на которое может заканчиваться НВП длины . При этом мы будем постепенно обрабатывать числа слева направо, и в этом массиве будет храниться только информация про все НВП в уже обработанном начале последовательности.
Изначально для . В качестве надо выбрать число, которое заведомо больше любого из , аналогично с .
Рассматривая очередной элемент, попробуем продлить им каждую подпоследовательность:
n = 6
a = [6, 1, 5, 3, 4, 2] # НВП: 1, 3, 4
inf = 100
min_end = [-inf] + [inf] * n
for i in range(n):
for j in range(n):
if min_end[j - 1] < a[i] < =min_end[j]:
min_end[j] = a[i]
print(dp)
---------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-1-930ff2a8d8b0> in <module>()
7 if min_end[j - 1] < a[i] < min_end[j]:
8 min_end[j] = a[i]
----> 9 print(dp)
NameError: name 'dp' is not defined
Ответом будет максимальный такой индекс , что . Это решение работает за .
Его можно значительно ускорить, заметив два факта: - На любом шаге . Это легко доказать от противного. - Из предыдущего факта следует, что любое обновит максимум одно значение динамики, так как попадет максимум в один интервал.
Значит, для поиска , которое обновится можно воспользоваться бинарным поиском. Это решение уже работает за .
Наибольшая общая подпоследовательность
Даны две последовательности и . Требуется найти длину их наибольшей общей подпоследовательности (НОП), то есть длину наибольшей таких последовательностей и , что .
Решим эту задачу с помощью динамического программирования, где будет обозначать длину НОП, если мы рассмотрели префиксы последовательностей длины и .
Тогда заметим, что есть две ситуации, когда мы считаем : * , тогда хотя бы один из этиз символов не содержится в НОП, иначе она заканчивается на два разных символа. В этом случае * , тогда несложно доказать, что точно есть максимальная НОП, в которую входят ОБА этих символа, а значит .
А на пустых префиксах ответ 0.
a = [1, 100, 2, 100, 3]
b = [10, 10, 1, 2, 3, 10] # НОП: 1,2,3
n = len(a)
m = len(b)
dp = [[0 for j in range(m + 1)] for i in range(n + 1)]
for i in range(1, n + 1):
for j in range(1, m + 1):
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
if a[i - 1] == b[j - 1]:
dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + 1)
print(dp[i])
[0, 0, 0, 1, 1, 1, 1]
[0, 0, 0, 1, 1, 1, 1]
[0, 0, 0, 1, 2, 2, 2]
[0, 0, 0, 1, 2, 2, 2]
[0, 0, 0, 1, 2, 3, 3]
Ответом является максимальное число в массиве . Решение работает за .
Ответ при это восстанавливается классическим способом - с конца. Нам все еще нужно просто в каждой ячейке смотреть - если символы в ней равны, то нужно уменьшить и , иначе только один из них - так, чтобы НОП был максимален.
Задание
Сведите задачу НВП к задаче НОП.
Задание
Найдите НОП двух перестановок длины за .
Динамика по префиксу и значению последнего элемента
Пусть, дана последовательность , с максимальным значением . Требуется найти длину наибольшей такой подпоследовательности, что ее элементы отличаются на более, чем на 1. Воспользуемся динамическим программированием, где будет обозначать ответ с последним взятым элементом, равным . Будем обновлять и хранить актуалььным весь массив целиком, проходясь по массиву слева направо.
Соответственно для каждого переходы можно делать только из таких , что .
for i in range(1, n + 1):
dp[a[i]] += 1
if a[i] > 0:
dp[a[i]] = max(dp[a[i]], dp[a[i] - 1] + 1)
if a[i] < A:
dp[a[i]] = max(dp[a[i]], dp[a[i] + 1] + 1)
Это решение за .
Заметим, что вот эти две идеи встречаются в задачах наиболее часто: * хранить в ответ для -ого префикса. Как в рюкзаке (где можно пользоваться первыми предметами), НВП(где ответ на префиксе длины ) и НОП (где ответ для префиксов длины и ). * хранить в ответ для последовательностей, заканчивающихся на .
Ленивая динамика
Если сложно придумать порядок обхода таким образом, чтобы все предыдущие значения уже были посчитаны, то можно вместо циклов использовать рекурсивную функцию и запоминать посчитанный результат, чтобы не считать несколько раз одно и то же.
Решим, например, обычную задачу о рюкзаке таким образом. Изначально все , это будет обозначать, что значение еще не посчитано, кроме .
def calc(i, j):
if dp[i][j] == -1:
dp[i][j] = calc(i - 1, j)
if a[i] <= j:
dp[i][j] = max(dp[i][j], calc(i - 1, j - a[i]) + c[i])
return dp[i][j]
answer = 0
for j in range(W + 1):
answer = max(answer, calc(n, j))
Время работы так же составит , так как каждое значение мы считаем только один раз, но истинное время работы будет в несколько раз больше, потому что константа на вызовы функции значительно выше чем на простой цикл.
Задание
Решите как можно больше задач из этих двух контестов:
https://informatics.msk.ru/mod/statements/view.php?id=35888
https://informatics.msk.ru/mod/statements/view.php?id=33257 ### Дополнительная сложная задача https://csacademy.com/contest/round-61/task/strictly-increasing-array/statement/