0Pricing
Coding Interview Prep · レッスン

Decode Waysと経路のカウント

decode-ways(数字から文字へのマッピング)をFibonacciに似たDPとして解き、次に可変ステップ幅の階段を上る経路数を数えます。

「Decode Waysと経路のカウント」はCoddyKit上の無料Coding Interview Prepレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはCoding Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Coding Interview Prepコースには全4レッスンが含まれています。

Decode Ways 問題

Decode Ways(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

Decode Ways のDP定式化

dp[i] を s[:i] のデコード方法の数とします。基本ケースは、dp[0] = 1(空文字列は1通り)で、s[1] != '0' なら dp[1] = 1、それ以外なら0です。遷移では、s[i-1] != '0' なら dp[i-1] を加えます(1桁のデコード)。また、10 ≤ int(s[i-2:i]) ≤ 26 なら dp[i-2] を加えます(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

先頭のゼロに関する注意点

Decode Ways で最も難しいのは、ゼロの扱いです。単独の「0」はデコードできません(0に対応する文字はないため)。そのため、s[i-1] == '0' の場合は dp[i-1] を加えません。2桁目の「0」が有効なのは、2桁の数が10または20の場合だけです。30や40(およびそれ以上)は26を超えるため無効です。two_digit ≤ 26 だけでなく、必ず 10 ≤ 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)

Decode Ways の空間最適化

フィボナッチ数列と同様に、デコード方法の漸化式は2つ前までしか参照しないため、2つの変数を使って空間計算量をO(n)からO(1)に削減できます。2つ前の値を保持する prev2 と、1つ前の値を保持する prev1 を使います。各ステップで両方から curr を計算し、その後に値をずらします。これはフィボナッチ数列における「2変数への最適化」と同じです。

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

階段を上る経路の数え上げ

Climbing Stairs(LeetCode 70)では、1回に1段または2段上れるとき、n段の階段を上る方法が何通りあるかを求めます。これはまさにフィボナッチ数列です。ways(n) = ways(n-1) + ways(n-2) となります。ways(1)=1、ways(2)=2、ways(3)=3、ways(4)=5 です。1回に最大 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...)

階段登りの最小コスト

Min Cost Climbing Stairs(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

Decode Ways II:ワイルドカードの数字

Decode Ways II(LeetCode 639)では、1~9の任意の数字を表せるワイルドカード文字「*」が導入されます。これにより、有効なデコード方法の数が大幅に増加します。単独の「*」は、1~9の任意の数字として9通りを表します。2つの「*」を組み合わせると、2桁の組み合わせは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

フィボナッチ数列との関係

Decode Ways と Climbing Stairs は、どちらも実質的にはフィボナッチ系の問題です。dp[i] が dp[i-1] と dp[i-2] だけに依存するDPは、フィボナッチ型であり、空間計算量O(1)で解けます。妥当性チェック(ゼロの数字や段数の種類)によって有効な遷移は変わりますが、2つ前まで参照する基本構造は変わりません。この系統の問題だとすぐに認識できることは、面接で素早く解くために役立つ重要なパターンです。

# 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解法では2次元テーブルを埋め、dp[i][j] = dp[i-1][j] + dp[i][j-1] とします。これはフィボナッチ型の階段問題を2次元にしたもので、各セルは上のセルと左のセルの合計になります。

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

面接での落とし穴のまとめ

Decode Ways でよくある落とし穴は次のとおりです。(1) 「0」単独は無効であることを忘れないこと。dp[i-1] を加える前に、必ず s[i-1] != '0' を確認してください。(2) two_digit <= 26 だけを使い、two_digit >= 10 を確認しないこと。「07」を「G」としてデコードしてはいけません。(3) dp[n] ではなく dp[n-1] を返してしまうこと。テーブルは1始まりで考えるため、dp[n] が文字列全体に対応します。DPテーブルの要素数が入力より1つ多い場合は、配列のインデックスを必ず再確認してください。

# 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)

理解度チェック

このレッスンで扱った Data Structures & Algorithms — Coding Interview Prep の概念について、理解度を確認しましょう。

レッスンのまとめ

このレッスンでは、Decode Ways は、1桁(ゼロ以外)および2桁(10~26)のデコードに対する妥当性チェックを伴う、フィボナッチ型の漸化式に従うこと、Climbing Stairs と Min Cost Staircase は、O(1)空間で解ける純粋なフィボナッチ系の変種であること、そして2つ前まで参照するフィボナッチ系の問題だと認識できれば、面接中の時間を大幅に節約できることを学びました。次は、グリッド上のUnique PathsとMinimum Path Sumを通して、2次元DPを学びます。

よくある質問

「Decode Waysと経路のカウント」レッスンは無料ですか?

はい。「Decode Waysと経路のカウント」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Coding Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Coding Interview Prepコースには全4レッスンが含まれています。

「Decode Waysと経路のカウント」で何を学びますか?

decode-ways(数字から文字へのマッピング)をFibonacciに似たDPとして解き、次に可変ステップ幅の階段を上る経路数を数えます。 ブラウザで直接実行するハンズオンコードでCoding Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Coding Interview Prepを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのCoding Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「Decode Waysと経路のカウント」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このCoding Interview Prepレッスンでコードを書いて実行できますか?

はい。すべてのCoding Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. House Robber:取るかスキップするかの漸化式
  2. 最大部分配列と最大積部分配列
  3. Word Breakと文字列の分割
  4. Decode Waysと経路のカウント
← Coding Interview Prepに戻る