Skip to content

Commit 159f8ff

Browse files
Merge pull request #156 from SharingSource/ac_oier
✨feat: Add 639
2 parents 6905704 + d9d6958 commit 159f8ff

File tree

2 files changed

+281
-0
lines changed

2 files changed

+281
-0
lines changed

Index/线性 DP.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
| [338. 比特位计数](https://leetcode-cn.com/problems/counting-bits/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/counting-bits/solution/po-su-jie-fa-dong-tai-gui-hua-jie-fa-by-vvail/) | 简单 | 🤩🤩🤩 |
1111
| [403. 青蛙过河](https://leetcode-cn.com/problems/frog-jump/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/frog-jump/solution/gong-shui-san-xie-yi-ti-duo-jie-jiang-di-74fw/) | 困难 | 🤩🤩🤩 |
1212
| [576. 出界的路径数](https://leetcode-cn.com/problems/out-of-boundary-paths/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/out-of-boundary-paths/solution/gong-shui-san-xie-yi-ti-shuang-jie-ji-yi-asrz/) | 中等 | 🤩🤩🤩🤩 |
13+
| [639. 解码方法 II](https://leetcode-cn.com/problems/decode-ways-ii/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/decode-ways-ii/solution/gong-shui-san-xie-fen-qing-kuang-tao-lun-902h/) | 困难 | 🤩🤩🤩🤩 |
1314
| [650. 只有两个键的键盘](https://leetcode-cn.com/problems/2-keys-keyboard/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/2-keys-keyboard/solution/gong-shui-san-xie-yi-ti-san-jie-dong-tai-f035/) | 中等 | 🤩🤩🤩🤩 |
1415
| [678. 有效的括号字符串](https://leetcode-cn.com/problems/valid-parenthesis-string/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/valid-parenthesis-string/solution/gong-shui-san-xie-yi-ti-shuang-jie-dong-801rq/) | 中等 | 🤩🤩🤩🤩🤩 |
1516
| [1137. 第 N 个泰波那契数](https://leetcode-cn.com/problems/n-th-tribonacci-number/) | [LeetCode 题解链接](https://leetcode-cn.com/problems/n-th-tribonacci-number/solution/gong-shui-san-xie-yi-ti-si-jie-die-dai-d-m1ie/) | 简单 | 🤩🤩🤩🤩 |
Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
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

Comments
 (0)