Дано натуральное число n
и следущие правила формирования строки длины n
:
- Состоит только из символов: 'a', 'e', 'i', 'o', 'u';
- За
a
может сделовать толькоe
; - За
e
может следоватьa
илиi
; - За
i
не может следоватьi
; - За
o
может следоватьi
илиu
; - За
u
может следовать толькоa
.
Нужно сказать сколько существует таких строк длины n
.
Начинаем, как обычно, с вопросов чтобы понять о чем задача и какие есть ограничения.
-
Ограничения задачи:
-
В каких пределах может быть
n
?Натуральные числа
1 <= n <= 2 * 10^4
-
-
Ожидаемый результат:
-
Кажется, для
2 * 10^4
число комбинацией может получиться очень большим. Нужно ли использовать длинную арифметику или можно выдать результат по модулю?
По модулю
1e9 + 7
-
- Пример 1:
n = 1, ответ: 5
Строки: "a", "e", "i" , "o" and "u"
- Пример 2:
n = 2, ответ: 10
Строки: "ae", "ea", "ei", "ia", "ie", "io", "iu", "oi", "ou" and "ua"
Что является наивным подходом в данной задаче? Можно построить все строки длины n
по описанным правилам и посчитать их количество. Представим, что n
относительно небольшое, скажем не более 5, тогда это вполне имеет смысл.
Полезно посмотреть на сам принцип "генерации строк по правилам".
Удобно воспользоваться рекурсией.
- На первом уровне у нас есть пять букв
- К каждой из них можно пристегнуть другую по определенным правилам (сделать рекурсивный вызов)
- Когда мы добираемся до строки длины
n
— кладем строку в ответ, заканчиваем рекурсию (базовый случай)
В итоге, должен получиться массив строк длины n
, надо вернуть его длину.
Для n
~ 10^4 такое решение не пойдет, потому что количество строк растет экспоненциально.
Если внимательно посмотреть на дерево рекурсивных вызовов, то каждый путь представляет собой префикс итоговой строки. Нас просят найти количество строк, что соответстует количеству листьев в этом дереве. По сути, можно не генерить никакие строки, а просто увеличивать счетчик каждый раз когда мы добрались до листа.
Это, конечно, улучшит наивное решение с точки зрения потребления памяти (не нужно создавать новые строки), но по-прежнему нужно будет сделать конское количество рекурсивных вызовов. Как быть?
Здесь мы приходим к динамическому программированию. Цель — избавиться от экспоненты в сложности.
Каждый следующий уровень в этом дереве строится на основе предыдущего, то есть количество узлов на следующем уровне будет как-то выражаться через количество узлов на предыдущем уровне (если только правила не меняются от уровня к уровню). Собственно, нам нужно найти это как-то - рекуррентное соотношение.
Если мы знаем количество префиксов длины i - 1
, которые заканчиваются в определенной букве, то можно сказать:
dp[i]['a'] = dp[i − 1]['e']
(Заa
может сделовать толькоe
)dp[i]['e'] = (dp[i − 1]['a'] + dp[i − 1]['i'])
(Заe
может следоватьa
илиi
)dp[i]['i'] = (dp[i − 1]['a'] + dp[i − 1]['e'] + dp[i − 1]['o'] + dp[i − 1]['u'])
(Заi
могут следовать все буквы, кромеi
)dp[i]['o'] = (dp[i − 1]['i'] + dp[i − 1]['u'])
(Заo
может следоватьi
илиu
)dp[i]['u'] = dp[i − 1]['a']
(Заu
может следовать толькоa
)
Обращение по символу условное, для наглядности. Логично использовать индекс
0 <= j < 5
под каждую буквы.
По сути, достаточно пяти переменных и одного цикла от 2..n, где эти переменные будут обновляться по правилам выше, давая ответ для нового уровня. В итоге, ответом для всех возможных строк будет сумма пяти значений, то есть сколько существует строк длины n
, которые заканчиваются соответствующей буквой.
В итоге, получаем линейную сложность 🙌
P. S. Обсудить можно в телеграм-чате любознательных программистов. Welcome! 🤗
Подписывайтесь на мой твитер или канал в телеграме, чтобы узнавать о новых разборах задач.