A string consisting of uppercase english characters can be encoded to a number using the following mapping:
'A' -> "1"
'B' -> "2"
...
'Z' -> "26"To decode a message, digits must be grouped and then mapped back into letters using the reverse of the mapping above. There may be multiple ways to decode a message. For example, "1012" can be mapped into:
"JAB" with the grouping (10 1 2)"JL" with the grouping (10 12)The grouping (1 01 2) is invalid because 01 cannot be mapped into a letter since it contains a leading zero.
Given a string s containing only digits, return the number of ways to decode it. You can assume that the answer fits in a 32-bit integer.
Example 1:
Input: s = "12"
Output: 2
Explanation: "12" could be decoded as "AB" (1 2) or "L" (12).Example 2:
Input: s = "01"
Output: 0Explanation: "01" cannot be decoded because "01" cannot be mapped into a letter.
Constraints:
1 <= s.length <= 100s consists of digits
You should aim for a solution as good or better than O(n) time and O(n) space, where n is the length of the given string.
The characters A through Z are mapped to the numbers from 1 to 26. A mapped number can have at most 2 digits. In the given string of digits, we can explore all possible decodings by combining one or two consecutive digits. Think of this in terms of a decision tree and explore all paths. Can you derive a recurrence relation for this?
Iterate over the string with index i. At each index, we have two choices: decode the current digit as a character with its mapped value, or combine the current digit with the next digit to form a two-digit value. The recurrence relation can be expressed as dfs(i + 1) + dfs(i + 2) where dfs is the recursive function. Also, consider edge cases, as not every two-digit number or a number with a leading zero is valid. How would you implement this?
A brute-force recursive solution would result in O(2^n) time complexity. Can you think of a better way? Perhaps you should consider the repeated work of calling the recursion multiple times with the same parameter values and find a way to avoid this. Also, can you think about the base condition of this recursive function?
The base condition is to return 1 if i goes out of bounds. If the current digit is '0', return 0, as no character maps to '0', making the string invalid. Use memoization to avoid repeated work by caching recursion results in an array or hash map and returning the stored value when the result is already calculated.
Before attempting this problem, you should be comfortable with:
Each digit (or pair of digits) in the string can be mapped to a letter:
"1" → "A", "2" → "B", …, "26" → "Z"At any index i, you only have two possible decoding choices:
s[i]) → valid if it’s not '0's[i:i+2]) → valid if it forms a number between 10 and 26So the problem naturally breaks into subproblems:
“How many ways can I decode the substring starting at index
i?”
This leads directly to a recursive structure.
Key base ideas:
'0' → 0 ways (invalid)dfs(i) = number of ways to decode s[i:].i == len(s), return 1 (successfully decoded entire string).s[i] == '0', return 0 (invalid decoding).dfs(i + 1)10-26), also try → dfs(i + 2)0.This is the same decoding logic as the recursive approach, but with memoization to avoid recomputing the same subproblems.
Observation:
i is reached multiple times.s[i:] never changes, so we can store it once and reuse it.So instead of recalculating:
“How many ways can I decode from index
i?”
we cache the answer the first time we compute it.
This converts the exponential recursion into linear time.
dp where dp[i] = number of ways to decode s[i:].dp[len(s)] = 1 (empty string has one valid decoding).dfs(i):i already in dp, return dp[i].s[i] == '0', return 0 (invalid).dfs(i + 1)10-26) → dfs(i + 2)dp[i] and return it.dfs(0).class Solution:
def numDecodings(self, s: str) -> int:
dp = {len(s) : 1}
def dfs(i):
if i in dp:
return dp[i]
if s[i] == "0":
return 0
res = dfs(i + 1)
if i + 1 < len(s) and (
s[i] == "1" or s[i] == "2" and
s[i + 1] in "0123456"
):
res += dfs(i + 2)
dp[i] = res
return res
return dfs(0)This is the iterative version of the decoding logic.
Instead of asking:
“How many ways can I decode starting at index
i?”
recursively, we build the answer from the back.
Key idea:
dp[i] = number of ways to decode the substring s[i:]dp[0]dp[i] = number of ways to decode s[i:]dp[len(s)] = 1 (empty string has one valid decoding)i from right to left:s[i] == '0' → dp[i] = 0 (invalid start)dp[i] = dp[i + 1]10-26) → add dp[i + 2]dp[0]class Solution:
def numDecodings(self, s: str) -> int:
dp = {len(s): 1}
for i in range(len(s) - 1, -1, -1):
if s[i] == "0":
dp[i] = 0
else:
dp[i] = dp[i + 1]
if i + 1 < len(s) and (s[i] == "1" or
s[i] == "2" and s[i + 1] in "0123456"
):
dp[i] += dp[i + 2]
return dp[0]This is the space-optimized version of bottom-up DP.
From the bottom-up approach, we know:
dp[i] depends only on dp[i+1] and dp[i+2]We just keep:
dp1 → ways to decode from i + 1dp2 → ways to decode from i + 2At each index i, we compute the current answer using these two values, then shift them forward.
dp1 = 1 → corresponds to dp[len(s)]dp2 = 0s[i] == '0' → current ways = 0dp1 (take one digit)10-26), add dp2dp2 = dp1dp1 = currentdp1class Solution:
def numDecodings(self, s: str) -> int:
dp = dp2 = 0
dp1 = 1
for i in range(len(s) - 1, -1, -1):
if s[i] == "0":
dp = 0
else:
dp = dp1
if i + 1 < len(s) and (s[i] == "1" or
s[i] == "2" and s[i + 1] in "0123456"
):
dp += dp2
dp, dp1, dp2 = 0, dp, dp1
return dp1A string starting with '0' or containing '0' that cannot pair with the previous digit has zero valid decodings. This is the most common edge case to miss.
# Wrong: doesn't handle '0' case
res = dfs(i + 1)
# Correct: return 0 for invalid '0'
if s[i] == '0':
return 0
res = dfs(i + 1)Two digits form a valid decoding only if they represent 10-26. Common mistakes include allowing "00", "07", or values above 26.
# Wrong: allows invalid two-digit codes like "00" or "30"
if i + 1 < len(s):
res += dfs(i + 2)
# Correct: only allow 10-26
if i + 1 < len(s) and (s[i] == '1' or (s[i] == '2' and s[i + 1] <= '6')):
res += dfs(i + 2)When the entire string is decoded (index reaches the end), there is exactly 1 valid decoding (the one that got us here). Returning 0 instead causes all paths to count as zero.
# Wrong: returns 0, making all counts zero
if i == len(s):
return 0
# Correct: reaching the end means one valid decoding
if i == len(s):
return 1