Problem overview
numDecodings receives a string s of digits and returns how many ways it can be split into groups that each read as a number from 1 to 26, where 1 is A, 2 is B and 26 is Z. A group can't start with 0: "06" is not "6", and no letter is 0. If there is no valid split, the answer is 0.
The string "12" has 2 decodings: 1 and 2 (AB), or 12 (L). The string "226" has 3: 2, 26 (BZ); 22, 6 (VF); and 2, 2, 6 (BBF). The string "06" has none, because the first group would have to start with 0.
Approach: count decodings of each beginning
Trying every split is exponential: each digit may start a new group or join the previous one. But the count for a long beginning of s can be built from the counts of shorter beginnings. Let dp(i) be the number of ways to decode the first i digits. Think of the last group of such a decoding. It is either:
- one digit, the digit at position
i - 1, which works when it isn't'0'. Every decoding of the firsti - 1digits can be followed by it, so it addsdp(i - 1); - two digits, the digits at positions
i - 2andi - 1, which works when they form a number from 10 to 26. Every decoding of the firsti - 2digits can be followed by it, so it addsdp(i - 2).
The two cases never produce the same decoding, because the last group has a different length, so dp(i) is their sum. The base is dp(0) = 1: the empty beginning has exactly one decoding, with no letters, and every longer decoding is built on it. The answer is dp(n). Each entry takes constant work, so the whole table costs O(n).
Here are the tables the solution builds:
"226" dp = [1, 1, 2, 3]
"11106" dp = [1, 1, 2, 3, 2, 2]
"100" dp = [1, 1, 1, 0]
"06" dp = [1, 0, 0]
For "226", dp(2) = 2 because the first two digits, 22, read as 2, 2 or as 22. Then dp(3) adds dp(2) for the single 6 and dp(1) for the pair 26, giving 2 + 1 = 3. For "11106" the last digit 6 adds dp(4) = 2, and the pair 06 adds nothing, since 06 is below 10. So dp(5) is 2, the two decodings from the problem statement.
Step by step
- Store the length:
val n = s.length. - Make the table.
val dp = Array.fill(n + 1)(0)createsn + 1zeros: one entry per beginning, plus the empty one at index 0. - Set
dp(0) = 1. The empty beginning has one decoding. - The first digit. If
s(0) != '0', setdp(1) = 1. A string starting with 0 keepsdp(1) = 0, and every count built on it stays 0 too. - Loop with
i <- 2 to n. The first two entries are known, each later one needs the two before it, andtoincludesn. For a one-digit string the range is empty and the loop doesn't run. - Single digit. If
s(i - 1) != '0', adddp(i - 1)todp(i)with+=. - Read the pair.
val two = s.substring(i - 2, i).toInttakes the characters ati - 2andi - 1as a number:"26"gives 26,"07"gives 7. - Valid pair. If
two >= 10 && two <= 26, adddp(i - 2)todp(i). A pair below 10 starts with 0, and one above 26 is no letter. - Return
dp(n)as the last expression.
The solution in Scala
class Solution {
def numDecodings(s: String): Int = {
val n = s.length
val dp = Array.fill(n + 1)(0)
dp(0) = 1
if (s(0) != '0') {
dp(1) = 1
}
for (i <- 2 to n) {
if (s(i - 1) != '0') {
dp(i) += dp(i - 1)
}
val two = s.substring(i - 2, i).toInt
if (two >= 10 && two <= 26) {
dp(i) += dp(i - 2)
}
}
dp(n)
}
}
Scala details worth knowing:
- Indexing. A string and an array are both indexed with parentheses,
s(i - 1)anddp(i). The string gives aChar, which is why it is compared with'0'in single quotes, not"0". Array.fill(n + 1)(0)builds an array ofn + 1elements, all 0. Arrays are mutable, sodp(i) += dp(i - 1)changes the element in place.substring(i - 2, i)includes the first index and excludes the second, so it holds exactly two characters..toIntparses them, and a leading zero is fine:"07".toIntis 7. That is why the check ontwouses the lower bound 10 instead of testing for'0'.- Integer size.
Intis 32 bits and overflow wraps silently. The lesson guarantees that the answer fits. - No
return. The last expression,dp(n), is the result of the method.
Complexity
Time: O(n). The loop runs n - 1 times and each round does a constant amount of work: one character check, one substring of two characters and at most two additions.
Space: O(n). The table has n + 1 entries. Because dp(i) depends only on the two entries before it, two variables could replace the table and bring the space down to O(1).
Edge cases and common mistakes
- A leading zero.
"06"gives 0:dp(1)stays 0, the 6 addsdp(1), which is 0, and the pair 06 is below 10. - A zero that can't be used.
"100"gives 0. The pair 10 is a valid letter, but the last 0 has nothing to join: 00 is no letter and a lone 0 is no letter. Likewise"30"gives 0, since 30 is above 26. - A zero that must join.
"10"gives 1, asJ. The 0 can't stand alone, so only the pair counts."110"also gives 1: the only way is 1 and 10. - Pairs above 26.
"27"gives 1, only 2 and 7, because 27 is no letter. - Forgetting
dp(0) = 1. With 0 there,dp(2)for"12"would miss the pair 12, and every answer would be too small. - Accepting pairs with a leading zero. Parsing
"06"gives 6, which looks like a valid letter. The checktwo >= 10is what rejects it. - A single digit.
"1"gives 1 and"0"gives 0; the loop doesn't run. The string is never empty, so readings(0)is safe.
Testing
Results from running the solution:
s | Result |
|---|---|
"12" | 2 |
"226" | 3 |
"06" | 0 |
"11106" | 2 |
"10" | 1 |
"100" | 0 |
"27" | 1 |
"30" | 0 |
"110" | 1 |
"1123" | 5 |
"1" | 1 |
