Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 159f8ff

Browse files
Merge pull request SharingSource#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ドル,ドル对应了方案 1ドルx$ 和 2ドルx,ドル此时有 $f[i] = f[i - 2] * 2$;
87+
* $s[i]$ 为数字 `7-9`,此时 $s[j]$ 可以代指 1ドル,ドル对应方案 1ドルx,ドル此时有 $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]$ 为上述所有的合法的分情况讨论的累加值,并对 1ドルe9+ 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)

AltStyle によって変換されたページ (->オリジナル) /