title: Decode Ways
slug: decode-ways
difficulty: medium
leetcode_id: 91
leetcode_url: https://leetcode.com/problems/decode-ways/
categories:
  - strings
  - dynamic-programming
patterns:
  - slug: dynamic-programming
    is_optimal: true

function_signature: "def num_decodings(s: str) -> int:"

test_cases:
  visible:
    - input: { s: "12" }
      expected: 2
    - input: { s: "226" }
      expected: 3
    - input: { s: "06" }
      expected: 0
  hidden:
    - input: { s: "1" }
      expected: 1
    - input: { s: "10" }
      expected: 1
    - input: { s: "2101" }
      expected: 1
    - input: { s: "11106" }
      expected: 2
    - input: { s: "111" }
      expected: 3
    - input: { s: "27" }
      expected: 1
    - input: { s: "1234" }
      expected: 3

description: |
  You have intercepted a secret message encoded as a string of numbers. The message is **decoded** via the following mapping:

  `"1" -> 'A', "2" -> 'B', ..., "25" -> 'Y', "26" -> 'Z'`

  However, while decoding the message, you realise that there are many different ways you can decode the message because some codes are contained in other codes (`"2"` and `"5"` vs `"25"`).

  For example, `"11106"` can be decoded into:

  - `"AAJF"` with the grouping `(1, 1, 10, 6)`
  - `"KJF"` with the grouping `(11, 10, 6)`
  - The grouping `(1, 11, 06)` is invalid because `"06"` is not a valid code (only `"6"` is valid).

  Note: there may be strings that are impossible to decode.

  Given a string `s` containing only digits, return *the number of ways to decode it*. If the entire string cannot be decoded in any valid way, return `0`.

  The test cases are generated so that the answer fits in a **32-bit** integer.

constraints: |
  - `1 <= s.length <= 100`
  - `s` contains only digits and may contain leading zero(s).

examples:
  - input: 's = "12"'
    output: "2"
    explanation: '"12" could be decoded as "AB" (1 2) or "L" (12).'
  - input: 's = "226"'
    output: "3"
    explanation: '"226" could be decoded as "BZ" (2 26), "VF" (22 6), or "BBF" (2 2 6).'
  - input: 's = "06"'
    output: "0"
    explanation: '"06" cannot be mapped to "F" because of the leading zero ("6" is different from "06"). In this case, the string is not a valid encoding, so return 0.'

explanation:
  intuition: |
    Think of this problem like climbing a staircase, but with rules about which steps you can take.

    Imagine you're reading the encoded string character by character. At each position, you have a decision to make: do you decode just the current digit as a single letter, or do you combine it with the next digit to form a two-digit number?

    The key insight is that this is a **counting problem with overlapping subproblems**. The number of ways to decode a string depends on the number of ways to decode its suffixes. If you know how many ways there are to decode the remaining string after taking one digit, and how many ways after taking two digits, you can combine these counts.

    Think of it like this: at position `i`, the total decodings equals:
    - The number of decodings if you use `s[i]` as a single digit (valid if `s[i] != '0'`)
    - Plus the number of decodings if you use `s[i:i+2]` as a two-digit number (valid if it's between `10` and `26`)

    This recurrence relation is the foundation of the dynamic programming solution.

  approach: |
    We solve this using **Dynamic Programming** with a bottom-up approach:

    **Step 1: Handle edge cases**

    - If the string is empty or starts with `'0'`, return `0` immediately
    - A leading zero means no valid decoding exists

    &nbsp;

    **Step 2: Initialise the DP array**

    - Create an array `dp` where `dp[i]` represents the number of ways to decode `s[0:i]`
    - `dp[0] = 1`: An empty string has one way to decode (the empty decoding)
    - `dp[1] = 1`: A single non-zero digit has exactly one way to decode

    &nbsp;

    **Step 3: Fill the DP array**

    - For each position `i` from `2` to `n`:
    - **Single digit check**: If `s[i-1] != '0'`, add `dp[i-1]` to `dp[i]`
    - **Two digit check**: If the two-digit number `s[i-2:i]` is between `10` and `26`, add `dp[i-2]` to `dp[i]`

    &nbsp;

    **Step 4: Return the result**

    - Return `dp[n]`, which contains the total number of ways to decode the entire string

    &nbsp;

    The DP builds up from smaller subproblems. Each position accumulates counts from valid single-digit and two-digit decodings.

  common_pitfalls:
    - title: Ignoring Leading Zeros
      description: |
        A common mistake is forgetting that `'0'` cannot be decoded on its own. The mapping starts at `'1' -> 'A'`, not `'0' -> something`.

        For example, `"06"` has zero valid decodings because you cannot use `'0'` as a single digit, and `"06"` is not a valid two-digit code (only `"10"` to `"26"` are valid).

        Always check that a single digit is not `'0'` before counting it as a valid decoding.
      wrong_approach: "Treating '0' as a valid single-digit code"
      correct_approach: "Skip single-digit decodings when the digit is '0'"

    - title: Incorrect Two-Digit Range
      description: |
        The valid two-digit codes are `"10"` through `"26"` only. Common errors include:
        - Accepting `"00"` through `"09"` (leading zeros are invalid)
        - Accepting `"27"` through `"99"` (no letters map to these)

        The check should be: the two-digit number must be `>= 10` AND `<= 26`.
      wrong_approach: "Checking only if two-digit value <= 26"
      correct_approach: "Check if 10 <= two_digit_value <= 26"

    - title: Confusing Index vs String Length
      description: |
        When implementing the DP, be careful about indices. `dp[i]` represents decodings for the first `i` characters, so:
        - `dp[0]` = 1 (base case: empty prefix)
        - `dp[1]` = 1 if `s[0] != '0'`, else 0
        - For `dp[i]`, check `s[i-1]` for single digit and `s[i-2:i]` for two digits

        Off-by-one errors here lead to incorrect results or index out of bounds.

  key_takeaways:
    - "**DP recurrence**: `dp[i] = dp[i-1]` (if valid single digit) `+ dp[i-2]` (if valid two digits)"
    - "**Fibonacci-like pattern**: This problem is similar to Climbing Stairs but with validity conditions at each step"
    - "**Space optimisation**: Since we only need the previous two values, we can reduce space from O(n) to O(1)"
    - "**Foundation for variants**: This pattern extends to Decode Ways II (with wildcards) and other string partition problems"

  time_complexity: "O(n). We iterate through the string once, performing constant-time operations at each position."
  space_complexity: "O(n) for the DP array, or O(1) if optimised to use only two variables."

solutions:
  - approach_name: Dynamic Programming (Array)
    is_optimal: true
    code: |
      def num_decodings(s: str) -> int:
          # Edge case: string starts with '0'
          if not s or s[0] == '0':
              return 0

          n = len(s)
          # dp[i] = number of ways to decode s[0:i]
          dp = [0] * (n + 1)

          # Base cases
          dp[0] = 1  # Empty string has one way (empty decoding)
          dp[1] = 1  # First char is valid (we checked it's not '0')

          for i in range(2, n + 1):
              # Single digit: check if current char is valid (not '0')
              if s[i - 1] != '0':
                  dp[i] += dp[i - 1]

              # Two digits: check if previous two chars form valid code (10-26)
              two_digit = int(s[i - 2:i])
              if 10 <= two_digit <= 26:
                  dp[i] += dp[i - 2]

          return dp[n]
    explanation: |
      **Time Complexity:** O(n) — Single pass through the string.

      **Space Complexity:** O(n) — DP array stores one value per position.

      We build up the solution by counting valid decodings at each position. The recurrence combines counts from single-digit and two-digit valid decodings.

  - approach_name: Dynamic Programming (Space Optimised)
    is_optimal: true
    code: |
      def num_decodings(s: str) -> int:
          # Edge case: string starts with '0'
          if not s or s[0] == '0':
              return 0

          # Only need previous two values, not full array
          prev2 = 1  # dp[i-2]: ways to decode up to 2 positions back
          prev1 = 1  # dp[i-1]: ways to decode up to 1 position back

          for i in range(1, len(s)):
              current = 0

              # Single digit: current char is valid (not '0')
              if s[i] != '0':
                  current += prev1

              # Two digits: chars at i-1 and i form valid code (10-26)
              two_digit = int(s[i - 1:i + 1])
              if 10 <= two_digit <= 26:
                  current += prev2

              # Slide the window forward
              prev2 = prev1
              prev1 = current

          return prev1
    explanation: |
      **Time Complexity:** O(n) — Single pass through the string.

      **Space Complexity:** O(1) — Only two variables used regardless of input size.

      Since each position only depends on the previous two values, we can use a sliding window of two variables instead of a full array. This is the Fibonacci optimisation pattern.

  - approach_name: Recursion with Memoisation
    is_optimal: false
    code: |
      def num_decodings(s: str) -> int:
          memo = {}

          def decode(index: int) -> int:
              # Reached the end: one valid way found
              if index == len(s):
                  return 1

              # Leading zero: no valid decoding
              if s[index] == '0':
                  return 0

              # Return cached result if available
              if index in memo:
                  return memo[index]

              # Option 1: Take single digit
              ways = decode(index + 1)

              # Option 2: Take two digits if valid
              if index + 1 < len(s):
                  two_digit = int(s[index:index + 2])
                  if two_digit <= 26:
                      ways += decode(index + 2)

              memo[index] = ways
              return ways

          return decode(0)
    explanation: |
      **Time Complexity:** O(n) — Each index is computed once and cached.

      **Space Complexity:** O(n) — Recursion stack and memoisation dictionary.

      This top-down approach is often more intuitive. We recursively try both options (one digit or two digits) and cache results. The base case is reaching the end of the string (one valid decoding found).
