1 动态规划简介
动态规划主要用来解决最优化的问题,所以看到求解“最**”的问题时可以先往动态规划考虑下是否可行。
使用动态规划解决最优化的问题的两个要素:
第一:具有最优子结构,即问题的最优解由相关子问题的最优解组合而成,且这些个子问题可以独立求解。所谓的独立求解就是子问题之间是相互独立的,反例见《算法导论3rd-p218》。
第二:子问题重叠,即可用备忘录优化穷举过程。
1.1 最优子结构
如果一个问题的最优解包含其子问题的最优解,就称此问题具有最优子结构。可用子问题的最优解来构造原问题的最优解。
1.2 重叠子问题
子问题的空间应该是足够的小,即问题的递归算法会反复求解“相同”的子问题,而不是一直生成“新”的子问题。
如果递归算法反复求解相同的子问题,就称最优化的问题具有重叠子问题的性质。
针对重叠子问题就可以使用备忘录进行剪枝操作,防止对相同子问题的重复求解。
2 动态规划的两种实现方法
动态规划主要有两种实现方式:
2.1 从上到下的实现方式
此种方式是将大问题逐步的转化为小问题,并且在这个转化过程中会出现一些对小问题的重复求解,这个时候就是用备忘录进行剪枝操作,从而缩短执行时间。
此种方式的实现就是靠“递归”来完成的,需要仔细考虑递归的出口。
2.2 从下到上的实现方式
此种方式是先求解小问题,然后由已知的小问题的解来构建大问题的解,也就是说在求解大问题的解之前,小问题的解已经被提前计算了出来。此种实现方式也就不会出现子问题的重复求解的过程,所以这个动态规划的实现方式,可以将备忘录优化掉,更换成几个变量来保存上一步的结果即可。
此种实现方式还有些不同的地方就是需要找到一个base case,其实也就是最小的那个子问题的解。
2.3 两种方法的一些对比
2.3.1 备忘录
两种方法都需要定义备忘录(从下到上可以优化掉备忘录,但是理解这个优化过程还需要引入备忘录),所以就一个非常重要的点就是“如何明确这个备忘录所表示的含义”。
例如:LeetCode-1143-字符串-子序列-最长公共子序列_hclbeloved的博客-CSDN博客
中dp[i][j]的定义。
2.3.2 实现方式
从上到下使用的是递归,从下到上使用的是迭代。
在做题的过程你会发现,如果使用的是从上到下的动态规划求解时,你也可以使用dfs进行解决,dfs本质上其实就是回溯,但是它的效率还不如动态规划高。
注意:从上到下的动态规划使用的是递归,而dfs常常使用的也是递归。
这里给出一个例子:字符串的交织 力扣
方法一:使用dfs,具体代码如下:
bool isInterleave(string s1, string s2, string s3) {
// 方法一:递归,对字符串“从前到后”进行扫描,超出时间限制
// 为了缩短时间,可将方法一中的“对字符串从前向后扫描的递归”改为“对字符串从后向前扫描的递归”,
// 并配合备忘录的剪枝操作,形成从上到下的动态规划,具体参考方法二
return dfs(s1,s2,s3,0,0,0);
}
bool dfs(const string& s1, const string& s2, const string& s3, int index1, int index2, int index3)
{
if ((s1.length()-index1)+(s2.length()-index2) != (s3.length()-index3))
return false;
if (index1 == s1.length())
return s2.substr(index2) == s3.substr(index3);
else if (index2 == s2.length())
return s1.substr(index1) == s3.substr(index3);
if (s1[index1] != s3[index3] && s2[index2] != s3[index3])
return false;
if (s1[index1] == s3[index3])
{
if (s2[index2] != s3[index3])
{
return dfs(s1,s2,s3,index1+1,index2,index3+1);
}
return (dfs(s1,s2,s3,index1+1,index2,index3+1) || dfs(s1,s2,s3,index1,index2+1,index3+1));
}
else
{
return dfs(s1,s2,s3,index1,index2+1,index3+1);
}
}
方法二:从上到下的动态规划
vector<vector<int>> dp;
bool isInterleave(string s1, string s2, string s3) {
// 方法一:递归,对字符串“从前到后”进行扫描,超出时间限制
// 为了缩短时间,可将方法一中的“对字符串从前向后扫描的递归”改为“对字符串从后向前扫描的递归”,
// 并配合备忘录的剪枝操作,形成从上到下的动态规划,具体参考方法二
// return helper(s1,s2,s3,0,0,0);
// 方法二:从上到下的动态规划,配合记录进行剪枝,此时对字符串的扫描是从后向前
// dp[i][j]: 表示s1的前i个字符和s2的前j个字符是否可以交织得到s3
// -1: 未初始化;0: s1和s2交织无法得到s3;1: s1和s2交织可以得到s3
int n1 = s1.length(), n2 = s2.length(), n3 = s3.length();
if (n1+n2 != n3)
return 0;
if (dp.empty())
dp.assign(n1+1, vector<int>(n2+1, -1));
do
{
if (-1 != dp[n1][n2])
break;
if (s1.empty())
{
dp[n1][n2] = (s2 == s3 ? 1 : 0);
break;
}
else if (s2.empty())
{
dp[n1][n2] = (s1 == s3 ? 1 : 0);
break;
}
if (s1.back() != s3.back() && s2.back() != s3.back())
{
dp[n1][n2] = 0;
break;
}
if (s1.back() == s3.back())
{
if (s2.back() != s3.back())
dp[n1][n2] = isInterleave(s1.substr(0,n1-1), s2, s3.substr(0,n1+n2-1));
else
dp[n1][n2] = isInterleave(s1.substr(0,n1-1), s2, s3.substr(0,n1+n2-1)) || isInterleave(s1, s2.substr(0,n2-1), s3.substr(0,n1+n2-1));
}
else
{
dp[n1][n2] = isInterleave(s1, s2.substr(0,n2-1), s3.substr(0,n1+n2-1));
}
}while(0);
return dp[n1][n2];
}
子序列的数目:LeetCode-097-字符串-子序列-子序列的数目_hclbeloved的博客-CSDN博客
这个题目同样也有类似的解答,大家可以在力扣上找找,有很多题目存在类似的情况。