LeetCode 639 - Decode Ways II
The problem asks us to determine the number of ways an encoded string can be decoded into letters, where 'A' maps to "1", 'B' maps to "2", up to 'Z' mapping to "26".
Difficulty: 🔴 Hard
Topics: String, Dynamic Programming
Solution
Problem Understanding
The problem asks us to determine the number of ways an encoded string can be decoded into letters, where 'A' maps to "1", 'B' maps to "2", up to 'Z' mapping to "26". The input string s contains digits '0'-'9' and potentially the wildcard character '*', which represents any digit from '1' to '9'. The output is the total number of valid decodings modulo $10^9 + 7$.
The key challenges are handling the '*' wildcard and ensuring that combinations of one-digit and two-digit numbers are correctly counted without double counting or including invalid sequences like "06". The string can be up to $10^5$ characters long, which rules out brute-force recursion due to exponential growth in the number of combinations.
Important edge cases include strings consisting entirely of '*', sequences containing '0' (which can only be part of '10' or '20'), and strings that mix digits and '*' in a way that creates multiple valid interpretations.
Approaches
The brute-force approach is to recursively consider every character in the string as either a single-digit decoding or, if possible, a two-digit decoding. For '*', we would iterate through all 9 possible digits at that position. This is correct in theory but infeasible due to exponential time complexity, $O(9^n)$, where $n$ is the length of the string.
The optimal approach uses dynamic programming. Let dp[i] represent the number of ways to decode the substring s[0:i]. The transition considers one-character and two-character combinations. For one-character, if s[i-1] is '*', it contributes 9 times dp[i-1]; otherwise, it contributes 1 if not '0'. For two-character combinations, we consider the valid ranges that form numbers from 10 to 26, carefully counting the possibilities when '*' appears in either position. The space can be optimized to O(1) by storing only the last two DP values, since each dp[i] only depends on dp[i-1] and dp[i-2].
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(9^n) | O(n) | Recursively explore all interpretations of * and digits |
| Optimal DP | O(n) | O(1) | Use DP with modulo arithmetic and handle * carefully |
Algorithm Walkthrough
-
Initialize
MOD = 10^9 + 7to handle large numbers. -
Create two variables,
dp1anddp2, representingdp[i-1]anddp[i-2]. Initially,dp2 = 1(empty string has 1 decoding), anddp1depends on the first character: 9 if it is'*', 1 if non-zero digit, 0 if'0'. -
Iterate over the string from index 1 to n-1:
-
Compute
singlefor decodings[i]as a single character: 9 if'*', 1 if digit'1'-'9', 0 if'0'. -
Compute
doublefor decodings[i-1]ands[i]as a two-digit number. Handle all combinations with'*'carefully:
- If
s[i-1]is'*'ands[i]is'*', valid two-digit numbers are11-19and21-26→ total 15 combinations. - If only one is
'*', count valid options based on the other digit. - If both are digits, check if they form a number between 10 and 26.
- Calculate
dp = (dp1 * single + dp2 * double) % MOD. - Update
dp2 = dp1,dp1 = dpfor the next iteration. - Return
dp1as the total number of ways to decode the string.
Why it works: This DP correctly counts all combinations of single- and double-digit decodings, handling '*' efficiently. The modulo ensures we do not overflow. By storing only the last two DP values, we reduce space complexity without losing correctness, because each dp[i] only depends on dp[i-1] and dp[i-2].
Python Solution
class Solution:
def numDecodings(self, s: str) -> int:
MOD = 10**9 + 7
n = len(s)
if not s:
return 0
dp2 = 1
dp1 = 9 if s[0] == '*' else 0 if s[0] == '0' else 1
for i in range(1, n):
single = 0
double = 0
if s[i] == '*':
single = 9
elif s[i] != '0':
single = 1
if s[i-1] == '*' and s[i] == '*':
double = 15 # 11-19 + 21-26
elif s[i-1] == '*':
double = 2 if '0' <= s[i] <= '6' else 1
elif s[i] == '*':
if s[i-1] == '1':
double = 9
elif s[i-1] == '2':
double = 6
else:
if s[i-1] != '0' and 10 <= int(s[i-1:i+1]) <= 26:
double = 1
dp = (dp1 * single + dp2 * double) % MOD
dp2, dp1 = dp1, dp
return dp1
The code initializes base cases for dp[0] and dp[1] and iterates through the string, calculating the number of ways for single- and double-character decodings. It handles '*' by explicitly counting all possible digit values that form valid letters.
Go Solution
func numDecodings(s string) int {
const MOD = 1_000_000_007
n := len(s)
if n == 0 {
return 0
}
dp2, dp1 := 1, 0
if s[0] == '*' {
dp1 = 9
} else if s[0] != '0' {
dp1 = 1
}
for i := 1; i < n; i++ {
single, double := 0, 0
if s[i] == '*' {
single = 9
} else if s[i] != '0' {
single = 1
}
if s[i-1] == '*' && s[i] == '*' {
double = 15
} else if s[i-1] == '*' {
if s[i] >= '0' && s[i] <= '6' {
double = 2
} else {
double = 1
}
} else if s[i] == '*' {
if s[i-1] == '1' {
double = 9
} else if s[i-1] == '2' {
double = 6
}
} else {
if s[i-1] != '0' {
twoDigit := (int(s[i-1]-'0')*10 + int(s[i]-'0'))
if twoDigit >= 10 && twoDigit <= 26 {
double = 1
}
}
}
dp := (dp1*single + dp2*double) % MOD
dp2, dp1 = dp1, dp
}
return dp1
}
The Go solution mirrors the Python implementation, with differences in syntax for integer conversion and modulo arithmetic. We use explicit casting to compute two-digit numbers and maintain only the last two DP values to reduce memory usage.
Worked Examples
Example: s = "1*"
| i | s[i] | single | double | dp |
|---|---|---|---|---|
| 0 | '1' | 1 | - | 1 |
| 1 | '*' | 9 | 9 | 18 |
Example: s = "2*"
| i | s[i] | single | double | dp |
|---|---|---|---|---|
| 0 | '2' | 1 | - | 1 |
| 1 | '*' | 9 | 6 | 15 |
Example: s = "*"
| i | s[i] | single | double | dp |
|---|---|---|---|---|
| 0 | '*' | 9 | - | 9 |
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) | Single pass through string with constant-time calculations per character |
| Space | O(1) | Only two DP values are maintained at any time |
The algorithm scales linearly with