Problem overview
A message of capital letters was turned into digits: A is 1, B is 2, and so on up to Z, which is 26. To decode a digit string, you split it into groups, each a number from 1 to 26, and replace each group with its letter. A group can't start with 0: "06" is not 6, and no letter is 0. numDecodings(s) returns the number of different ways to decode s, or 0 when there is none.
The string "12" has 2 decodings: AB (1, 2) and L (12). The string "226" has 3: BZ (2, 26), VF (22, 6) and BBF (2, 2, 6). The string "06" has none, because the first group would start with 0.
Approach
Look at the last group of a decoding of the first i digits. It is either the single digit s[i - 1] or the two digits s[i - 2] and s[i - 1]. That gives a recurrence over prefixes of the string. Let dp[i] be the number of ways to decode the first i digits. Then:
- If the last digit is not
0, it can be a letter on its own. Every decoding of the firsti - 1digits extends with it, so adddp[i - 1]. - If the last two digits form a number from 10 to 26, they can be one letter. Every decoding of the first
i - 2digits extends with it, so adddp[i - 2]. A pair below 10, such as07, starts with 0 and is not a letter, and a pair above 26 is not a letter either.
The two cases are independent, and both can apply, which is why the code adds to dp[i] instead of choosing. Without the table, plain recursion would solve the same prefixes again and again and take exponential time. The table solves each prefix once.
The base case is dp[0] = 1: the empty beginning has exactly one decoding, the one with no letters. Every longer decoding is built on top of it, so it must count as one way, not zero.
Step by step
- Length. Store
s.lengthinval n. - The table. Create
val dp = IntArray(n + 1), one more entry than digits, becausedp[0]stands for "no digits yet". All entries start at 0. - Empty prefix. Set
dp[0] = 1. - First digit. If
s[0]isn't'0', setdp[1] = 1. A string starting with 0 keepsdp[1] = 0, and everything built on it stays 0 too. - Fill the rest with a loop over
2..n. Bothdp[0]anddp[1]are known, and each later entry needs the two before it. - Single digit. If
s[i - 1]isn't'0', dodp[i] += dp[i - 1]. - Pair. Take
s.substring(i - 2, i).toInt()intoval two, and iftwois between 10 and 26, dodp[i] += dp[i - 2]. - Answer. Return
dp[n].
The solution in Kotlin
class Solution {
fun numDecodings(s: String): Int {
val n = s.length
val dp = IntArray(n + 1)
dp[0] = 1
if (s[0] != '0') {
dp[1] = 1
}
for (i in 2..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]
}
}
return dp[n]
}
}
Kotlin details worth knowing
'0'is aChar.s[0]gives aChar, which is compared with the character'0'in single quotes. The comparisons[0] != '0'is about the character, not the number zero.substring(i - 2, i). The start index is included and the end index is excluded, so it holds the characters at positionsi - 2andi - 1.toInt()then parses it:"26".toInt()is 26, and"07".toInt()is 7, which fails the testtwo >= 10.2..nis inclusive. The loop reachesi == n, the whole string. Forn == 1the range2..1is empty, so a one-digit string skips the loop, anddp[1]is already set.IntArray(n + 1)creates an array ofn + 1zeros.valonly keeps the name tied to the same array; its entries can still change, sodp[i] += ...is fine.Intoverflow.Intis 32 bits and wraps silently. The lesson guarantees the answer fits, so it is safe here. For a string of 45 ones the result is 1836311903, but 46 ones would need more than anIntholds and the same code returns -1323752223.- An empty string is not allowed.
s[0]throws for an empty string. The lesson's constraints promise at least one digit.
Complexity
- Time: O(n). The loop runs about n times, and each round does a few comparisons, one
substringof two characters and at most two additions. - Space: O(n) for the
dptable. Each entry needs only the two before it, so two variables could replace the array, but the table is clearer for a first solution.
Edge cases and common mistakes
- Leading zero.
"06"gives 0. The first digit is0, sodp[1]stays 0, and the pair06is below 10, so nothing is added ati = 2. - A zero in the middle.
"100"gives 0: the last0can't stand alone, and00is not a pair in range."10"gives 1: only the group 10 works, and"30"gives 0 because 30 is too big and 0 can't stand alone. - Pairs above 26.
"27"gives 1, because only 2 and 7 work as separate letters. - Treating a pair like
"06"as 6. Taking the number alone,"06".toInt()is 6, which seems fine. The checktwo >= 10is what rejects it. - Forgetting
dp[0] = 1. Setting it to 0 makes every answer 0, since all counts are built from it. - Using
=instead of+=. Both cases can apply at once, and the second one would erase the first.
Testing
The table shows the dp array the solution builds, which makes each result easy to follow:
s | dp | Result |
|---|---|---|
"12" | [1, 1, 2] | 2 |
"226" | [1, 1, 2, 3] | 3 |
"06" | [1, 0, 0] | 0 |
"11106" | [1, 1, 2, 3, 2, 2] | 2 |
"10" | [1, 1, 1] | 1 |
"100" | [1, 1, 1, 0] | 0 |
"27" | [1, 1, 1] | 1 |
"2101" | [1, 1, 2, 1, 1] | 1 |
"6" | [1, 1] | 1 |
"30" | [1, 1, 0] | 0 |
