Разберем несколько учебных задач на динамику, где состоянием является префикс или непрерывный подотрезок какого-то массива.
#Кузнечик
Есть полоска . Кузнечик стоит на первой клетке, он может прыгать вперед на 1, 2 или 3 клетки. Сколько есть способов добраться от начальной клетки до последней?
Попытаемся придумать рекуррентную формулу — то есть вывести, как ответ для зависит от ответа для меньших чисел.
Пусть равно количеству способов добраться от 1 клетки до клетки номер . Попытаемся руками посчитать несколько его первых значений:
- способ (стоять на месте)
- способ:
- способа:
- способа:
- способов:
Дальше становится сложнее. Но можно заметить закономерность. А можно и не заметить. В любом случае, когда мы придумаем формулу, с выписанными несколькими первыми значениями легко проверить, работает ли она или нет.
Давайте подумаем, каким мог быть последний прыжок кузнечика в его пути до -й клетки? Один из трёх вариантов:
То есть все пути до разбиваются на 3 группы, и причём мы знаем сколько путей в каждой группе: в первой из них ровно путей — столько путей идут до -й клетки, и дальше идет еще один прыжок; аналогично, во второй и третьей группах и путей соответственно.
Просуммировав число путей во всех трёх группах, получаем
Очень похоже на числа Фибоначчи, да? На самом деле, если бы кузнечик мог прыгать только на 1 или 2 клетки вперед, ответом было бы в точности -ое число Фибоначчи.
Для подсчета этой динамики можно аналогично завести массив и заполнять его проходом слева направо:
f[0] = 1
f[1] = 1
f[2] = 2
for (int i = 3; i < n; i++)
f[i] = f[i - 3] + f[i - 2] + f[i - 1];
Примечание. Такая последовательность называется числами трибоначчи.
Упражнение. Определите количество последовательностей из нулей и единиц длины , в которых нет трёх единиц подряд.
#Кузнечик с препятствиями
Немного изменим задачу: теперь некоторые из клеток закрыты, то есть нам известно про какие-то конкретные клетки, что на них кузнечик прыгать не может.
Тогда задача все еще решается такой же рекурсивной формулой, только нужно убедиться, что для всех запрещенных .
Так как нам нужно добавить в реализацию такую проверку, немного отрефакторим код, чтобы не рассматривать прыжок каждого размера по отдельности, а также будем инициализировать только :
// a -- булевый массив, свободна ли i-ая клетка
f[0] = a[0];
for (int i = 1; i < n; i++)
for (int j = i; j > max(i - 3, 0); j--)
// f[i] изначально заполнены нулями
f[i] += f[j] * a[j];
Заметим, что такая реализация масштабируется на любую длину прыжка . Асимптотика будет .
#Число подпалиндромов
Палиндромом называется последовательность символов длины , для которой выполняется .
Требуется для данной строки размера определить, сколько её подстрок являются палиндромами.
Заведем динамику , равную единице, если подстрока с по является палиндромом, и нулем в противном случае. Посчитав её, мы найдем все подстроки-палиндромы в исходной строке, и для ответа сможем просто просуммировать все ячейки этой динамики.
Определение палиндрома можно альтернативно сформулировать так: строка является палиндромом, если и строка является палиндромом. Такое определение и будем использовать для пересчета динамики:
Заведем двумерный массив для хранения динамики и пройдемся по базовым случаям: подстрокам размера 1, всегда являющимися палиндромами, и подстрокам размера 2, являющимися палиндромами если первый символ равен второму. Для остальных состояний будем пользоваться формулой выше.
Единственный нюанс — порядок обхода. Нельзя просто проходиться фориком сначала в порядке увеличения , а потом увеличения , потому что пересчет зависит от динамик с большими . Вместо этого будем итерироваться по размеру подстроки , а затем перебирать все подстроки с таким размером. Тогда для любой подстроки динамика для уже будет посчитана.
bool f[n][n];
for (int i = 0; i < n; i++) {
f[i][i] = true;
f[i][i + 1] = (s[i] == s[i + 1]);
}
for (int d = 3; d <= n; d++)
for (int l = 0, r = l + d - 1; r < n; l++, r++)
f[l][r] = (s[l] == s[r]) && f[l + 1][r - 1];
int ans = 0;
for (int l = 0; l < n; l++)
for (int r = l; r < n; r++)
ans += f[l][r];
Альтернативно, можно проходиться в порядке уменьшения и увеличения . Также базовым состоянием для палиндромов четного размера можно выбрать «подстроки размера ноль», всегда являющиеся палиндромами, выполнив в цикле f[i][i - 1] = true
— либо тогда уже просто инициализировать весь массив динамик единицами и начинать с :
memset(f, 1, sizeof f);
for (int l = n - 1; l >= 0; l--)
for (int r = l + 1; r < n; r++)
f[l][r] = (s[l] == s[r]) && f[l + 1][r - 1];
Отметим, что у этой задачи есть более оптимальные решения, в том числе линейные.