Нет, я не имею в виду Convex Hull-оптимизацию и оптимизацию Кнута. Я имею в виду более простые и распространённые методы.
Это продолжение моей серии постов о ДП, чтобы понимать обозначения, читайте пролог.
Переходы из отрезка
Очень часто после построения графа динамики можно обнаружить, что переходы в то или иное состояние ведут не из случайного набора вершин, а из некоторого отрезка по одному из параметров. Например, из состояний dp[i][j]
, при некотором константном i
и при j
, лежащим в отрезке [a, b]
.
В таком случае можно не реализовывать каждый переход в отдельности, а реализовать все скопом. Если под реализацией перехода понимается прибавление из dp[A]
в dp[B]
, то можно воспользоваться префиксной суммой. Если же нам нужно найти наилучший переход в dp[B]
, то можно воспользоваться деревом отрезков.
Пример
AtCoder DP Contest: Задача M Найти количество целочисленных последовательностей длины n
, где i
-е число от 0
до a[i]
, а сумма чисел равна k
.
Решим методом симуляции процесса. Рассмотрим процесс выписывания по одному числу последовательности. Параметрами процесса будут:
Сколько мы уже выписали чисел.
Сумма выписанных чисел.
Значением динамики dp[i][j]
будет количество путей процесса, ведущих в данное состояние.
Количество переходов из одного состояния равно O(k)
, общее количество переходов – O(n k^2)
, слишком долго.
Рассмотрим, откуда ведут переходы в состояние dp[i][j]
. Очевидно, из dp[i - 1][l]
, где l
принадлежит отрезку [max(0, j - a[i - 1]), j]
.
Значит, перед тем, как считать слой i
, можно насчитать префиксные суммы на слое i - 1
, чтобы считать dp[i][j]
за O(1)
.
Переходы из косого отрезка
Вернёмся к задаче из прошлой части моего блога.
543A - Пишем код Есть n
программистов, они должны написать m
строк кода, сделав не более b
ошибок. i
-й программист совершает a[i]
ошибок за строку. Сколько есть способов это сделать? (Два способа различаются, если различается количество строк, написанных одним из программистов).
Вспомним самое первое решение: определим процесс, где за один шаг мы назначаем очередному программисту количество написанных им строк. Параметрами будут:
Количество пройденных программистов.
Суммарное количество написанных строк.
Суммарное количество сделанных ошибок.
Значением динамики dp[i][j][k]
будет количество путей процесса, ведущих в данное состояние.
Тогда состояний всего получается O(n m b)
, а переходов из одного состояния – O(m)
, значит всего переходов – O(n m^2 b)
, что слишком много.
Но давайте рассмотрим, из каких состояний есть переход в состояние dp[i][j][k]
. Если последний шаг был "i - 1
-й программист написал x
строк кода", то переход был из состояния dp[i - 1][j - x][k - x * a[i - 1]]
(где x >= 0
).
То есть состояния, из которых ведёт переход, расположены на некоторой прямой в таблице i - 1
-ого слоя, и в общем случае сумму всех таких значений можно за O(1)
вычислить из аналогичной суммы для состояния dp[i][j - 1][k - a[i - 1]]
.
После некоторых упрощений код выглядит так:
Поменять местами значение и параметр
Эта техника стара, как мир. Если один из параметров слишком большой, а значение наоборот ограничено, можно попробовать поменять их ролями.
Пример
AtCoder DP Contest: Задача E Есть n
объектов, i
-й имеет вес w[i]
и цену v[i]
. Какая максимальная сумма цен по всем наборам объектов с суммой весов не более m
? В этой версии задачи n <= 100
, m <= 10^9
, v[i] <= 10^3
.
Возьмём решение из предыдущей части блога: рассмотрим процесс, где на каждом шагу рассматривается очередной объект и либо выбирается, либо пропускается, параметрами процесса будут:
Количество рассмотренных объектов.
Суммарный вес выбранных объектов.
Значением динамики будет наибольшая сумма стоимостей выбранных объектов при данном состоянии процесса.
При данных ограничениях, максимальное значение второго параметра будет 10^9
, а максимальное значение динамики – 10^5
, поэтому имеет смысл поменять их ролями. Процесс будет тем же самым, но параметрами будут:
Количество рассмотренных объектов.
Суммарная стоимость выбранных объектов.
Значением динамики будет наименьшая сумма весов выбранных объектов при данном состоянии процесса.
В конце мы рассмотрим наибольшую сумму стоимостей объектов из числа тех, для которых минимальный суммарный вес не больше W
.
Объединить два параметра
Иногда мы можем заметить о неких параметрах, что нам необязательно знать значение каждого, а достаточно следить за некой их комбинацией. Например, если в конце мы делаем проверку на то, что сумма двух параметров не должна превышать некоторое значение, то возможно у нас получится заменить эти два параметра на один, равный их сумме.
Пример
19B - Кассир Есть n
товаров, у каждого есть цена c[i]
и время обработки t[i]
. Найдите множество товаров с минимальной суммарной ценой такое, что их суммарное время обработки не меньше количества остальных товаров.
Рассмотрим процесс, где на каждом шагу рассматривается очередной товар и либо выбирается, либо пропускается. Параметрами процесса будут:
Количество рассмотренных товаров.
Суммарное время обработки выбранных товаров.
Количество невыбранных товаров.
Значением динамики будет минимальная суммарная стоимость выбранных товаров при данном состоянии процесса.
По итогу нас интересуют состояния dp[n][j][k]
, где j >= k
. Так как k <= n
, мы будем считать, что если параметр j
достиг n
, то в дальнейшем нет смысла его повышать, так что все состояния процесса при больших значениях параметра j
будем записывать, будто j = n
.
Таким образом количество состояний равно O(n^3)
, как и количество переходов, что не проходит под ограничения.
Рассмотрим подробнее, куда ведут переходы из состояния dp[i][j][k]
: в случае выбора товара – в dp[i + 1][j + t[i]][k]
, а в случае пропуска – в dp[i + 1][j][k + 1]
.
Заметим, что в первом случае разность второго и третьего параметров просто увеличивается на t[i]
, во втором случае – уменьшается на 1
, а в конце нужно отсечь те состояния, где эта разность неотрицательна, следовательно вместо каждого этого параметра по отдельности, можно следить за их разностью.
Для удобства вместо j - k
будем следить за j + i - k
(чтобы параметр был неотрицательным, т. к. k <= i
), тогда при выборе товара параметр увеличивается на t[i] + 1
, а при пропуске не изменяется.
Так как в конце отсекаются состояния, где этот параметр не меньше n
, при этом он ни при каком переходе не уменьшается, то можно аналогично предыдущему решению записывать состояния с j + i - k > n
в dp[i][n]
.
Таким образом, количество состояний и переходов оба равны O(n^2)
.
Заключение
Если хотите узнать больше, я напоминаю, что даю частные уроки по спортивному программированию, цена – 1800 руб/ч. Пишите мне в Telegram, Discord: rembocoder#3782, или в личные сообщения на CF.