Count how many ways a digit string can be decoded back into letters (A=1 … Z=26) using a compact 1-D DP table — each position either extends a 1-digit mapping, a 2-digit mapping, or both, with leading-zero traps at every step.
A phone keypad maps letters to digits: A=1, B=2, …, Z=26. Given a non-empty string of digits, count the number of ways to decode it back into letters.
Example: s = "226". Possible decodings:
"2" "2" "6" → BBF"22" "6" → VF"2" "26" → BZAnswer: 3. Edge case: s = "06" → 0 (leading zero, impossible).
i, look back at the last one character (1-digit code) and the last two characters (2-digit code). If either window is a valid letter code, add the number of ways from before that window. dp[i] = (valid1 ? dp[i-1] : 0) + (valid2 ? dp[i-2] : 0). The recurrence is local: you only ever need the previous two values.dp[0] = 1(empty string decodes one way — it's the "nothing consumed yet" seed). s[i-1] ≠ '0' (i.e. '1'–'9'), add dp[i-1] — every decoding of the prefix can be extended by this single letter.i ≥ 2 and 10 ≤ s[i-2..i) ≤ 26, add dp[i-2] — every decoding ending two positions back can be extended by this pair as one letter.i — a dead end (usually caused by a stray '0').Space reduction: because dp[i] only depends on dp[i-1] and dp[i-2], you can replace the array with two rolling variables (prev2, prev1) for O(1) space — identical to the Fibonacci rolling trick.
'*' digit that counts for 1–9, requiring careful multiplication of branch counts.226. We fill dp[0..3] where dp[i] = ways to decode s[0..i). Base: dp[0] = 1 (empty string decodes one way).1▶function numDecodings(s: string): number {2▶ const n = s.length;3 // dp[i] = number of ways to decode s[0..i)4▶ const dp: number[] = new Array(n + 1).fill(0);5▶ dp[0] = 1; // base: empty string has exactly one decoding67 for (let i = 1; i <= n; i++) {8 // 1-digit: s[i-1] maps to a letter iff it is '1'–'9'9 if (s[i - 1] !== '0') {10 dp[i] += dp[i - 1];11 }12 // 2-digit: s[i-2..i) maps to a letter iff the value is 10–2613 if (i >= 2) {14 const two = parseInt(s.slice(i - 2, i), 10);15 if (two >= 10 && two <= 26) {16 dp[i] += dp[i - 2];17 }18 }19 }2021 return dp[n];22}
function numDecodings(s: string): number {
const n = s.length;
// dp[i] = number of ways to decode s[0..i)
const dp: number[] = new Array(n + 1).fill(0);
dp[0] = 1; // base: empty string has exactly one decoding
for (let i = 1; i <= n; i++) {
// 1-digit: s[i-1] maps to a letter iff it is '1'–'9'
if (s[i - 1] !== '0') {
dp[i] += dp[i - 1];
}
// 2-digit: s[i-2..i) maps to a letter iff the value is 10–26
if (i >= 2) {
const two = parseInt(s.slice(i - 2, i), 10);
if (two >= 10 && two <= 26) {
dp[i] += dp[i - 2];
}
}
}
return dp[n];
}dp is length n+1 (1-indexed so dp[i] covers the first i characters). dp[0] = 1is the "empty prefix" seed — without it, no valid decoding can ever accumulate.s[i-1] is the character at the current position (0-indexed). Any digit except '0' is a valid single-letter code. If valid, propagate dp[i-1] forward.i ≥ 2. parseInt(s.slice(i-2, i), 10) reads the pair as a number. The bounds 10–26capture exactly the letters J–Z; "07" parses to 7 (below 10) and "27" is above 26, so both are rejected. If valid, add dp[i-2].dp[n] counts ways to decode the entire string. A value of 0 means no valid decoding exists (e.g. a bare '0' not covered by any 2-digit window).n positions, O(1) work each. O(n) space for the dp array; this collapses to O(1) with two rolling variables since only the previous two values are ever read.prev2 and prev1 (Fibonacci rolling pattern). Update them at each step: prev2 = prev1; prev1 = cur.'*' stands for any digit 1–9 (nine choices). For the 1-digit branch multiply by 9; for the 2-digit branch carefully count how many pairs 10–26 it can form with the adjacent digit."0" → 0 (no valid code). "10" → 1 (only "10"=J). "30"→ 0 ("30" > 26, and "0" alone is invalid). "1" → 1.dp array: at each position, if dp[i] has a 1-digit contribution, step back by 1; if a 2-digit contribution exists, step back by 2 (prefer one arbitrarily or enumerate all).function numDecodings(s: string): number {
const n = s.length;
// dp[i] = number of ways to decode s[0..i)
const dp: number[] = new Array(n + 1).fill(0);
dp[0] = 1; // base: empty string has exactly one decoding
for (let i = 1; i <= n; i++) {
// 1-digit: s[i-1] maps to a letter iff it is '1'–'9'
if (s[i - 1] !== '0') {
dp[i] += dp[i - 1];
}
// 2-digit: s[i-2..i) maps to a letter iff the value is 10–26
if (i >= 2) {
const two = parseInt(s.slice(i - 2, i), 10);
if (two >= 10 && two <= 26) {
dp[i] += dp[i - 2];
}
}
}
return dp[n];
}n positions, O(1) work each. O(n) space for the dp array; this collapses to O(1) with two rolling variables since only the previous two values are ever read.From a position i, branch on consuming one digit (valid if it isn't '0') and on consuming two digits (valid if they form 10–26), summing the ways from each branch. Reaching the end counts as one decoding.
function numDecodings(s: string): number {
const n = s.length;
function ways(i: number): number {
if (i === n) return 1; // consumed whole string → one valid decoding
if (s[i] === '0') return 0; // a leading 0 can't start any letter
let total = ways(i + 1); // take one digit (1–9)
if (i + 1 < n) { // take two digits, if 10–26
const two = parseInt(s.slice(i, i + 2), 10);
if (two <= 26) total += ways(i + 2);
}
return total;
}
return ways(0);
}ways(i) repeatedly. Memoizing on i — or filling the bottom-up dp array above — collapses it to O(n).| "how many ways to decode / interpret a digit string" | 1-D DP: dp[i] += dp[i-1] (1-digit) + dp[i-2] (2-digit) |
| each position depends on the previous 1–2 positions only | rolling-variable O(1) space (Fibonacci pattern) |
| "0" in the input / leading zero in a window | guard: skip 1-digit if s[i-1]==="0"; skip 2-digit if value < 10 or > 26 |
| "count ways" + strings + bounded alphabet | bottom-up DP with small alphabet or digit constraints |
function numDecodings(s: string): number {
const n = s.length;
const dp = new Array(n + 1).fill(0);
dp[0] = 1;
for (let i = 1; i <= n; i++) {
if (s[i - 1] !== '0') dp[i] += dp[i - 1]; // 1-digit
if (i >= 2) {
const two = parseInt(s.slice(i - 2, i), 10);
if (two >= 10 && two <= 26) dp[i] += dp[i - 2]; // 2-digit
}
}
return dp[n];
}s[0..i) (the first i characters). dp[0] = 1 is the empty-prefix seed.numDecodings("226")?numDecodings("06")?i ≥ 2?