Декодирование способов и подсчёт путей
Решайте задачу decode-ways с отображением цифр на буквы как DP, подобную Фибоначчи, а затем считайте пути по лестнице с шагами разного размера
«Декодирование способов и подсчёт путей» — бесплатный урок DSA Interview Prep на CoddyKit. Это урок 4 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения DSA Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс DSA Interview Prep содержит 4 уроков всего.
Задача о декодировании
Декодирование (LeetCode 91) сопоставляет строку цифр с буквами: 'A'=1, 'B'=2, ..., 'Z'=26. Для заданной закодированной строки цифр подсчитайте количество различных способов декодирования. Например, '12' можно декодировать как 'AB' (1+2) или 'L' (12), то есть существует 2 способа. '226' можно декодировать как 'BZ' (2+26), 'VF' (22+6) или 'BBF' (2+2+6), то есть существует 3 способа. Начальные нули делают некоторые варианты декодирования недопустимыми.
# Encoding: A=1, B=2, ..., Z=26
# '12' → 'AB' or 'L' → 2 ways
# '226' → 'BZ' or 'VF' or 'BBF' → 3 ways
# '06' → invalid (no letter for '0')
# '10' → 'J' only → 1 way (only valid as 10, not 1+0)
s = '226'
print('Decodings for', s, ':', 3) # Expected: 3Формулировка DP для декодирования
Пусть dp[i] = количество способов декодировать s[:i]. Базовые случаи: dp[0] = 1 (пустая строка, один способ) и dp[1] = 1, если s[0] != '0', иначе 0. Переход: если s[i-1] != '0', добавьте dp[i-1] (однозначное декодирование). Если 10 ≤ int(s[i-2:i]) ≤ 26, добавьте dp[i-2] (двузначное декодирование). По сути, это схема Фибоначчи с проверками допустимости.
def num_decodings(s):
n = len(s)
dp = [0] * (n + 1)
dp[0] = 1 # empty prefix
dp[1] = 0 if s[0] == '0' else 1
for i in range(2, n + 1):
# Single digit decode
if s[i-1] != '0':
dp[i] += dp[i-1]
# Two digit decode
two_digit = int(s[i-2:i])
if 10 <= two_digit <= 26:
dp[i] += dp[i-2]
return dp[n]
print(num_decodings('12')) # 2
print(num_decodings('226')) # 3
print(num_decodings('06')) # 0Ловушка ведущего нуля
Самая сложная часть задачи «Декодирование способов» — обработка нулей. Отдельный «0» нельзя декодировать (ни одна буква не соответствует 0), поэтому, если s[i-1] == '0', не добавляйте dp[i-1]. «0» в качестве второй цифры допустим только тогда, когда двузначное число равно 10 или 20. «30» или «40» (и большие значения) недопустимы, поскольку они превышают 26. Всегда проверяйте 10 ≤ two_digit ≤ 26, а не только two_digit ≤ 26.
def num_decodings(s):
if not s or s[0] == '0': return 0
n = len(s)
dp = [0] * (n + 1)
dp[0] = 1
dp[1] = 1 # s[0] != '0' guaranteed by guard above
for i in range(2, n + 1):
one = int(s[i-1])
two = int(s[i-2:i])
if one != 0: dp[i] += dp[i-1] # valid single digit
if 10 <= two <= 26: dp[i] += dp[i-2] # valid two digits
return dp[n]
print(num_decodings('10')) # 1 (only 'J')
print(num_decodings('30')) # 0 (30 > 26, '0' alone invalid)
print(num_decodings('100')) # 0 (dp[2]=1 then '00' invalid, single '0' invalid)Декодирование способов с оптимизацией памяти
Как и в последовательности Фибоначчи, рекуррентная формула для количества способов декодирования обращается только к двум предыдущим позициям, поэтому память можно сократить с O(n) до O(1) с помощью двух переменных. Используйте prev2 (значение двумя шагами ранее) и prev1 (значение одним шагом ранее). На каждом шаге вычисляйте curr на основе обоих значений, а затем сдвигайте их. Это та же оптимизация последовательности Фибоначчи → оптимизация с двумя переменными.
def num_decodings_o1(s):
if not s or s[0] == '0': return 0
prev2 = 1 # dp[0]
prev1 = 1 # dp[1]
for i in range(2, len(s) + 1):
curr = 0
if s[i-1] != '0':
curr += prev1
two = int(s[i-2:i])
if 10 <= two <= 26:
curr += prev2
prev2, prev1 = prev1, curr
return prev1
print(num_decodings_o1('226')) # 3
print(num_decodings_o1('12')) # 2
print(num_decodings_o1('0')) # 0Подсчёт путей на лестнице
Подъём по лестнице (LeetCode 70) — это задача о том, сколькими способами можно подняться по лестнице из n ступеней, если за один раз разрешено делать 1 или 2 шага. Это в точности последовательность Фибоначчи: ways(n) = ways(n-1) + ways(n-2). ways(1)=1, ways(2)=2, ways(3)=3, ways(4)=5. Обобщение для случая, когда можно делать до k шагов: ways(n) = sum(ways(n-1), ..., ways(n-k)).
def climb_stairs(n):
if n <= 2: return n
prev2, prev1 = 1, 2
for _ in range(3, n + 1):
prev2, prev1 = prev1, prev1 + prev2
return prev1
for i in range(1, 8):
print(f'climb_stairs({i}) = {climb_stairs(i)}')
# 1, 2, 3, 5, 8, 13, 21 — Fibonacci!Подъём по лестнице с переменным числом шагов
Когда можно делать любое число шагов из заданного набора (например, {1, 3, 5}), рекуррентная формула принимает вид dp[i] = sum(dp[i-k] for k in steps if i-k >= 0). Для экономии памяти используйте скользящее окно размером max(steps). Это вариант подсчёта в задаче о неограниченном рюкзаке — каждый размер шага можно использовать любое количество раз.
def count_ways(n, steps):
dp = [0] * (n + 1)
dp[0] = 1 # one way to stay at ground
for i in range(1, n + 1):
for step in steps:
if i >= step:
dp[i] += dp[i - step]
return dp[n]
# Steps of 1 or 2 (classic climbing stairs)
print(count_ways(5, [1, 2])) # 8
# Steps of 1, 3, or 5
print(count_ways(5, [1, 3, 5])) # 5
# Steps of 2 or 3
print(count_ways(6, [2, 3])) # 3 (2+2+2, 3+3, 2+4-invalid, 2+2+2, 3+3, 3+2+1-no...)Подъём по лестнице с минимальной стоимостью
Подъём по лестнице с минимальной стоимостью (LeetCode 746) — это задача, в которой каждому шагу назначена стоимость и требуется найти минимальную стоимость достижения вершины. С шага i можно перейти на i+1 или i+2. Рекуррентная формула: dp[i] = cost[i] + min(dp[i-1], dp[i-2]). Начать можно с шага 0 или с шага 1. Ответ равен min(dp[n-1], dp[n-2]).
def min_cost_climbing(cost):
n = len(cost)
if n == 1: return cost[0]
dp = [0] * n
dp[0] = cost[0]
dp[1] = cost[1]
for i in range(2, n):
dp[i] = cost[i] + min(dp[i-1], dp[i-2])
return min(dp[-1], dp[-2]) # can start from step 0 or 1
print(min_cost_climbing([10, 15, 20])) # 15
print(min_cost_climbing([1, 100, 1, 1, 1, 100, 1, 1, 100, 1])) # 6Декодирование способов II: символ подстановки
Декодирование способов II (LeetCode 639) добавляет символ подстановки «*», который может обозначать любую цифру от 1 до 9. Это резко увеличивает количество допустимых вариантов декодирования. Один «*» даёт 9 вариантов — по одному для каждой цифры от 1 до 9. Два символа «*» вместе могут образовать 9×9 двузначных комбинаций, но допустимы только числа не больше 26 (11–19 дают 9 вариантов, 21–26 — 6 вариантов, то есть для «**» всего 15 вариантов). Требуется внимательно разобрать все случаи.
def num_decodings_ii(s):
MOD = 10**9 + 7
prev2, prev1 = 1, 9 if s[0] == '*' else (0 if s[0] == '0' else 1)
for i in range(1, len(s)):
curr = 0
c, p = s[i], s[i-1]
# Single digit
if c == '*': curr += 9 * prev1
elif c != '0': curr += prev1
# Two digits
if p == '*' and c == '*': curr += 15 * prev2 # 11-19(9) + 21-26(6)
elif p == '*': curr += (2 if c <= '6' else 1) * prev2
elif c == '*': curr += (9 if p == '1' else (6 if p == '2' else 0)) * prev2
else:
two = int(p + c)
if 10 <= two <= 26: curr += prev2
prev2, prev1 = prev1, curr % MOD
return prev1 % MOD
print(num_decodings_ii('*')) # 9
print(num_decodings_ii('1*')) # 18Связь с Фибоначчи
И задача «Декодирование способов», и задача «Подъём по лестнице» — это замаскированные задачи семейства Фибоначчи. Любая задача DP, в которой dp[i] зависит только от dp[i-1] и dp[i-2], имеет структуру Фибоначчи и решается с памятью O(1). Проверки допустимости (нулевые цифры, размеры шагов) изменяют набор активных переходов, но не меняют фундаментальную структуру с обращением к двум предыдущим значениям. Умение сразу распознавать это семейство — ценный навык, помогающий работать быстрее на собеседованиях.
# Fibonacci family: dp[i] = f(dp[i-1], dp[i-2])
# Fibonacci itself: dp[i] = dp[i-1] + dp[i-2]
# Climbing stairs: dp[i] = dp[i-1] + dp[i-2]
# Decode ways: dp[i] = (dp[i-1] if one_valid) + (dp[i-2] if two_valid)
# Min cost stairs: dp[i] = cost[i] + min(dp[i-1], dp[i-2])
# House robber: dp[i] = max(dp[i-1], nums[i] + dp[i-2])
# All solved with 2 rolling variables:
prev2, prev1 = 0, 1
for _ in range(10):
prev2, prev1 = prev1, prev1 + prev2
print('Fibonacci F(10):', prev1) # 89Подсчёт путей в таблице
Рассмотрим связанную задачу: дана сетка размером m×n, требуется определить количество уникальных путей из верхнего левого угла в нижний правый, если двигаться можно только вправо или вниз. Ответом является биномиальный коэффициент C(m+n-2, m-1). Решение с помощью DP заполняет двумерную таблицу, где dp[i][j] = dp[i-1][j] + dp[i][j-1]. Это двумерная версия лестницы Фибоначчи: каждая ячейка равна сумме ячейки сверху и ячейки слева.
def unique_paths(m, n):
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
# Or use math for O(1) solution
import math
def unique_paths_math(m, n):
return math.comb(m + n - 2, m - 1)
print(unique_paths(3, 7)) # 28
print(unique_paths_math(3, 7)) # 28
print(unique_paths(3, 3)) # 6Итоги типичных ошибок на собеседовании
Типичные ошибки в задаче «Декодирование способов»: (1) забыть, что один «0» недопустим — всегда проверяйте s[i-1] != '0' перед добавлением dp[i-1]; (2) использовать two_digit <= 26, не проверяя two_digit >= 10 — «07» не должно декодироваться как «G»; (3) вернуть dp[n-1] вместо dp[n] — таблица индексируется с 1, поэтому dp[n] соответствует всей строке. Всегда перепроверяйте индексы массива, если таблица DP содержит на один элемент больше, чем входные данные.
# Common bug: checking two_digit <= 26 without >= 10
def buggy_decode(s):
dp = [0] * (len(s) + 1)
dp[0] = dp[1] = 1
for i in range(2, len(s) + 1):
if s[i-1] != '0': dp[i] += dp[i-1]
two = int(s[i-2:i])
# BUG: '07' gives two=7, and 7 <= 26 would add dp[i-2]
# Fix: require two >= 10
if 10 <= two <= 26: dp[i] += dp[i-2] # CORRECT
return dp[len(s)]
print(buggy_decode('06')) # 0 (correct, '0' alone invalid)
print(buggy_decode('07')) # 0 (correct, '07' not valid, '0' alone invalid)
print(buggy_decode('27')) # 1 (only 'BG', 27>26 so no two-digit)Быстрая проверка
Проверьте своё понимание концепций «Структуры данных и алгоритмы — подготовка к техническому собеседованию», рассмотренных в этом уроке.
Итоги урока
В этом уроке Вы узнали: задача «Декодирование способов» использует рекуррентную формулу, похожую на формулу Фибоначчи, с проверками допустимости для однозначного (ненулевого) и двузначного (10–26) декодирования, задачи «Подъём по лестнице» и «Лестница с минимальной стоимостью» являются вариантами последовательности Фибоначчи и решаются с памятью O(1), а также распознавание семейства Фибоначчи с обращением к двум предыдущим значениям значительно экономит время на собеседованиях. Далее мы рассмотрим двумерное DP: задачи «Уникальные пути» и «Минимальная сумма пути» на сетках.
Часто задаваемые вопросы
Урок «Декодирование способов и подсчёт путей» бесплатный?
Да — полный текст урока «Декодирование способов и подсчёт путей» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс DSA Interview Prep, подпишись на CoddyKit PRO. Курс DSA Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Декодирование способов и подсчёт путей»?
Решайте задачу decode-ways с отображением цифр на буквы как DP, подобную Фибоначчи, а затем считайте пути по лестнице с шагами разного размера Ты практикуешь DSA Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать DSA Interview Prep?
Предыдущий опыт не требуется. DSA Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 4 из 4.
Сколько времени занимает урок «Декодирование способов и подсчёт путей»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке DSA Interview Prep?
Да. Каждый урок DSA Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Грабитель домов: рекуррентное решение «взять или пропустить»
- Максимальный подмассив и подмассив с максимальным произведением
- Разбиение слов и сегментация строки
- Декодирование способов и подсчёт путей