Паттерны в динамическом программировании - 2


Дисклеймер: способ, описанный ниже, не является универсальным, однако зачастую может решить задачу или помочь прийти к правильному решению.

Если в задаче вас просят оптимальным образом уничтожить/схлопнуть/объединить (или похожее) массив, то стоит попробовать решить ее с помощью динамического программирования на подотрезках.

Заведем dp[l][r] - оптимальный ответ для подотрезка с индексами от l до r. Пересчет динамики сильно зависит от задачи, но есть следующие общие идеи:

1) Посмотреть на крайние элементы l и r. Если они совпадают или еще как-либо комплементируют, то, скорее всего, можно будет обновить значение dp[l][r] через dp[l+1][r-1]. Иначе через dp[l][r-1] или dp[l+1[r].

2) Часто бывает, что отрезок [l;r] нельзя представить через единую конструкцию. Тогда необходимо рассмотреть все возможные разделы этого подотрезка на две части, то есть перебрать элемент k от l до r-1 и пересчитывать dp[l][r] через dp[l][k] и dp[k+1][r]. В более маленьких подотрезках также перебирались эти разделы, поэтому на самом деле учитываются варианты разбиения подотрезка [l;r] не только на две части, а на любое их число.
 

Дисклеймер: способ, описанный ниже, не является универсальным, однако зачастую может решить задачу или помочь прийти к правильному решению.

Если в задаче вам необходимо проверить существование какой-либо перестановки или найти количество подходящих перестановок или что-то похожее, то стоит подумать о том, чтобы применить динамическое программирование по подмножествам.

Основным параметром будет являться битовая маска, которая будет показывать какие элементы уже были включены в перестановку, а какие нет. Также часто требуется второй параметр, который показывает, какой элемент был включен последним.

Часто перестановки можно увидеть в контексте путей в графах. Соответственно рассмотрим классический пример - задача о гамильтоновом пути.
Гамильтонов путь - это простой путь, проходящий через каждую вершину графа ровно один раз. Его можно представить просто как перестановку длины n, где n - количество вершин. Порядок внутри этой перестановки будет обозначать порядок обхода вершин в этом пути.

Для того, чтобы проверить наличие гамильтонова пути в графе, заведем динамику dp[mask][last] - существует ли простой путь, который обошел вершины, помеченные единицами в битовой маске mask и при этом закончил в вершине last.
Начальные значения будут dp[2i][i] = true, остальные false, что означает, что всегда есть пустой путь, начинающийся в любой из вершин.
Имея значение true в dp[mask][last] будем делать переходы вперед, по смыслу "продлевая путь". То есть будем искать номера вершин, которые помечены нулем в mask (мы их еще не посещали в пути) и при этом такие, что есть ребро из last в эту вершину (путь должен быть продлен существующим ребром). То есть делаем dp[mask | 2k][k] = true, если dp[mask][last] = true И mask & 2k = 0 (вершину k еще не посещали) И есть ребро last -> k.
В конечном итоге гамильтонов путь существует, если есть значение true в dp по параметрам битовой маски, состоящей только из единиц, и любой последней вершины.