91. 解码方法
描述
一条包含字母 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 位 的整数。
题解
动态规划(Dynamic Programming, DP)
-
动态规划只能
应用于有最优 子结构
的问题。最优子结构的意思是局部最优解能决定全局最优解
(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。 -
简单地说,
问题能够分解成子问题来解决
。 -
通俗一点来讲,动态规划和其它遍历算法(如深/广度优先搜索)都是将
原问题拆成多个子问题然后求解
,他们之间最本质的区别是,动态规划保存子问题的解,避免重复计算
。 -
解决动态规划问题的关键是找到
状态转移方程
,这样我们可以通计算和储存子问题的解来求解最终问题
。 -
同时,我们也可以对动态规划进行
空间压缩
,起到节省空间消耗的效果。 -
在一些情况下,动态规划可以看成是
带有状态记录(memoization)的优先搜索
。 -
动态规划是自下而上的
,即先解决子问题,再解决父问题; -
而用带有
状态记录的优先搜索
是自上而下
的,即从父问题搜索到子问题,若重复搜索到同一个子问题则进行状态记录,防止重复计算。 -
如果题目需求的是最终状态,那么使用动态搜索比较方便;
-
如果题目需要输出所有的路径,那么使用带有状态记录的优先搜索会比较方便。
回到本题目
设 f[i] 表示字符串 s 的前 i 个字符解码方法数。在进行状态转移时 有下面的两种情况:
第一种情况是我们使用了一个字符,即 s[i] 进行解码,那么只要 s[i] !== '0', 它就可以被解码成A∼I 中的某个字母。由于剩余的前 i−1 个字符的解码方法数为 f_i = f_i-1
状态转移方程:
f_i = f_(i-1), 其中 s[i] !== '0'
第二种情况是我们使用了两个字符,即 s[i−1] 和 s[i] 进行编码。与第一种情况类似,s[i-1]s[i−1] 不能等于 '0',并且 s[i−1] 和 s[i] 组成的整数必须小于等于 26,这样它们就可以被解码成J∼Z 中的某个字母。由于剩余的前i−2 个字符的解码方法数为 f_(i-2) 状态转移方程:
f_i = f_(i-2), 其中 s[i-1] !== '0' 并且10 * s[i-1]+s[i] <= 26
需要注意的是,只有当i>1 时才能进行转移,否则 s[i−1] 不存在。
将上面的两种状态转移方程在对应的条件满足时进行累加得到最终结果;
coding
/**
* @param {string} s
* @return {number}
*/
var numDecodings = function(s) {
let n = s.length;
if(n === 0) return 0;
if(!(s[0] - '0')) return 0; // 前置0的干掉
if(n === 1) return 1;
const dp = new Array(n+1).fill(0);
dp[0] = 1;
for(let i = 1; i <= n; i ++) {
if (s[i - 1] !== '0') {
dp[i] += dp[i - 1];
}
if (i > 1) {
if(s[i - 2] != '0' && ((s[i - 2] - '0') * 10 + (s[i - 1] - '0') <= 26)) {
dp[i] += dp[i - 2];
}
if((s[i-2] - '0' > 2 || s[i-2] === '') && s[i-1] === '0') return 0; // 中间不符合字母的数字干掉
}
}
return dp[n];
};
复制代码