91. 解码方法https://leetcode.cn/problems/decode-ways/description/一条包含字母A-Z的消息进行以下映射进行了编码:'A' -> "1",'B' -> "2",……,'Z' -> "26"。要解码已编码的消息,所有数字必须基于以上映射的方法,反向映射回字母(可能有多种方法)。例如:"11106"可以映射为:"AAJF",将消息分组为(1 1 10 6);"KJF",将消息分组为(11 10 6)。注意:消息不能分组为(1 11 06),因为"06"不能映射为'F',这是因为"6"和"06"在映射中并不等价。给你一个只含数字的非空字符串s,请计算并返回解码方法的总数。题目数据保证答案肯定是一个32位的整数。
- 输入:s = "12",输出:2,解释:它可以解码为"AB"(1 2)或者"L"(12)。
- 输入:s = "226",输出:3,解释:它可以解码为"BZ"(2 26),"VF"(22 6)或者"BBF"(2 2 6)。
- 输入:s = "06",输出:0,解释:"06"无法映射到"F",因为存在前导零("6"和"06"并不等价)。
提示:1 <= s.length <= 100;s只包含数字,并且可能包含前导零。
我们用动态规划的思想来解决这个问题。
确定状态表示:根据经验和题目要求,我们用dp[i]表示字符串中[0, i]区间上,一共有多少种解码方法。
推导状态转移方程:这里我们要根据字符串中i位置和i-1位置的情况分类讨论。
- 如果我们把字符串中i位置的数字单独解码,解码为1个字母。
- 如果字符串中i位置的数字在[1, 9]的范围内,则解码成功,解码方法数和字符串中[0, i - 1]区间上的解码方法数相同,即dp[i - 1]。
- 如果字符串中i位置的数字是0,则解码失败,前面的解码作废,前功尽弃,解码方法数为0。
- 如果我们把字符串中i - 1位置和i位置的2个数字结合起来,解码为1个字母.
- 如果字符串中i - 1位置和i位置的2个数字结合起来的数字在[10, 26]的范围内,则解码成功,解码方法数和字符串中[0, i - 2]区间上的解码方法数相同,即dp[i - 2]。
- 否则,解码失败,前面的解码作废,前功尽弃,解码方法数为0。
综上所述,dp[i]默认是0。若字符串中i位置的数字解码成功,dp[i] += dp[i - 1];若字符串中i - 1位置和i位置的2个数字结合起来解码成功,dp[i] += dp[i - 2]。
初始化:根据状态转移方程,dp[0]和dp[1]是会越界的。我们在dp表的最左边加上一个辅助结点。辅助结点的特点有:
- 下标的映射关系:由于dp表的其他位置整体向右移动了1个单位,所以dp表的i位置对应字符串的i - 1位置。
- 辅助结点里面的值要保证后续填表是正确的。
如何保证后续填表是正确的呢?
- dp[1]的值要根据字符串中0位置的数字是否能解码来决定。如果字符串中0位置的数字是0,那么就不能解码,dp[1] = 0;否则dp[1] = 1。
- dp[2]的值如果要满足状态转移方程,关键在于状态转移方程的后半句话:若字符串中0位置和1位置的2个数字结合起来解码成功,dp[2] += dp[0],而此时dp[2] += 1是最合适的,从而倒推出dp[0] = 1。
综上所述,dp[0] = 1,dp[1] = s[0] == '0' ? 0 : 1。
填表顺序:根据状态转移方程,dp[i]依赖于dp[i - 1]和dp[i - 2],所以应从左往右填表。
返回值:根据状态表示,应返回dp[n],对应字符串中[0, n - 1]区间上的解码方法数,注意此时dp表的n位置对应字符串的n - 1位置。
细节问题:由于下标范围是[0, n],dp表的规模应该为1 x (n + 1)。
class Solution {
public:
int numDecodings(string s) {
int n = s.size();
// 创建dp表
vector<int> dp(n + 1);
// 初始化
dp[0] = 1;
dp[1] = s[0] == '0' ? 0 : 1;
// 填表
for (int i = 2; i <= n; i++) {
// 最后一个位置解码
dp[i] += s[i - 1] == '0' ? 0 : dp[i - 1];
// 最后两个位置结合起来解码
int num = (s[i - 2] - '0') * 10 + s[i - 1] - '0';
dp[i] += num >= 10 && num <= 26 ? dp[i - 2] : 0;
}
// 返回结果
return dp[n];
}
};