Decode Ways - 数字文字列の復号方法カウント

ローリングDP(O(n) 時間 / O(1) 空間)で効率的に解く動的計画法の視覚化

📋 アルゴリズム概要

問題: 数字文字列 s が与えられ、1→A, 2→B, ..., 26→Z のマッピングでデコードできる方法の総数を返します。

制約: 1桁は 1~9 のみ有効(0 単独は無効)、2桁は 10~26 のみ有効(先頭0は不可)。

戦略: ローリング動的計画法を使用。各位置で「1桁として取る」「2桁として取る」の2通りを判定し、前2状態(prev2, prev1)から現在の通り数を計算します。

✨ 最適化ポイント

  • 配列不要: スカラー変数2個(prev2, prev1)のみで O(1) 空間
  • 早期終了: デコード不能を検知した時点で即座に 0 を返す
  • 整数演算: 文字列スライス回避で高速化

🎯 ステップバイステップ解説

💻 Python コード実装

LeetCode 形式の Solution クラス。ローリングDP で O(n) 時間 / O(1) 空間を実現。

from __future__ import annotations
from typing import Final


class Solution:
    """
    Decode Ways (LeetCode 91)
    数字文字列を A-Z ('1'-'26') にデコードする方法の総数を返す。

    アルゴリズム:
    - ローリングDP (prev2, prev1) で O(n) 時間 / O(1) 追加メモリ
    - 1桁: '1'..'9' が有効
    - 2桁: '10'..'26' が有効
    - '0' 単独や先頭0は無効
    """

    def numDecodings(self, s: str) -> int:
        """
        Args:
            s: 数字のみから成る文字列(長さ 1..100)

        Returns:
            デコード方法の総数(不能なら 0)

        Complexity:
            Time: O(n), Space: O(1)
        """
        n: Final[int] = len(s)

        # 基底条件: 空文字列は不正
        if n == 0:
            return 0

        # 先頭が '0' ならデコード不能
        first_digit: int = ord(s[0]) - ord('0')
        if first_digit == 0:
            return 0

        # DP初期値:
        # prev2 = dp[-1] = 1 (空文字列の基数)
        # prev1 = dp[0] = 1 (先頭1文字、'1'..'9' が確定)
        prev2: int = 1
        prev1: int = 1

        # 位置 1 から n-1 まで処理
        for i in range(1, n):
            d1: int = ord(s[i]) - ord('0')        # 現在の1桁
            d0: int = ord(s[i - 1]) - ord('0')    # 直前の1桁

            cur: int = 0

            # 遷移1: 1桁が有効 ('1'..'9')
            if 1 <= d1 <= 9:
                cur += prev1

            # 遷移2: 2桁が有効 ('10'..'26')
            two_digit: int = d0 * 10 + d1
            if 10 <= two_digit <= 26:
                cur += prev2

            # どちらも不可 → デコード不能
            if cur == 0:
                return 0

            # ローリング更新
            prev2, prev1 = prev1, cur

        return prev1

📊 視覚的フローチャート

アルゴリズムの処理フロー全体を俯瞰する静的図解。

開始 最初の文字が '0'? いいえ はい 0を返す 初期化 prev2=1, prev1=1 i = 1 から n-1 まで 位置 i を処理 d0 = s[i-1] を取得 d1 = s[i] 1 ≤ d1 ≤ 9? cur += prev1 10 ≤ d0*10+d1 ≤ 26? cur += prev2 cur == 0? はい 0を返す いいえ 更新 prev2, prev1 prev1を返す ループ終了

各位置で1桁・2桁の有効性を判定し、通り数を累積。どちらも不可なら即座に 0 を返す。

💡 アルゴリズムの概要

動的計画法: 各位置での有効なデコード方法数を前の2つの位置から計算します。

1桁の場合: 現在の文字が1〜9なら、前の位置の方法数を加算します。

2桁の場合: 前の文字と現在の文字で10〜26の範囲なら、2つ前の位置の方法数を加算します。

無効な場合: 1桁も2桁も有効でない場合、デコード不可能として0を返します。

📝 例: "226"のデコード

方法1: BBF

2 → B, 2 → B, 6 → F

方法2: VF

22 → V, 6 → F

方法3: BZ

2 → B, 26 → Z

⚡ 計算量分析

⏱️ 時間計算量

O(n)

各文字を1回ずつ処理。各ステップは定数時間の演算(整数加算、比較、更新)のみ。

💾 空間計算量

O(1)

追加メモリはスカラー変数3個(prev2, prev1, cur)のみ。配列不要。

📋 アプローチ比較

手法 時間 空間 備考
ローリングDP(採用) O(n) O(1) 最小メモリ、実装簡潔
配列DP O(n) O(n) 学習用に明快
再帰+メモ化 O(n) O(n) スタック深度+キャッシュ