91. Decode Ways - Explanation
Description
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 <= 100sconsists of digits
Topics
Recommended Time & Space Complexity
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.
Hint 1
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?
Hint 2
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?
Hint 3
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?
Hint 4
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.
Prerequisites
Before attempting this problem, you should be comfortable with:
- Recursion - Breaking down problems into smaller subproblems by making recursive function calls
- Dynamic Programming - Using memoization to cache results and avoid redundant computation
- String Manipulation - Parsing and extracting substrings, character comparisons
1. Recursion
Intuition
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:
- Take one digit (
s[i]) → valid if it’s not'0' - Take two digits (
s[i:i+2]) → valid if it forms a number between10and26
So 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:
- If you reach the end of the string → 1 valid decoding
- If a substring starts with
'0'→ 0 ways (invalid) - Otherwise, sum the ways from:
- decoding one digit
- decoding two digits (if valid)
Algorithm
- Define a recursive function
dfs(i)= number of ways to decodes[i:]. - Base cases:
- If
i == len(s), return1(successfully decoded entire string). - If
s[i] == '0', return0(invalid decoding).
- If
- Recursively:
- Always try decoding one digit →
dfs(i + 1) - If two digits form a valid number (
10-26), also try →dfs(i + 2)
- Always try decoding one digit →
- Return the sum of valid choices.
- Start recursion from index
0.
Time & Space Complexity
- Time complexity:
- Space complexity:
2. Dynamic Programming (Top-Down)
Intuition
This is the same decoding logic as the recursive approach, but with memoization to avoid recomputing the same subproblems.
Observation:
- While decoding, the same index
iis reached multiple times. - The number of ways to decode
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.
Algorithm
- Use a dictionary
dpwheredp[i]= number of ways to decodes[i:]. - Initialize base case:
dp[len(s)] = 1(empty string has one valid decoding).
- Define recursive function
dfs(i):- If
ialready indp, returndp[i]. - If
s[i] == '0', return0(invalid).
- If
- Compute:
- Take one digit →
dfs(i + 1) - Take two digits if valid (
10-26) →dfs(i + 2)
- Take one digit →
- Store result in
dp[i]and return it. - Call
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)Time & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up)
Intuition
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:
- Let
dp[i]= number of ways to decode the substrings[i:] - The answer we want is
dp[0] - Each position depends only on the next one or two positions → perfect for bottom-up DP
Algorithm
- Create a DP table (or map) where:
dp[i]= number of ways to decodes[i:]
- Base case:
dp[len(s)] = 1(empty string has one valid decoding)
- Iterate
ifrom right to left:- If
s[i] == '0'→dp[i] = 0(invalid start) - Else:
- Take one digit →
dp[i] = dp[i + 1] - Take two digits if valid (
10-26) → adddp[i + 2]
- Take one digit →
- If
- Return
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]Time & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (Space Optimized)
Intuition
This is the space-optimized version of bottom-up DP.
From the bottom-up approach, we know:
dp[i]depends only ondp[i+1]anddp[i+2]- So we don’t need an entire DP array
We just keep:
dp1→ ways to decode fromi + 1dp2→ ways to decode fromi + 2
At each index i, we compute the current answer using these two values, then shift them forward.
Algorithm
- Initialize:
dp1 = 1→ corresponds todp[len(s)]dp2 = 0
- Iterate from right to left:
- If
s[i] == '0'→ current ways =0 - Else:
- Start with
dp1(take one digit) - If two digits form a valid number (
10-26), adddp2
- Start with
- If
- After computing current:
- Shift values:
dp2 = dp1dp1 = current
- Shift values:
- Return
dp1
class 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 dp1Time & Space Complexity
- Time complexity:
- Space complexity:
Common Pitfalls
Not Handling Leading Zeros
A 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)Incorrect Two-Digit Validation
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)Confusing Base Case Value
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
Sign in to join the discussion