Problem overview
num_decodings receives a string s of digits and returns how many ways it can be read as letters, where A is 1, B is 2, and so on up to Z as 26. A decoding splits the digits into groups, each a number from 1 to 26. A group can't start with 0, so "06" is not the letter for 6, and no group is just "0". If no decoding exists, the answer is 0.
The string "12" has 2 decodings: 1 and 2 (AB), or 12 (L). The string "226" has 3: 2, 26; 22, 6; and 2, 2, 6. The string "06" has none, since the first group would have to start with 0.
Approach: count the decodings of every beginning
Let dp[i] be the number of ways to decode the first i digits. The last letter of such a decoding is made of either the last digit alone or the last two digits, so:
- if digit
i(the last of the firsti) isn't 0, it can be a letter by itself, and every decoding of the firsti - 1digits extends with it: adddp[i - 1]; - if the last two digits form a number from 10 to 26, they can be one letter, and every decoding of the first
i - 2digits extends with it: adddp[i - 2].
The two cases are different groupings, so no decoding is counted twice. A pair such as 07 is below 10, so it is not a letter; 27 and above are too big. Each entry needs only the two before it, so one pass is enough. Trying every grouping recursively would repeat work on the same suffixes; the table remembers it.
The table needs a starting point. The empty beginning, dp[0], counts as 1 way: no letters. Every longer decoding is built on it. And dp[1] is 1 if the first digit isn't 0, and 0 otherwise.
Step by step
- Take the bytes with
s.as_bytes(). A RustStringcan't be indexed by position, and each digit is one byte, sobytes[i]is the digit at positioni, comparable withb'0', the byte of the character0. - Store
bytes.len()inn. It is ausize, the type of indexes. - Create
dpasvec![0i32; n + 1]: one more entry than digits, becausedp[0]stands for the empty beginning. The suffixi32makes the counts match the return type. - Set
dp[0] = 1. With 0 here, every count built on it would stay 0. - Set
dp[1] = 1whenbytes[0]isn'tb'0'. A string starting with 0 can't be decoded, sodp[1]stays 0, and so does every count that builds on it. - Loop
iover2..=n. The first two entries are known, and the loop ends withdp[n]. For a one-digit string the range is empty. - Single-digit case. If
bytes[i - 1]isn'tb'0', adddp[i - 1]todp[i]. - Compute the last two digits as a number. A digit byte minus
b'0'is the digit's value, sotwois(bytes[i - 2] - b'0') * 10 + (bytes[i - 1] - b'0'). It is au8, and at most 99, so it fits. - Two-digit case. If
twois between 10 and 26, adddp[i - 2]todp[i]. - Return
dp[n]with no semicolon.
The solution in Rust
impl Solution {
fn num_decodings(s: String) -> i32 {
let bytes = s.as_bytes();
let n = bytes.len();
let mut dp = vec![0i32; n + 1];
dp[0] = 1;
if bytes[0] != b'0' {
dp[1] = 1;
}
for i in 2..=n {
if bytes[i - 1] != b'0' {
dp[i] += dp[i - 1];
}
let two = (bytes[i - 2] - b'0') * 10 + (bytes[i - 1] - b'0');
if two >= 10 && two <= 26 {
dp[i] += dp[i - 2];
}
}
dp[n]
}
}
The table for a few inputs, printed from the same logic, shows how the counts build up:
226 dp = [1, 1, 2, 3]
11106 dp = [1, 1, 2, 3, 2, 2]
06 dp = [1, 0, 0]
2101 dp = [1, 1, 2, 1, 1]
For 226, dp[2] is 2 (the groups 2, 2 and 22), and dp[3] adds the single 6 after both of those (dp[2]) and the pair 26 after the single 2 (dp[1]), giving 3. For 11106 the entry dp[4] drops to 2: the 0 can't stand alone, so only the pair 10 counts, and dp[2] is its source.
Rust details worth a closer look:
- Bytes instead of characters.
s.as_bytes()gives a&[u8], a view of the string's bytes with no copy. That is correct here because the input is digits only, and each digit is a single byte. Letters outside ASCII would take several bytes, but they don't occur in this problem. - Byte arithmetic.
b'7' - b'0'is theu8value 7, since digit characters are consecutive. The arithmetic stays inu8, which is enough for values up to 99. usizeindexes.i - 1andi - 2are safe because the loop starts at 2.- Indexing a one-digit string.
dp[1]exists for everynof at least 1, becausedphasn + 1entries. i32counts. The lesson says the answer fits in 32 bits. In a build with optimizations, as the lessons use, ani32that overflows wraps silently instead of stopping: a string of 100 ones, whose true count is larger thani32::MAX, gives-424332979. Inside the constraints that doesn't happen.
Complexity
Time: O(n). Each of the n entries takes a constant amount of work.
Space: O(n). The table has n + 1 entries. Since each entry looks back only two places, two variables would be enough for O(1) space, but the table makes the idea easy to follow.
Edge cases and common mistakes
- Leading zero.
"0"and"06"give 0. - A zero in the middle. A 0 must be part of a 10 or a 20.
"10"gives 1 and"100"gives 0, because the last 0 follows a 0."30"also gives 0: 30 is above 26 and the 0 can't stand alone."1001"gives 0 too. - 27 and above.
"27"gives 1, only 2 and 7, since 27 is not a letter. - Pairs below 10. A pair like
07is not the letter 7. That's why the check istwo >= 10, not justtwo <= 26. - A zero that is rescued.
"2101"gives 1: the only valid split is 2, 10, 1. - A wrong base case. With
dp[0] = 0, nothing would ever count. Withdp[1] = 1set unconditionally, strings starting with 0 would get a wrong count. - One digit.
"1"gives 1, and the loop doesn't run. - Empty input. The constraints say the string has at least one digit. Reading
bytes[0]of an empty string would panic.
Testing
"12"gives 2."226"gives 3."06"gives 0."11106"gives 2."10"gives 1."100"gives 0."27"gives 1."30"gives 0."2101"gives 1."1001"gives 0."1"gives 1.
