小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
介绍
今天做了一道题,觉得这个思想还挺有价值的,故写下这篇文记录一下。
题目是LC91,是一道很基础的题。
解
递归
看到这个题,一下能想到的方法就是递归搜索了。 因为这个题解码时只有两种情况,在某个状态截取1个字符或截取两个字符,所以递归做起来也不会很复杂。
class Solution {
public int numDecodings(String s) {
// 递归解法(超时)
return dfs(s.toCharArray(), s.length(), 0);
}
public int dfs(char[] chars, int len, int index) {
// 找到了一条解码方式
if (index >= len) return 1;
// '0'没有解码方式(10或20可以在下面截取2个字符时截取得到,所以不用担心这样漏解)
if (chars[index] == '0') return 0;
// 截取1个字符时
int res = dfs(chars, len, index + 1);
// 截取2个字符时
if (index < len - 1 && ((chars[index] - '0') * 10 + (chars[index+1] - '0') <= 26))
res += dfs(chars, len, index + 2);
return res;
}
}
当然,这个方法毫无疑问会超时,但是这无疑是我们往后优化所必不可少的第一步。
记忆化递归
抓耳挠腮了半天,甚至动笔模拟了很久,会发现这个递归的方法存在很多不必要的重复计算。 在计算一个字符串编码方式时,这个普通的递归方法会多次重复计算它的子编码方式(即它的子字符串的编码方式)
如果不懂的话,看一下代码比较容易理解
class Solution {
public int numDecodings(String s) {
// 记忆化递归
// 使用一个数组来存储计算结果就行了
// 只需要存储s中从index开始到最后有多少种解码方式就行了
int[] record = new int[s.length()];
Arrays.fill(record, -1);
return dfs(s.toCharArray(), s.length(), 0, record);
}
public int dfs(char[] chars, int len, int index, int[] record) {
// 找到了一条解码方式
if (index >= len) return 1;
// '0'没有解码方式(10或20可以在下面截取2个字符时截取得到,所以不用担心这样漏解)
if (chars[index] == '0') return 0;
// 如果已经计算过了,直接返回计算结果
if (record[index] != -1) return record[index];
// 截取1个字符时
int res = dfs(chars, len, index + 1, record);
// 截取2个字符时
if (index < len - 1 && ((chars[index] - '0') * 10 + (chars[index+1] - '0') <= 26))
res += dfs(chars, len, index + 2, record);
record[index] = res; // 记录下index下有多少种解码方式
return res;
}
}
到这里,其实已经优化的很好了,但是还可以使用动态规划解决。
DP
DP的方法其实就是把记忆化递归的方式写成迭代的方式。 在记忆化递归中,记录数组record是在s中从后往前记录的,当然DP也可以从后往前写,但是从前往后比较符合我们人类的思维,所以我这里从前往后使用迭代的方式来做。
如果是一步步做下来的,其实可以很轻松地发现状态转移方程。
dp[i] = dp[i-1] + dp[i-2]
只是在处理边界条件和判断进入哪种状态时需要稍微注意一下。
class Solution {
public int numDecodings(String s) {
// dp解法
// 比s长度大1,是为了添加一个哨兵
int[] dp = new int[s.length() + 1];
dp[0] = 1; // 哨兵,防止第一个字符就是'0'
for (int i = 1; i <= s.length(); i++) {
// 截取一个字符时
// 只要不是'0'都可以接受
if (s.charAt(i-1) != '0')
dp[i] = dp[i-1];
// 截取两个字符时
// 除了要小于26以外,使用我这种方法判断的话还要避免第一位是0
if (i >= 2 && s.charAt(i-2) != '0'
&& ((s.charAt(i-2) - '0') * 10 + (s.charAt(i-1) - '0') <= 26))
dp[i] += dp[i-2];
}
return dp[s.length()];
}
}
是不是觉得dp的方式更加优雅简洁呢。
其实还有更加节省空间的方式,就是把dp数组换成只使用两个变量。因为这里在计算第i个位置的dp时只用到了i-1与i-2两个位置,我这里就不再写下去啦。
总结
其实DP和记忆化递归是一样的,只是采用了不同的写法。但是从头直接用动态规划的思想做的话可能会有点困难,所以想不出解法的时候不妨先从最普通的递归开始,一步一步进行优化。
扩展
有什么相关题目值得一做的我会补充在这里。