Динамика по подотрезкам - Алгоритмика
Динамика по подотрезкам

Динамика по подотрезкам

авторы Андрей Гаркавый Сергей Слотин

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

#Кузнечик

Есть полоска 1×n1 \times n. Кузнечик стоит на первой клетке, он может прыгать вперед на 1, 2 или 3 клетки. Сколько есть способов добраться от начальной клетки до последней?

Попытаемся придумать рекуррентную формулу — то есть вывести, как ответ для nn зависит от ответа для меньших чисел.

Пусть f[k]f[k] равно количеству способов добраться от 1 клетки до клетки номер kk. Попытаемся руками посчитать несколько его первых значений:

  • f[1]=1f[1] = 1 способ (стоять на месте)
  • f[2]=1f[2] = 1 способ:
12 1 \rightarrow 2
  • f[3]=2f[3] = 2 способа:
12313 \begin{aligned} 1 \rightarrow 2 \rightarrow 3 \\ 1 \rightarrow 3 \end{aligned}
  • f[4]=4f[4] = 4 способа:
123413412414 \begin{aligned} 1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \\ 1 \rightarrow 3 \rightarrow 4 \\ 1 \rightarrow 2 \rightarrow 4 \\ 1 \rightarrow 4 \end{aligned}
  • f[5]=7f[5] = 7 способов:
12345134512451451235135125 \begin{aligned} 1 \rightarrow 2 \rightarrow 3 \rightarrow 4 \rightarrow 5 \\ 1 \rightarrow 3 \rightarrow 4 \rightarrow 5 \\ 1 \rightarrow 2 \rightarrow 4 \rightarrow 5 \\ 1 \rightarrow 4 \rightarrow 5 \\ 1 \rightarrow 2 \rightarrow 3 \rightarrow 5 \\ 1 \rightarrow 3 \rightarrow 5 \\ 1 \rightarrow 2 \rightarrow 5 \end{aligned}

Дальше становится сложнее. Но можно заметить закономерность. А можно и не заметить. В любом случае, когда мы придумаем формулу, с выписанными несколькими первыми значениями легко проверить, работает ли она или нет.

Давайте подумаем, каким мог быть последний прыжок кузнечика в его пути до nn-й клетки? Один из трёх вариантов:

  • (n1)n(n - 1) \rightarrow n
  • (n2)n(n - 2) \rightarrow n
  • (n3)n(n - 3) \rightarrow n

То есть все пути до nn разбиваются на 3 группы, и причём мы знаем сколько путей в каждой группе: в первой из них ровно f[n1]f[n-1] путей — столько путей идут до (n1)(n-1)-й клетки, и дальше идет еще один прыжок; аналогично, во второй и третьей группах f[n2]f[n-2] и f[n3]f[n-3] путей соответственно.

Просуммировав число путей во всех трёх группах, получаем

f[n]=f[n3]+f[n2]+f[n1] f[n] = f[n-3] + f[n-2] + f[n-1]

Очень похоже на числа Фибоначчи, да? На самом деле, если бы кузнечик мог прыгать только на 1 или 2 клетки вперед, ответом было бы в точности nn-ое число Фибоначчи.

Для подсчета этой динамики можно аналогично завести массив и заполнять его проходом слева направо:

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];

Примечание. Такая последовательность называется числами трибоначчи.

Упражнение. Определите количество последовательностей из нулей и единиц длины nn, в которых нет трёх единиц подряд.

#Кузнечик с препятствиями

Немного изменим задачу: теперь некоторые из клеток закрыты, то есть нам известно про какие-то конкретные клетки, что на них кузнечик прыгать не может.

Тогда задача все еще решается такой же рекурсивной формулой, только нужно убедиться, что f[k]=0f[k] = 0 для всех запрещенных kk.

Так как нам нужно добавить в реализацию такую проверку, немного отрефакторим код, чтобы не рассматривать прыжок каждого размера по отдельности, а также будем инициализировать только f1f_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];

Заметим, что такая реализация масштабируется на любую длину прыжка kk. Асимптотика будет O(nk)O(n k).

#Число подпалиндромов

Палиндромом называется последовательность символов aia_i длины nn, для которой выполняется ai=ania_i = a_{n-i}.

Требуется для данной строки размера nn определить, сколько её подстрок являются палиндромами.

Заведем динамику f[l,r]f[l, r], равную единице, если подстрока с ll по rr является палиндромом, и нулем в противном случае. Посчитав её, мы найдем все подстроки-палиндромы в исходной строке, и для ответа сможем просто просуммировать все ячейки этой динамики.

Определение палиндрома можно альтернативно сформулировать так: строка ss является палиндромом, если s0=sns_0 = s_n и строка s1n1s_{1\ldots n - 1} является палиндромом. Такое определение и будем использовать для пересчета динамики:

f[l,r]=(sl=sr)f[l+1,r1] f[l, r] = (s_l = s_r) \land f[l + 1, r - 1]

Заведем двумерный массив для хранения динамики и пройдемся по базовым случаям: подстрокам размера 1, всегда являющимися палиндромами, и подстрокам размера 2, являющимися палиндромами если первый символ равен второму. Для остальных состояний будем пользоваться формулой выше.

Единственный нюанс — порядок обхода. Нельзя просто проходиться фориком сначала в порядке увеличения ll, а потом увеличения rr, потому что пересчет зависит от динамик с большими ll. Вместо этого будем итерироваться по размеру подстроки d=rl+1d = r - l + 1, а затем перебирать все подстроки с таким размером. Тогда для любой подстроки динамика для f[l+1][r1]f[l + 1][r - 1] уже будет посчитана.

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];

Альтернативно, можно проходиться в порядке уменьшения ll и увеличения rr. Также базовым состоянием для палиндромов четного размера можно выбрать «подстроки размера ноль», всегда являющиеся палиндромами, выполнив в цикле f[i][i - 1] = true — либо тогда уже просто инициализировать весь массив динамик единицами и начинать с d=2d=2:

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];

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