LeetCode 91: Decode Ways Solution in Python Explained

Decoding strings into messages might feel like cracking a secret code, and LeetCode 91: Decode Ways is a medium-level challenge that makes it fun! Given a string of digits, you need to count how many ways it can be decoded into letters (1-26 mapping to A-Z), considering both single and two-digit combinations. In this blog, we’ll solve it with Python, exploring two solutions—Dynamic Programming Bottom-Up (our primary, efficient approach) and Recursive with Memoization (a top-down alternative). With step-by-step examples, detailed code breakdowns, and tips, you’ll master this problem. Let’s decode it!

Problem Statement

Section link icon

In LeetCode 91, you’re given a string s containing digits. Each digit (1-9) maps to a letter (A-I), and two-digit numbers (10-26) map to J-Z (0 alone isn’t valid). Your task is to return the number of possible decodings. This differs from subset generation like LeetCode 90: Subsets II, focusing on string interpretation.

Constraints

  • 1 <= s.length <= 100
  • s contains only digits (0-9)

Example

Let’s see some cases:

Input: s = "12"
Output: 2
Explanation: "12" can be decoded as "AB" (1,2) or "L" (12) - 2 ways.
Input: s = "226"
Output: 3
Explanation: "BBF" (2,2,6), "VF" (22,6), "BF" (2,26) - 3 ways.
Input: s = "06"
Output: 0
Explanation: "06" can’t be decoded (0 isn’t valid alone, 06 isn’t 1-26).

These examples show we’re counting valid decoding combinations.

Understanding the Problem

Section link icon

How do you solve LeetCode 91: Decode Ways in Python? Take s = "226". We need to count ways: "BBF" (2,2,6), "VF" (22,6), "BF" (2,26) = 3. Each digit can be decoded alone (1-9) or paired (10-26), but 0 complicates things. This isn’t a binary sequence like LeetCode 89: Gray Code; it’s about dynamic string partitioning. We’ll use: 1. Dynamic Programming Bottom-Up: O(n) time, O(n) space—our top pick. 2. Recursive with Memoization: O(n) time, O(n) space—a recursive option.

Let’s dive into the primary solution.

Solution 1: Dynamic Programming Bottom-Up Approach (Primary)

Section link icon

Explanation

Use a DP array where dp[i] is the number of ways to decode s[0:i]. For each position, add ways from using the last digit (if valid) and the last two digits (if valid). Iterate left to right, building solutions based on previous results.

For s = "226":

  • dp[0] = 1 (empty string).
  • dp[1] = 1 ("2").
  • dp[2] = 2 ("22" = "BB" or "V").
  • dp[3] = 3 ("226" = "BBF", "VF", "BF").

Step-by-Step Example

Example 1: s = "226"

Goal: Return 3.

  • Start: dp = [1,0,0,0] (length 4 for "226").
  • Step 1: i = 1, s[0] = "2".
    • Single digit (2): Valid, dp[1] += dp[0] = 1.
    • dp = [1,1,0,0].
  • Step 2: i = 2, s[1] = "2", s[0:2] = "22".
    • Single (2): dp[2] += dp[1] = 1.
    • Two-digit (22): Valid, dp[2] += dp[0] = 2.
    • dp = [1,1,2,0].
  • Step 3: i = 3, s[2] = "6", s[1:3] = "26".
    • Single (6): dp[3] += dp[2] = 2.
    • Two-digit (26): Valid, dp[3] += dp[1] = 3.
    • dp = [1,1,2,3].
  • Finish: Return dp[3] = 3.

Example 2: s = "12"

Goal: Return 2.

  • Start: dp = [1,0,0].
  • Step 1: i = 1, s[0] = "1".
    • Single (1): dp[1] = 1.
    • dp = [1,1,0].
  • Step 2: i = 2, s[1] = "2", s[0:2] = "12".
    • Single (2): dp[2] = 1.
    • Two-digit (12): dp[2] = 2.
    • dp = [1,1,2].
  • Finish: Return 2.

Example 3: s = "06"

Goal: Return 0.

  • Start: dp = [1,0,0].
  • Step 1: i = 1, s[0] = "0".
    • Single (0): Invalid, dp[1] = 0.
    • dp = [1,0,0].
  • Step 2: i = 2, s[1] = "6", s[0:2] = "06".
    • Single (6): dp[2] = 0 (dp[1]=0).
    • Two-digit (06): Invalid, dp[2] = 0.
    • dp = [1,0,0].
  • Finish: Return 0.

How the Code Works (Dynamic Programming Bottom-Up) – Detailed Line-by-Line Explanation

Here’s the Python code with a thorough breakdown:

def numDecodings(s: str) -> int:
    # Line 1: Initialize DP array
    dp = [0] * (len(s) + 1)
    # Creates array of zeros with length s + 1 (e.g., [0,0,0,0] for "226")
    # dp[i] = ways to decode s[0:i], extra slot for empty string

    # Line 2: Base case for empty string
    dp[0] = 1
    # dp[0] = 1, represents an empty string having 1 way (no decoding)
    # Foundation for building up solutions

    # Line 3: Iterate over each position in the string
    for i in range(1, len(s) + 1):
        # i goes from 1 to len(s) (e.g., 1 to 3 for "226")
        # Each iteration computes ways to decode up to index i

        # Line 4: Check single-digit decoding
        if s[i-1] != '0':
            # If last digit isn’t '0' (e.g., s[0]='2'), it’s decodable (1-9)
            # Add ways from previous position

            # Line 5: Add ways using single digit
            dp[i] += dp[i-1]
            # Adds the number of ways to decode up to i-1 (e.g., dp[1] += dp[0] = 1)
            # If s[0]='2', "2" has 1 way, same as empty string plus "B"

        # Line 6: Check two-digit decoding
        if i > 1 and '10' <= s[i-2:i] <= '26':
            # If i > 1 (at least two digits) and s[i-2:i] is between "10" and "26"
            # e.g., i=2, s[0:2]="22" is valid (22 <= 26)

            # Line 7: Add ways using two digits
            dp[i] += dp[i-2]
            # Adds ways from two positions back (e.g., dp[2] += dp[0] = 1)
            # For "22", adds 1 way ("V") to existing 1 way ("BB"), total 2

    # Line 8: Return the total number of ways
    return dp[len(s)]
    # Returns dp[len(s)], ways to decode the entire string (e.g., dp[3] = 3 for "226")

This detailed explanation makes the bottom-up DP process clear, efficiently counting decodings.

Alternative: Recursive with Memoization Approach

Section link icon

Explanation

Recursively compute ways to decode from each position, using memoization to cache results. At each step, try single-digit and two-digit decodings if valid, summing the possibilities.

For "226":

  • "2" + decode("26") = 2.
  • "22" + decode("6") = 1.
  • Total = 3.

Step-by-Step Example (Alternative)

For "226":

  • decode(0): "2" + decode(1) = 2, "22" + decode(2) = 1 → 3.
  • decode(1): "2" + decode(2) = 2.
  • decode(2): "6" + decode(3) = 1.
  • decode(3): 1 (empty).
  • Finish: 3.

How the Code Works (Recursive)

def numDecodingsRecursive(s: str) -> int:
    @lru_cache(None)
    def dp(i: int) -> int:
        if i == len(s):
            return 1
        if s[i] == '0':
            return 0
        ways = dp(i + 1)
        if i + 1 < len(s) and '10' <= s[i:i+2] <= '26':
            ways += dp(i + 2)
        return ways
    return dp(0)

Complexity

  • DP Bottom-Up:
    • Time: O(n) – single pass.
    • Space: O(n) – DP array.
  • Recursive with Memoization:
    • Time: O(n) – memoized states.
    • Space: O(n) – memoization cache.

Efficiency Notes

Both are O(n) time, but Bottom-Up avoids recursion overhead, making it slightly faster and more space-efficient—preferred for its clarity and performance.

Key Insights

  • DP State: Ways up to each position.
  • Zero Handling: Invalidates single-digit decoding.
  • Two-Digit Check: 10-26 range.

Additional Example

Section link icon

For s = "123":

  • Goal: 3.
  • DP: "1,2,3", "12,3", "1,23" → 3.

Edge Cases

Section link icon
  • Single Digit: "1"1.
  • All Zeros: "0"0.
  • Leading Zero: "06"0.

Both solutions handle these well.

Final Thoughts

Section link icon

LeetCode 91: Decode Ways in Python is a delightful string decoding puzzle. The Bottom-Up DP solution is efficient and intuitive, while Recursive with Memoization offers a top-down perspective. Want more? Try LeetCode 139: Word Break for string segmentation or LeetCode 90: Subsets II for combinatorics. Ready to practice? Solve LeetCode 91 on LeetCode with "226", aiming for 3—test your skills now!