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


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

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

Примерная схема решения такая:
dp[i] - ответ для первых i элементов

Подсчет dp[i]: так как мы рассматриваем только первые i элементов, то i-й будет последним и значит, что этот элемент будет в последнем подотрезке и при этом самым правым там. Поэтому мы можем перебрать левую границу последнего подотрезка j. В процессе перебора будем подсчитывать значение этого подотрезка и если он является корректным, то пересчитаем dp[i] через dp[j - 1] и значение подотрезка [j;i].

Рассмотрим следующую простую задачу: дан массив из целых чисел, необходимо разбить его на минимальное количество непересекающихся подотрезков, чтобы каждое число входило в какой-нибудь подотрезок и чтобы в каждом подотрезке были одинаковые числа. Например, для массива 1 2 2 3 3 3 2 1 1 оптимальное разбиение выглядит так: [1] [2 2] [3 3 3] [2] [1 1]. Эта задача несложно решается простым проходом по массиву (все одинаковые подряд идущие элементы помещаем в один подотрезок), но мы решим ее с помощью динамического программирования для примера.
 
	int n;
	cin >> n;

	// заполняем массив с 1-индексацией
	vector arr(n + 1);
	for (int i = 1; i <= n; i++)
		cin >> arr[i];

	// изначально ставим +оо в значения дп
	vector dp(n + 1, 1000000000);

	// массив длины ноль разбивать не нужно, поэтому для него ответ 0
	dp[0] = 0;

	// считаем ответ для dp[i] по возрастанию i
	for (int i = 1; i <= n; i++) {
		// в текущий момент arr[i] - последний элемент, значит он будет самым правым числом в последнем подотрезке
		// переберем все варианты того, где этот последний подотрезок начинался
		for (int j = i; j > 0; j--) {
			if (arr[j] != arr[i]) {
				// если встретили элемент, который не равен последнему, то подотрезок будет содержать разные числа, а это не подходит по условию
				// продолжать уже нет смысла, т.к. сдвигая левую границу левее, этот элемент не пропадет, поэтому делаем break
				break;
			}

			// представляем, что последний подотрезок был [j;i]
			// значит надо взять оптимальное разбиение первых j-1 элементов и прибавить 1 (сам подотрезок  [j;i])
			dp[i] = min(dp[i], dp[j - 1] + 1);
		}
	}
	cout << dp[n];

Если элементы могут не принадлежать ни одному из подотрезков, то необходимо просто рассмотреть соответствующий вариант, как dp[i] = dp[i - 1]

Если необходимо разделить массив на ровно k подотрезков, то просто добавляется второй параметр в динамическом программировании - на сколько отрезков разбиваем.
То есть теперь будем считать следующее дп:
dp[i][j] - ответ для первых i элементов, если мы разбиваем их на ровно j отрезков.
Следите за невалидными состояниями.

Пересчет динамики такой же, но с учетом второго параметра. То есть подсчитывая dp[i][k] и перебирая левую границу последнего подотрезка j, то пересчитываем dp[i][k] через dp[j - 1][k - 1] и значение отрезка [j;i].

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

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

Примерная схема решения:

Изначально отсортируем имеющиеся промежутки по правой границе. Заведем следующую динамику: dp[i] - ответ для первых i промежутков. 
Пересчитывать будем следующим образом: во-первых, рассмотрим ситуацию, что данный промежуток не будет использоваться, тогда просто dp[i] = dp[i-1]. Заметим, что это обеспечивает неубывание значений dp[i] с ростом i. И это логично, т.к. добавляя новый промежуток, мы не можем ухудшить глобальный ответ: либо мы просто проигнорируем новый промежуток, либо сконструируем более выгодный вариант с помощью него. Теперь, если мы хотим использовать i-й промежуток, то мы можем использовать те промежутки, правые границы которых меньше левой границы текущего промежутка, так как мы должны выбрать набор непересекающихся промежутков. Для этого мы изначально сортировали промежутки по правой границе, чтобы сейчас можно было эффективно найти необходимую позицию. Это можно делать аналитически, если это возможно, но в общем случае можно найти бинпоиском промежуток, правая граница которого меньше левой границы текущего и при этом максимально возможная. Максимизировать правую границу мы хотим из жадных соображений, т.к. с ростом i ответ может только увеличиваться. Соответственно, находим необходимую позицию p и пересчитываем dp[i] через dp[p] и i-й промежуток.

Пропустить Навигационные Ссылки.
Чтобы оставить комментарий нужна авторизация