|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[639. 解码方法 II](https://leetcode-cn.com/problems/decode-ways-ii/solution/gong-shui-san-xie-fen-qing-kuang-tao-lun-902h/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「线性 DP」、「枚举」 |
| 6 | + |
| 7 | +一条包含字母 A-Z 的消息通过以下的方式进行了编码: |
| 8 | +``` |
| 9 | +'A' -> 1 |
| 10 | +'B' -> 2 |
| 11 | +... |
| 12 | +'Z' -> 26 |
| 13 | +``` |
| 14 | +要 解码 一条已编码的消息,所有的数字都必须分组,然后按原来的编码方案反向映射回字母(可能存在多种方式)。例如,"11106" 可以映射为: |
| 15 | +* "AAJF" 对应分组 (1 1 10 6) |
| 16 | +* "KJF" 对应分组 (11 10 6) |
| 17 | + |
| 18 | +注意,像 (1 11 06) 这样的分组是无效的,因为 "06" 不可以映射为 'F' ,因为 "6" 与 "06" 不同。 |
| 19 | + |
| 20 | +除了 上面描述的数字字母映射方案,编码消息中可能包含 `'*'` 字符,可以表示从 '1' 到 '9' 的任一数字(不包括 '0')。例如,编码字符串 `"1*"` 可以表示 "11"、"12"、"13"、"14"、"15"、"16"、"17"、"18" 或 "19" 中的任意一条消息。对 "1*" 进行解码,相当于解码该字符串可以表示的任何编码消息。 |
| 21 | + |
| 22 | +给你一个字符串 s ,由数字和 '*' 字符组成,返回 解码 该字符串的方法 数目 。 |
| 23 | + |
| 24 | +由于答案数目可能非常大,返回对 $10^9 + 7$ 取余 的结果。 |
| 25 | + |
| 26 | +示例 1: |
| 27 | +``` |
| 28 | +输入:s = "*" |
| 29 | +
|
| 30 | +输出:9 |
| 31 | +
|
| 32 | +解释:这一条编码消息可以表示 "1"、"2"、"3"、"4"、"5"、"6"、"7"、"8" 或 "9" 中的任意一条。 |
| 33 | +可以分别解码成字符串 "A"、"B"、"C"、"D"、"E"、"F"、"G"、"H" 和 "I" 。 |
| 34 | +因此,"*" 总共有 9 种解码方法。 |
| 35 | +``` |
| 36 | +示例 2: |
| 37 | +``` |
| 38 | +输入:s = "1*" |
| 39 | +
|
| 40 | +输出:18 |
| 41 | +
|
| 42 | +解释:这一条编码消息可以表示 "11"、"12"、"13"、"14"、"15"、"16"、"17"、"18" 或 "19" 中的任意一条。 |
| 43 | +每种消息都可以由 2 种方法解码(例如,"11" 可以解码成 "AA" 或 "K")。 |
| 44 | +因此,"1*" 共有 9 * 2 = 18 种解码方法。 |
| 45 | +``` |
| 46 | +示例 3: |
| 47 | +``` |
| 48 | +输入:s = "2*" |
| 49 | +
|
| 50 | +输出:15 |
| 51 | +
|
| 52 | +解释:这一条编码消息可以表示 "21"、"22"、"23"、"24"、"25"、"26"、"27"、"28" 或 "29" 中的任意一条。 |
| 53 | +"21"、"22"、"23"、"24"、"25" 和 "26" 由 2 种解码方法,但 "27"、"28" 和 "29" 仅有 1 种解码方法。 |
| 54 | +因此,"2*" 共有 (6 * 2) + (3 * 1) = 12 + 3 = 15 种解码方法。 |
| 55 | +``` |
| 56 | + |
| 57 | +提示: |
| 58 | +* $1$ <= s.length <= $10^5$ |
| 59 | +* s[i] 是 `0-9` 中的一位数字或字符 '*' |
| 60 | + |
| 61 | +--- |
| 62 | + |
| 63 | +### 分情况讨论 DP |
| 64 | + |
| 65 | +这是一道普通的线性 DP 题(之所以说普通,是因为状态定义比较容易想到),也是 [(题解)91. 解码方法](https://leetcode-cn.com/problems/decode-ways/solution/gong-shui-san-xie-gen-ju-shu-ju-fan-wei-ug3dd/) 的进阶题。 |
| 66 | + |
| 67 | +我们称一个解码内容为一个 `item`。 |
| 68 | + |
| 69 | +**定义 $f[i]$ 为考虑以 $s[i]$ 为结尾的字符串,共有多少种解码方案。** |
| 70 | + |
| 71 | +那么最终答案为 $f[n - 1]$,同时我们有显而易见的起始状态 `f[0] = s[0] == '*' ? 9 : (s[0] != '0' ? 1 : 0)`. |
| 72 | + |
| 73 | +不失一般性考虑 $f[i]$ 该如何转移,$s[i]$ 要么是 `*`,要么是数字,对应一个分情况讨论过程: |
| 74 | + |
| 75 | +* 当 $s[i]$ 为 `*`:此时考虑 $s[i]$ 是单独作为一个 `item`,还是与上一个字符共同作为一个 `item`: |
| 76 | + * $s[i]$ 单独作为一个 `item`:由于 `*` 可以代指数字 `1-9`,因此有 $f[i] = f[i - 1] * 9$; |
| 77 | + * $s[i]$ 与上一个字符共同作为一个 `item`:此时需要对上一个字符 $s[j]$ 进行讨论: |
| 78 | + * $s[j]$ 为数字 `1`:此时 $s[i]$ 可以代指 `1-9`,对应了 `item` 为 `11-19` 这 $9$ 种情况,此时有 $f[i] = f[i - 2] * 9$(**如果 $f[i - 2]$ 取不到,则使用 $1$ 代指,下面同理**); |
| 79 | + * $s[j]$ 为数字 `2`:此时 $s[i]$ 可以代指 `1-6`,对应了 `item` 为 `21-26` 这 $6$ 种情况,此时有 $f[i] = f[i - 2] * 6$; |
| 80 | + * $s[j]$ 为字符 `*`:此时两个 `*` 对应了合法方案为 $11-19$ 和 $21-26$ 共 $15$ 种方案,此时有 $f[i] = f[i - 2] * 15$; |
| 81 | + |
| 82 | +* 当 $s[i]$ 为数字:此时可以从「前一字符 $s[j]$ 为何种字符」和「当前 $s[i]$ 是否为 $0$」出发进行讨论: |
| 83 | + * $s[j]$ 为字符 `*`,根据当前 $s[i]$ 是否为 $0$ 讨论: |
| 84 | + * $s[i]$ 为数字 $0$:此时 $s[i]$ 无法独自作为一个 `item`,只能与 $s[j]$ 组合,对应了 $10$ 和 $20$ 两种情况,此时有 $f[i] = f[i - 2] * 2$; |
| 85 | + * $s[i]$ 为数字 `1-9`,此时首先有 $s[i]$ 可以作为一个独立 `item` 的情况,即有 $f[i] = f[i - 1]$,然后对 $s[i]$ 的数值大小进一步分情况讨论: |
| 86 | + * $s[i]$ 为数字 `1-6`,此时 $s[j]$ 可以代指 $1$ 和 $2$,对应了方案 $1x$ 和 $2x$,此时有 $f[i] = f[i - 2] * 2$; |
| 87 | + * $s[i]$ 为数字 `7-9`,此时 $s[j]$ 可以代指 $1$,对应方案 $1x$,此时有 $f[i] = f[i - 2]$; |
| 88 | + * $s[j]$ 为数字类型,此时从「当前 $s[i]$ 是否为 $0$」出发进行讨论: |
| 89 | + * $s[i]$ 为数字 $0$:此时 $s[j]$ 只有为 $1$ 和 $2$ 时,才是合法方案,则有 $f[i] = f[i - 2]$; |
| 90 | + * $s[i]$ 为数字 `1-9`:此时首先有 $s[i]$ 可以作为一个独立 `item` 的情况,即有 $f[i] = f[i - 1]$,然后再考虑能够与 $s[j]$ 组成合法 `item` 的情况: |
| 91 | + * $s[j]$ 为数值 $1$:此时有 $f[i] = f[i - 2]$; |
| 92 | + * $s[j]$ 为数值 $2$,且 $s[i]$ 为数值 `1-6`:此时有 $f[i] = f[i - 2]$。 |
| 93 | + |
| 94 | +由于是求方案数,因此最终的 $f[i]$ 为上述所有的合法的分情况讨论的累加值,并对 $1e9+ 7$ 取模。 |
| 95 | + |
| 96 | +> 一些细节:实现上了避免大量对 $f[i - 2]$ 是否可以取得的讨论,我们可以对 `s` 前追加一个空格作为哨兵(无须真正插入),以简化代码,同时由于 $f[i]$ 只依赖于 $f[i - 1]$ 和 $f[i - 1]$,可以使用「滚动数组」的形式进行空间空间优化(见 $P2$)。 |
| 97 | +
|
| 98 | +> 另外,对于「滚动数组」的空间优化方式,还需要说明两点:转移前先使用变量保存 `(i-1)%3` 和 `(i-2)%3` 的计算结果,防止大量的重复计算;不能再偷懒使用 `toCharArray`,只能使用 `charAt`,因为 Java 为了遵循字符串不变的原则,会在调用 `toCharArray` 时返回新数组,这样复杂度就还是 $O(n)$ 的。 |
| 99 | +诸如此类的「滚动数组」优化方式,最早在 [这里](https://mp.weixin.qq.com/s?__biz=MzU4NDE3MTEyMA==&mid=2247485638&idx=1&sn=d7468955c5b121664031fd5c1b5a6f10&chksm=fd9ca3d9caeb2acf2dd4e9f512b2d4dc820107489a094749ffe7cae646f0cc60de06f2d18898&scene=178&cur_album_id=1751702161341628417#rd) 讲过。 |
| 100 | + |
| 101 | +代码: |
| 102 | +```Java |
| 103 | +class Solution { |
| 104 | + int mod = (int)1e9+7; |
| 105 | + public int numDecodings(String s) { |
| 106 | + char[] cs = s.toCharArray(); |
| 107 | + int n = cs.length; |
| 108 | + long[] f = new long[n]; |
| 109 | + f[0] = cs[0] == '*' ? 9 : (cs[0] != '0' ? 1 : 0); |
| 110 | + for (int i = 1; i < n; i++) { |
| 111 | + char c = cs[i], prev = cs[i - 1]; |
| 112 | + if (c == '*') { |
| 113 | + // cs[i] 单独作为一个 item |
| 114 | + f[i] += f[i - 1] * 9; |
| 115 | + // cs[i] 与前一个字符共同作为一个 item |
| 116 | + if (prev == '*') { |
| 117 | + // 11 - 19 & 21 - 26 |
| 118 | + f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 15; |
| 119 | + } else { |
| 120 | + int u = (int)(prev - '0'); |
| 121 | + if (u == 1) { |
| 122 | + f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 9; |
| 123 | + } else if (u == 2) { |
| 124 | + f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 6; |
| 125 | + } |
| 126 | + } |
| 127 | + } else { |
| 128 | + int t = (int)(c - '0'); |
| 129 | + if (prev == '*') { |
| 130 | + if (t == 0) { |
| 131 | + f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 2; |
| 132 | + } else { |
| 133 | + // cs[i] 单独作为一个 item |
| 134 | + f[i] += f[i - 1]; |
| 135 | + // cs[i] 与前一个字符共同作为一个 item |
| 136 | + if (t <= 6) { |
| 137 | + f[i] += (i - 2 >= 0 ? f[i - 2] : 1) * 2; |
| 138 | + } else { |
| 139 | + f[i] += i - 2 >= 0 ? f[i - 2] : 1; |
| 140 | + } |
| 141 | + } |
| 142 | + } else { |
| 143 | + int u = (int)(prev - '0'); |
| 144 | + if (t == 0) { |
| 145 | + if (u == 1 || u == 2) { |
| 146 | + f[i] += i - 2 >= 0 ? f[i - 2] : 1; |
| 147 | + } |
| 148 | + } else { |
| 149 | + // cs[i] 单独作为一个 item |
| 150 | + f[i] += (f[i - 1]); |
| 151 | + // cs[i] 与前一个字符共同作为一个 item |
| 152 | + if (u == 1) { |
| 153 | + f[i] += i - 2 >= 0 ? f[i - 2] : 1; |
| 154 | + } else if (u == 2 && t <= 6) { |
| 155 | + f[i] += i - 2 >= 0 ? f[i - 2] : 1; |
| 156 | + } |
| 157 | + } |
| 158 | + } |
| 159 | + } |
| 160 | + f[i] %= mod; |
| 161 | + } |
| 162 | + return (int)(f[n - 1]); |
| 163 | + } |
| 164 | +} |
| 165 | +``` |
| 166 | + |
| 167 | +```Java |
| 168 | +class Solution { |
| 169 | + int mod = (int)1e9+7; |
| 170 | + public int numDecodings(String s) { |
| 171 | + int n = s.length() + 1; |
| 172 | + long[] f = new long[3]; |
| 173 | + f[0] = 1; |
| 174 | + f[1] = s.charAt(0) == '*' ? 9 : (s.charAt(0) != '0' ? 1 : 0); |
| 175 | + for (int i = 2; i < n; i++) { |
| 176 | + char c = s.charAt(i - 1), prev = s.charAt(i - 2); |
| 177 | + int p1 = (i - 1) % 3, p2 = (i - 2) % 3; |
| 178 | + long cnt = 0; |
| 179 | + if (c == '*') { |
| 180 | + // cs[i] 单独作为一个 item |
| 181 | + cnt += f[p1] * 9; |
| 182 | + // cs[i] 与前一个字符共同作为一个 item |
| 183 | + if (prev == '*') { |
| 184 | + cnt += f[p2] * 15; |
| 185 | + } else { |
| 186 | + int u = (int)(prev - '0'); |
| 187 | + if (u == 1) cnt += f[p2] * 9; |
| 188 | + else if (u == 2) cnt += f[p2] * 6; |
| 189 | + } |
| 190 | + } else { |
| 191 | + int t = (int)(c - '0'); |
| 192 | + if (prev == '*') { |
| 193 | + if (t == 0) { |
| 194 | + cnt += f[p2]* 2; |
| 195 | + } else { |
| 196 | + // cs[i] 单独作为一个 item |
| 197 | + cnt += f[p1]; |
| 198 | + // cs[i] 与前一个字符共同作为一个 item |
| 199 | + if (t <= 6) cnt += f[p2] * 2; |
| 200 | + else cnt += f[p2]; |
| 201 | + } |
| 202 | + } else { |
| 203 | + int u = (int)(prev - '0'); |
| 204 | + if (t == 0) { |
| 205 | + if (u == 1 || u == 2) cnt += f[p2]; |
| 206 | + } else { |
| 207 | + // cs[i] 单独作为一个 item |
| 208 | + cnt += f[p1]; |
| 209 | + // cs[i] 与前一个字符共同作为一个 item |
| 210 | + if (u == 1) cnt += f[p2]; |
| 211 | + else if (u == 2 && t <= 6) cnt += f[p2]; |
| 212 | + } |
| 213 | + } |
| 214 | + } |
| 215 | + f[i % 3] = cnt % mod; |
| 216 | + } |
| 217 | + return (int)(f[(n - 1) % 3]); |
| 218 | + } |
| 219 | +} |
| 220 | +``` |
| 221 | +* 时间复杂度:$O(n)$ |
| 222 | +* 空间复杂度:使用「滚动数组」进行优化,复杂度为 $O(1)$,否则为 $O(n)$ |
| 223 | + |
| 224 | + |
| 225 | +--- |
| 226 | + |
| 227 | +### 枚举 DP |
| 228 | + |
| 229 | +上述解法之所以复杂,是因为不仅仅要对当前字符 $s[i]$ 分情况讨论,还需要对上一个字符 $s[j]$ 分情况讨论。 |
| 230 | + |
| 231 | +事实上,我们可以利用解码对象只有 `A-Z` 来进行枚举。 |
| 232 | + |
| 233 | +在从前往后处理字符串 `s` 时,枚举 $s[i]$ 参与构成的解码内容 `item` 是字母 `A-Z` 中哪一个,从而将分情况讨论转变成对应位的字符对比。 |
| 234 | + |
| 235 | +代码: |
| 236 | +```Java |
| 237 | +class Solution { |
| 238 | + int mod = (int)1e9+7; |
| 239 | + public int numDecodings(String s) { |
| 240 | + int n = s.length(); |
| 241 | + long[] f = new long[3]; |
| 242 | + f[0] = 1; |
| 243 | + for (int i = 1; i <= n; i++) { |
| 244 | + char c = s.charAt(i - 1); |
| 245 | + int t = c - '0'; |
| 246 | + long cnt = 0; |
| 247 | + int p1 = (i - 1) % 3, p2 = (i - 2) % 3; |
| 248 | + // 枚举组成什么 item(A -> 1; B -> 2 ...) |
| 249 | + for (int item = 1; item <= 26; item++) { |
| 250 | + if (item < 10) { // 该 item 由一个字符组成 |
| 251 | + if (c == '*' || t == item) cnt += f[p1]; |
| 252 | + } else { // 该 item 由两个字符组成 |
| 253 | + if (i - 2 < 0) break; |
| 254 | + char prev = s.charAt(i - 2); |
| 255 | + int u = prev - '0'; |
| 256 | + int a = item / 10, b = item % 10; |
| 257 | + if ((prev == '*' || u == a) && (t == b || (c == '*' && b != 0))) cnt += f[p2]; |
| 258 | + } |
| 259 | + } |
| 260 | + f[i % 3] = cnt % mod; |
| 261 | + } |
| 262 | + return (int)(f[n % 3]); |
| 263 | + } |
| 264 | +} |
| 265 | +``` |
| 266 | +* 时间复杂度:$O(n * C)$,其中 $C$ 为解码内容字符集大小,固定为 $26$ |
| 267 | +* 空间复杂度:$O(1)$ |
| 268 | + |
| 269 | +--- |
| 270 | + |
| 271 | +### 最后 |
| 272 | + |
| 273 | +这是我们「刷穿 LeetCode」系列文章的第 `No.639` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 274 | + |
| 275 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 276 | + |
| 277 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode。 |
| 278 | + |
| 279 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 280 | + |
0 commit comments