Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

639. Decode Ways II - Leetcode Solution

Problem Description

You are given a string s containing digits and the character '*'. The '*' character can represent any digit from '1' to '9'. Each digit or pair of digits can be mapped to a letter using the mapping: 'A' -> 1, 'B' -> 2, ..., 'Z' -> 26.

Your task is to determine how many different ways you can decode the string s, considering all possible interpretations of the '*' characters. The answer may be very large, so return it modulo 10^9 + 7.

  • Each character in s must be decoded as part of a valid mapping.
  • '*' can take any value from '1' to '9' for each occurrence.
  • You cannot use leading zeros or map numbers not in the range 1 to 26.
  • Return the total number of ways to decode s, modulo 10^9 + 7.

Thought Process

The problem is an extension of the classic "Decode Ways" problem, but now with the addition of the '*' wildcard. At first glance, one might consider generating all possible replacements for '*' and then counting the number of valid decodings for each. However, this brute-force approach quickly becomes infeasible as the number of '*' increases, since each one multiplies the number of possibilities by up to 9.

Instead, we recognize that this problem has overlapping subproblems and optimal substructure—key signs for applying dynamic programming (DP). We can build up the solution by considering, for each position in the string, how many ways the substring up to that point can be decoded, using previous results to efficiently compute the current answer.

The main challenge is carefully handling all the cases introduced by the '*' character, both when it appears alone and when it forms a two-digit number with the previous character.

Solution Approach

We use dynamic programming to solve this problem efficiently. Let's break down the approach step by step:

  1. DP State Definition:
    Let dp[i] represent the number of ways to decode the substring s[0:i] (the first i characters).
  2. Base Cases:
    • dp[0] = 1 (empty string has one way to decode: do nothing)
    • dp[1]: depends on s[0]. If s[0] == '*', there are 9 ways (for '1' to '9'); if s[0] == '0', zero ways; otherwise, one way.
  3. Transition:
    For each position i from 2 to len(s):
    • Single character:
      • If s[i-1] == '*', it can be any digit from 1 to 9: add 9 * dp[i-1].
      • If s[i-1] != '0', add dp[i-1].
    • Two characters:
      • If both s[i-2] and s[i-1] are digits, check if the two-digit number is between 10 and 26. If so, add dp[i-2].
      • If either is '*', enumerate all valid combinations and count how many are valid two-digit numbers from 10 to 26, multiplying by dp[i-2] accordingly.
  4. Modulo Operation:
    Since the result can be very large, take dp[i] % (10^9 + 7) at every step.
  5. Optimization:
    We only need the last two DP values at each step, so we can reduce space complexity to O(1).

By carefully considering all cases for '*' and using DP, we efficiently count all valid decodings.

Example Walkthrough

Let's walk through an example input: s = "1*"

  1. Initialization:
    dp[0] = 1 (for the empty string)
    dp[1] = 1 (since s[0] = '1')
  2. At position i = 2 (s[1] = '*'):
    • Single-character decode:
      '*' can be any of '1' to '9', so 9 * dp[1] = 9 * 1 = 9
    • Two-character decode:
      s[0] = '1', s[1] = '*'. Together, '1*' can be '11' to '19' (all valid), so 9 options: 9 * dp[0] = 9 * 1 = 9
    • Total:
      dp[2] = 9 (single) + 9 (double) = 18

So, there are 18 ways to decode "1*".

Time and Space Complexity

  • Brute-force approach:
    For each '*' there are up to 9 possibilities, so time complexity is O(9^k * n), where k is the number of '*' characters and n is the string length. This is infeasible for large inputs.
  • Optimized DP approach:
    We process each character in the string exactly once, and each step does O(1) work, so time complexity is O(n).
    Space complexity is O(1) (constant), since we only keep the last two DP values.

Summary

By recognizing the structure of the problem and leveraging dynamic programming, we efficiently compute the number of ways to decode a string with digits and wildcards. The key is to account for all possibilities introduced by the '*' character, both as a single digit and as part of two-digit numbers. By carefully handling these cases and using only constant space, we achieve an elegant and optimal solution.

Code Implementation

class Solution:
    def numDecodings(self, s: str) -> int:
        MOD = 10 ** 9 + 7
        n = len(s)
        if not s:
            return 0

        prev = 1  # dp[0]
        curr = 9 if s[0] == '*' else (0 if s[0] == '0' else 1)

        for i in range(1, n):
            temp = 0

            # Single digit
            if s[i] == '*':
                temp += 9 * curr
            elif s[i] != '0':
                temp += curr

            # Two digits
            if s[i-1] == '*' and s[i] == '*':
                # '**' can be 11-19 and 21-26
                temp += 15 * prev  # 9 + 6
            elif s[i-1] == '*':
                if '0' <= s[i] <= '6':
                    temp += 2 * prev  # '1x' and '2x'
                elif '7' <= s[i] <= '9':
                    temp += prev  # only '1x'
            elif s[i] == '*':
                if s[i-1] == '1':
                    temp += 9 * prev  # '11'-'19'
                elif s[i-1] == '2':
                    temp += 6 * prev  # '21'-'26'
            else:
                two = int(s[i-1:i+1])
                if 10 <= two <= 26:
                    temp += prev

            temp %= MOD
            prev, curr = curr, temp

        return curr
      
class Solution {
public:
    int numDecodings(string s) {
        const int MOD = 1e9 + 7;
        int n = s.size();
        long prev = 1;
        long curr = (s[0] == '*') ? 9 : (s[0] == '0' ? 0 : 1);

        for (int i = 1; i < n; ++i) {
            long temp = 0;
            // Single digit
            if (s[i] == '*') {
                temp += 9 * curr;
            } else if (s[i] != '0') {
                temp += curr;
            }
            // Two digits
            if (s[i-1] == '*' && s[i] == '*') {
                temp += 15 * prev; // 11-19 and 21-26
            } else if (s[i-1] == '*') {
                if (s[i] >= '0' && s[i] <= '6') {
                    temp += 2 * prev;
                } else if (s[i] >= '7' && s[i] <= '9') {
                    temp += prev;
                }
            } else if (s[i] == '*') {
                if (s[i-1] == '1') {
                    temp += 9 * prev;
                } else if (s[i-1] == '2') {
                    temp += 6 * prev;
                }
            } else {
                int two = (s[i-1] - '0') * 10 + (s[i] - '0');
                if (two >= 10 && two <= 26) {
                    temp += prev;
                }
            }
            temp %= MOD;
            prev = curr;
            curr = temp;
        }
        return (int)curr;
    }
};
      
class Solution {
    public int numDecodings(String s) {
        int MOD = 1000000007;
        int n = s.length();
        long prev = 1;
        long curr = (s.charAt(0) == '*') ? 9 : (s.charAt(0) == '0' ? 0 : 1);

        for (int i = 1; i < n; ++i) {
            long temp = 0;
            // Single digit
            if (s.charAt(i) == '*') {
                temp += 9 * curr;
            } else if (s.charAt(i) != '0') {
                temp += curr;
            }
            // Two digits
            if (s.charAt(i-1) == '*' && s.charAt(i) == '*') {
                temp += 15 * prev;
            } else if (s.charAt(i-1) == '*') {
                if (s.charAt(i) >= '0' && s.charAt(i) <= '6') {
                    temp += 2 * prev;
                } else if (s.charAt(i) >= '7' && s.charAt(i) <= '9') {
                    temp += prev;
                }
            } else if (s.charAt(i) == '*') {
                if (s.charAt(i-1) == '1') {
                    temp += 9 * prev;
                } else if (s.charAt(i-1) == '2') {
                    temp += 6 * prev;
                }
            } else {
                int two = (s.charAt(i-1) - '0') * 10 + (s.charAt(i) - '0');
                if (two >= 10 && two <= 26) {
                    temp += prev;
                }
            }
            temp %= MOD;
            prev = curr;
            curr = temp;
        }
        return (int)curr;
    }
}
      
var numDecodings = function(s) {
    const MOD = 1e9 + 7;
    let n = s.length;
    let prev = 1;
    let curr = s[0] === '*' ? 9 : (s[0] === '0' ? 0 : 1);

    for (let i = 1; i < n; ++i) {
        let temp = 0;
        // Single digit
        if (s[i] === '*') {
            temp += 9 * curr;
        } else if (s[i] !== '0') {
            temp += curr;
        }
        // Two digits
        if (s[i-1] === '*' && s[i] === '*') {
            temp += 15 * prev;
        } else if (s[i-1] === '*') {
            if ('0' <= s[i] && s[i] <= '6') {
                temp += 2 * prev;
            } else if ('7' <= s[i] && s[i] <= '9') {
                temp += prev;
            }
        } else if (s[i] === '*') {
            if (s[i-1] === '1') {
                temp += 9 * prev;
            } else if (s[i-1] === '2') {
                temp += 6 * prev;
            }
        } else {
            let two = parseInt(s[i-1]) * 10 + parseInt(s[i]);
            if (two >= 10 && two <= 26) {
                temp += prev;
            }
        }
        temp %= MOD;
        [prev, curr] = [curr, temp];
    }
    return curr;
};