Decode Ways
Table of Contents + β
Decode Ways is a favorite dynamic programming question. Dynamic programming is a way to solve a big problem by breaking it into small pieces, solving each piece once, and saving the answer. This problem looks like simple counting. But the edge cases with zeros trip up many people. So it tests if you can be careful and still keep the solution fast.
π― The Problem
A message is hidden as digits. Count how many ways the string of digits can be decoded back into letters.
The rules:
- The mapping is
Ais1,Bis2, all the way toZis26. - One digit can form a letter. So
2could beB. - Two digits can form a letter. So
26could beZ. - A single
0is never a letter. So a two-digit code cannot start with zero. Watch the zeros. - Count every valid split.
For the string "226", you can read it as 2 2 6 which is BBF. Or 22 6 which is VF. Or 2 26 which is BZ. So there are 3 ways.
Input: s = "226"Output: 3
Explanation: "226" can be decoded as "BBF" (2 2 6), "VF" (22 6), or "BZ" (2 26).This diagram shows the splits for "226". Each branch takes one digit or two digits.
π’ Approach 1: Plain Recursion (Brute Force)
The idea in one line: from each position, try a one-digit cut and a two-digit cut, then add the counts.
The idea:
- Start at the front of the string.
- Take one digit if it is not zero, then solve the rest.
- Take two digits if they form a number from
10to26, then solve the rest. - Add the counts from both choices.
How it works:
- A function from a position returns the ways to decode the rest.
- It calls itself for the position after one digit and after two digits.
- When it reaches the end of the string, that is one valid way, so it returns
1.
Why it is weak:
- The same position gets asked the same question over and over.
- These repeated questions are called overlapping subproblems, the same small case many times.
- The branching doubles, so it is about O(2βΏ). Too slow for a long string.
Here is the plain recursion code:
def num_decodings(s): def dfs(i): if i == len(s): return 1 if s[i] == "0": return 0 count = dfs(i + 1) if i + 1 < len(s) and int(s[i:i + 2]) <= 26: count += dfs(i + 2) return count
return dfs(0)π§ Approach 2: Memoization (Better)
The idea in one line: remember the ways for each position so you never recompute it.
The idea:
- Keep an array
memowherememo[i]is the ways to decode the string starting at positioni. - The first time you solve a position, store it.
How it works:
- Run the same recursion as before.
- Before computing a position, check the cache.
- The next time the position appears, read it back.
Why it is fast:
- Each position is solved once.
- This memoization drops the time to O(n).
Here is the memoized recursion:
from functools import lru_cache
def num_decodings(s): @lru_cache(None) def dfs(i): if i == len(s): return 1 if s[i] == "0": return 0 count = dfs(i + 1) if i + 1 < len(s) and int(s[i:i + 2]) <= 26: count += dfs(i + 2) return count
return dfs(0)π Approach 3: Bottom-Up Tabulation (Better)
The idea in one line: fill a table from the smallest prefix toward the full string.
The idea:
- Make an array
dpof sizen+1. dp[i]is the ways to decode the firsticharacters.- Set
dp[0]to1, the empty string with one way.
How it works:
- Set
dp[1]based on whether the first digit is zero. - For each later position, add the one-digit way if the digit is not zero.
- Add the two-digit way if those two digits form
10to26.
Why it is fine:
- Each cell is filled once.
- The time is O(n) with no recursion. This tabulation uses one table.
This diagram shows the table filling for "226". Each cell adds the valid one-digit and two-digit moves.
β‘ Approach 4: Space-Optimized (Best)
The idea in one line: the table only needs the last two counts, so keep two numbers.
The idea:
- The table only ever uses the two cells before the current one.
- So drop the full array and track
prev2andprev1.
How it works:
- At each character, compute a new count from the valid one-digit and two-digit moves.
- Then slide
prev2 = prev1andprev1 = new. - The final
prev1is the answer.
Why it is best:
- It keeps O(n) time.
- The memory drops to O(1) since only two counts are alive.
Steps to Solve
- If the string is empty or starts with
0, there are zero ways. - Set
prev2 = 1for the empty prefix andprev1 = 1for the first valid digit. - Walk from the second character to the end.
- If the current digit is not
0, addprev1to the new count, since one digit decodes. - If the two digits from the previous character form
10to26, addprev2, since two digits decode. - Slide
prev2 = prev1andprev1 = current. The answer isprev1.
This Python version keeps only two counts, so it uses O(1) extra memory.
def num_decodings(s): n = len(s) if n == 0 or s[0] == "0": return 0 # empty or leading zero, no way prev2 = 1 # ways for the empty prefix prev1 = 1 # ways for the first valid digit for i in range(1, n): current = 0 if s[i] != "0": current += prev1 # one-digit decode two = int(s[i - 1]) * 10 + int(s[i]) if 10 <= two <= 26: current += prev2 # two-digit decode prev2 = prev1 # slide forward prev1 = current return prev1
s = "226"print(num_decodings(s))The output of the above code will be:
3Let us walk through the Python version line by line and see why each piece is there.
def num_decodings(s): n = len(s) if n == 0 or s[0] == "0": return 0 prev2 = 1 prev1 = 1 for i in range(1, n): current = 0 if s[i] != "0": current += prev1 two = int(s[i - 1]) * 10 + int(s[i]) if 10 <= two <= 26: current += prev2 prev2 = prev1 prev1 = current return prev1The check if n == 0 or s[0] == "0" stops bad input early. An empty string has nothing to decode. A leading zero cannot start any letter. So both return zero ways.
The lines prev2 = 1 and prev1 = 1 set the base. Here prev2 is the count of ways for the empty prefix, which is one way: decode nothing. And prev1 is the count after the first digit, which we already checked is valid, so it is one way too.
The loop starts at i = 1 because we handled the first digit. We reset current = 0 for each new character. The check if s[i] != "0" asks if this single digit is a letter on its own. A zero alone is not, so we skip it. If it is fine, we add prev1, the ways up to the digit before.
The line two = int(s[i - 1]) * 10 + int(s[i]) builds the two-digit number from the previous and current digit. The check if 10 <= two <= 26 asks if those two digits form a real letter. We use 10 as the floor because 00 to 09 are not valid two-digit codes. If valid, we add prev2, the ways from two digits back.
The lines prev2 = prev1 and prev1 = current slide the window forward. The final prev1 is the count for the whole string. Trace "226". After 2 we have prev1 = 1. At 2 again, single 2 adds 1 and 22 adds 1, so current = 2. At 6, single 6 adds 2 and 26 adds 1, so current = 3. The answer is 3.
β±οΈ Time and Space Complexity
Plain recursion repeats work, so it grows to O(2βΏ). Memoization and tabulation touch each position once, so they run in O(n). The space-optimized version also runs in O(n) time but keeps just two counts, so its memory is O(1).
| Approach | Time Complexity | Space Complexity |
|---|---|---|
| Brute force recursion | O(2βΏ) | O(n) |
| Memoization (top-down) | O(n) | O(n) |
| Tabulation (bottom-up) | O(n) | O(n) |
| Space-optimized | O(n) | O(1) |
Tip
The zeros are where people lose points. Say it clearly in the interview. A single zero is never a letter. And a two-digit code must be between 10 and 26. Handle those two rules and the rest is just adding two counts.
π§© Key Takeaways
- β At each spot you can take one digit or two digits, so the count is the sum of both valid choices.
- β
A single
0is never a letter, and a two-digit code only counts if it is between10and26. - β Plain recursion repeats overlapping subproblems, so it is O(2βΏ). Saving answers makes it O(n).
- β
Tabulation fills a
dptable wheredp[i]is the ways to decode the firsticharacters. - β Each count needs only the two before it, so two variables give O(1) space.
Check Your Knowledge
Test what you learned. Pick an answer for each question, then click Check.
- 1
In Decode Ways, how many digits can map to one letter?
Why: A letter is one digit from 1 to 9 or two digits from 10 to 26.
- 2
Why does a leading zero make the answer zero?
Why: No letter maps to a code beginning with 0, so a leading zero has no valid decoding.
- 3
When can we add the two-digit decoding count?
Why: Two digits decode to a letter only when they form a value between 10 and 26.
- 4
What makes the space-optimized version O(1) in space?
Why: Each new count only needs the two counts before it, so two variables suffice.