Solving tips
- This is Climbing Stairs with validity gates: ways(i) = ways(i+1) [if s[i]!='0'] + ways(i+2) [if s[i:i+2] in 10..26].
- The whole difficulty is zeros: a '0' is only valid as the second digit of 10 or 20, otherwise that suffix contributes 0 ways.
- Guard i+1 < n before reading a two-digit pair, and enforce both bounds 10 and 26 (so '01' and '27' are rejected).
- Seed the base case ways(n)=1 (empty suffix) and collapse to two rolling scalars for O(n) time, O(1) space.
Problem
A message of letters AβZ was encoded to digits using the mapping A β "1", B β "2", β¦, Z β "26". Given a string of digits s, count how many distinct original letter strings could have produced it.
To decode, s is split into groups, each group being either one digit (1β9) or two digits forming a value 10β26. A group with a leading zero (like "06") is invalid, and a lone "0" decodes to nothing.
Return the number of valid decodings (which may be 0).
Examples
s = "12" β 2 β "1 2" β "AB" or "12" β "L".
s = "226" β 3 β "2 2 6" (BBF), "22 6" (VF), "2 26" (BZ).
s = "06" β 0 β no group can start with 0, and "06" is not a valid two-digit code.
Constraints
1 <= s.length <= 100
s consists of digits only and may contain '0'.
Small length, so O(n) is trivial to hit; the difficulty is entirely in the zero-handling edge cases, not performance.
Think about it first
Hint 1
Scan the string and decide, at each position, whether the next code is one digit or two. The count of decodings for the rest of the string depends only on where you are β a classic 1-D DP over the index.
Hint 2
Let `ways(i)` be the number of ways to decode the suffix starting at index `i`. Take one digit (valid iff `s[i] != '0'`) and add `ways(i+1)`; take two digits (valid iff `s[i:i+2]` is between `"10"` and `"26"`) and add `ways(i+2)`. Base case `ways(n) = 1` (empty suffix, one way).
Hint 3
A `'0'` is decodable *only* as the second digit of `10` or `20`; anywhere else it forces `0` ways. Compute `ways` from the end backward, or forward with `dp[i]`; only the last two values matter, so O(1) space is possible.
TL;DR
1-D DP over the suffix index β add one-digit and two-digit continuations β collapsed to two rolling scalars: O(n) time, O(1) space.
The recurrence
Let n = len(s) and ways(i) = number of decodings of the suffix s[i:]:
ways(n) = 1 # empty suffix: exactly one decoding
ways(i) = one + two, where
one = ways(i+1) if s[i] != '0' else 0
two = ways(i+2) if i+1 < n and 10 <= int(s[i:i+2]) <= 26 else 0
answer = ways(0)
If both one and two are 0 (e.g. a stray '0'), that suffix contributes nothing and the total collapses to 0.
Approach 1 β Brute-force recursion
Branch on βtake one digitβ vs. βtake two digits,β guarding validity.
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
def ways(i: int) -> int:
if i == n:
return 1
if s[i] == "0":
return 0 # no valid single or leading-zero double
total = ways(i + 1)
if i + 1 < n and 10 <= int(s[i:i + 2]) <= 26:
total += ways(i + 2)
return total
return ways(0)
Complexity: O(2^n) time, O(n) stack.
Why the constraints kill it: at n = 100 a doubly-branching recursion is ~2^100 calls, recomputing each suffix count exponentially often.
Approach 2 β Top-down memoization
The insight: only n + 1 suffixes exist; cache ways(i).
from functools import cache
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
@cache
def ways(i: int) -> int:
if i == n:
return 1
if s[i] == "0":
return 0
total = ways(i + 1)
if i + 1 < n and 10 <= int(s[i:i + 2]) <= 26:
total += ways(i + 2)
return total
return ways(0)
Complexity: O(n) time, O(n) space (cache + stack).
Approach 3 β Bottom-up tabulation
The insight: ways(i) depends on ways(i+1) and ways(i+2), so fill the table from the end backward.
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
dp = [0] * (n + 2)
dp[n] = 1 # empty suffix
for i in range(n - 1, -1, -1):
if s[i] == "0":
dp[i] = 0
continue
dp[i] = dp[i + 1]
if i + 1 < n and 10 <= int(s[i:i + 2]) <= 26:
dp[i] += dp[i + 2]
return dp[0]
Walkthrough (s = "226", n = 3), filling right to left:
dp[3] = 1 (base)
i=2 ('6'): dp[2] = dp[3] = 1 ("26" two-digit needs index 1, not here) β 1
i=1 ('2'): dp[1] = dp[2] = 1; "26" is in 10..26 β add dp[3] = 1 β 2
i=0 ('2'): dp[0] = dp[1] = 2; "22" is in 10..26 β add dp[2] = 1 β 3
Return dp[0] = 3. β
Complexity: O(n) time, O(n) space.
Approach 4 β Space-optimized (two rolling scalars)
The insight: each cell reads only the next two, so keep ahead1 = dp[i+1] and ahead2 = dp[i+2].
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
ahead2, ahead1 = 0, 1 # dp[i+2], dp[i+1]; dp[n] = 1
for i in range(n - 1, -1, -1):
if s[i] == "0":
curr = 0
else:
curr = ahead1
if i + 1 < n and 10 <= int(s[i:i + 2]) <= 26:
curr += ahead2
ahead2, ahead1 = ahead1, curr
return ahead1
Walkthrough (s = "12"): start (ahead2, ahead1) = (0, 1). i=1 ('2'): curr = ahead1 = 1 β shift to (1, 1). i=0 ('1'): curr = ahead1 = 1; "12" in range β + ahead2 = 1 β 2. Return 2. β
Complexity: O(n) time, O(1) space.
Common pitfalls
- Treating
'0' as decodable alone. '0' has no letter; a suffix starting at a '0' that isnβt the tail of 10/20 has 0 decodings.
- Two-digit range errors. Valid two-digit codes are
10β26 only. "27", "30", "09" are all invalid as pairs; forgetting the lower bound 10 wrongly accepts "01".
- Bounds on the two-digit read. Guard
i + 1 < n before forming s[i:i+2]; at the last index there is no pair.
- Base case
dp[n] = 1. The empty suffix has exactly one decoding; seeding it to 0 zeroes out every count.
Pattern takeaway
Decode Ways is Climbing Stairs with validity gates: the same f(i) = f(i+1) + f(i+2) shape, but each term is admitted only if its 1- or 2-character group forms a legal code. The reusable rule for string DP: define the state as an index into the string, split on how many characters the next token consumes, and guard each branch with the tokenβs validity condition β then collapse the fixed-width look-ahead to rolling scalars.