0Pricing
DSA Interview Prep · Урок

Декодирование способов и подсчёт путей

Решайте задачу 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 — локальная установка не требуется.

Все уроки этого курса

  1. Грабитель домов: рекуррентное решение «взять или пропустить»
  2. Максимальный подмассив и подмассив с максимальным произведением
  3. Разбиение слов и сегментация строки
  4. Декодирование способов и подсчёт путей
← Назад к DSA Interview Prep