LeetCode #91 Medium

Decode Ways

Count the ways to decode a digit string where 1-26 map to A-Z.

Constraints
  • 1 <= s.length <= 100
  • s contains only digits and may contain leading zero(s).
dynamic-programmingstring
Open on LeetCode ↗
02

Intuition

Decode ways counts how many ways a digit string maps back to letters, where A is 1 through Z is 26. The count grows like Fibonacci, and the reason is visible in the recurrence. At each position, a decoding either takes one digit or two. So the number of ways to decode the string up to position i is the ways up to i−1 (if the single digit is valid) plus the ways up to i−2 (if the two-digit pair is valid): - dp[i] = dp[i-1] + dp[i-2], with each term included only when that digit or pair actually decodes. That structure is Fibonacci with validity conditions, and those conditions are where the problem's difficulty lives. A zero cannot stand alone. '0' maps to no letter, so a single digit is valid only when it is 1 through 9. A string containing 0 decodes only if that zero pairs with the digit before it, as in 10 or 20. A pair must fall between 10 and 26. Both bounds matter — 27 exceeds Z, and 06 is not a valid two-digit code because leading zeros are not permitted. Checking only the upper bound wrongly accepts 06. The base case dp[0] = 1 represents the empty string having exactly one decoding, which makes dp[2] correct for valid two-digit inputs. A leading '0' makes the whole string undecodable, returning 0. Since only the previous two values are ever needed, the array collapses to two variables and O(1) space.

How to spot this pattern

Climbing Stairs with validity conditions on each step. From position i you may take one digit (if it isn't '0') or two (if they read 10–26). Zeros are the whole difficulty: '0' can never stand alone, so it only survives as the second half of a 10 or 20.

03

Approach

Try it first

Before reading on: price up what the direct approach costs here, then ask what a single cell should mean, and which earlier cells it needs. Aim for O(n) time and O(n), or O(1) with rolling variables space.

1

See the two choices per position

A decoding takes either one digit or two, so the ways to reach position i come from i-1 and i-2. This is Fibonacci with validity conditions attached.

2

Set the base cases

dp[0] = 1 — the empty string has exactly one decoding. This seeming technicality is what makes two-digit inputs come out right.

3

Validate the single digit

Add dp[i-1] only when the current digit is 1 through 9. A 0 cannot stand alone, since it maps to no letter.

4

Validate the two-digit pair

Add dp[i-2] only when the pair is between 10 and 26. Both bounds matter — 27 exceeds Z, and 06 is invalid because leading zeros are not permitted.

5

Return zero for a leading zero

A string starting with '0' has no valid decoding. Detecting this immediately avoids propagating zeros through the whole table.

6

Collapse to two variables

Only the previous two values are ever read, so the array reduces to two rolling variables — O(1) space with no change to the logic.

7

Cost of the scan

One pass with constant work per character gives O(n) time and O(1) space after the rolling-variable optimisation.

04

Solution & live demo

▶1class Solution:
▶2 def numDecodings(self, s:
▶3 str) -> int:
▶4 n = len(s)
▶5 dp = [0] * (n + 1)
▶6 dp[0] = 1
▶7 dp[1] = 1 if s[0] != '0' else 0
▶8 for i in range(2, n + 1):
▶9 one_digit = s[i - 1]
▶10 two_digit = int(s[i - 2:i])
▶11 if one_digit != '0':
▶12 dp[i] += dp[i - 1]
▶13 if 10 <= two_digit <= 26:
▶14 dp[i] += dp[i - 2]
▶15 return dp[n]
05

Common pitfalls

Allowing '0' as a single digit

✗ Wrong
dp[i] += dp[i - 1]
✓ Right
if one_digit != '0':
    dp[i] += dp[i - 1]

No letter maps to 0, so a standalone zero decodes to nothing and that path must contribute zero ways. Without the guard, strings like "100" report decodings that don't exist.

Accepting two-digit values below 10

✗ Wrong
if two_digit <= 26:
✓ Right
if 10 <= two_digit <= 26:

"06" is not a valid encoding of 6 — leading zeros aren't allowed. Testing only the upper bound admits every "0X" pair and inflates the count.

Seeding dp[1] unconditionally

✗ Wrong
dp[1] = 1
✓ Right
dp[1] = 1 if s[0] != '0' else 0

A string starting with '0' has no valid decoding at all, and that zero must propagate from the very first position. Seeding 1 lets an impossible prefix contribute ways to every later position.

06

Edge cases

String starts with '0'

dp[1] = 0 immediately, and that zero propagates forward, correctly producing 0 total decodings.

A '0' appears mid-string, e.g. '100'

The single digit check fails for '0', so only the pair (must be '10' or '20') can supply a decoding; anything else collapses to 0.

Pair exceeds 26, e.g. '27'

The two-digit branch is skipped since 27 > 26; only the single-digit branch (if valid) contributes.

Single character string

Loop never runs; the answer is just dp[1], which is 1 unless that character is '0'.

07

Complexity

Time
O(n)
Space
O(n), or O(1) with rolling variables
Single left-to-right pass over the string.