前言
最近在牛客上刷机试题,刚好去年考研复试刷了题,所以做起来还挺顺手的。大多数题都知道该怎么做,不会的看了题解也可以写出来。直到动态规划这部分内容,除了简单的斐波那契数列和爬楼梯外(这两道题还是用递归才AC的),其他题目是真的没有思路,能够看懂题解,但是下一道题还是不会做。于是,我上B站看视频学习,看到“代码随想录”的Carl老师讲得很详细,就在这里记录以下学习心得。
动态规划五部曲
即分析动态规划问题的五个步骤
确定dp数组以及下标的含义
确定递推公式
dp数组如何初始化
确定遍历顺序
打印dp数组(根据dp数组判断出错的地方)
例子
斐波那契数列、爬楼梯等简单的例子就不放这里了,放几个我觉得思路有点难想到的题目
整数拆分
给定一个正整数n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。返回可以获得的最大乘积。
示例1:
输入:2
输出:1
示例2:
输入:10
输出:36
下面开始用动态规划五部曲来分析这个问题
确定dp数组及下标的含义
dp[i]表示正整数i拆分成多个正整数的最大乘积
确定递推公式
dp[i]有两个来源:
j * (i - j),j为小于i - 1的正整数(因为后面初始化的下标为2,所以j的最大值为i - 2)
j * dp[i-j]
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]))
dp数组初始化
0和1不可以拆分成其他正整数,所以不管这两个
将dp[2]初始化为1,因为2只能拆为1和1,乘积为1
至于其他数,可以不初始化,递推公式会修改值
确定遍历顺序
dp[i]的值一部分由下标比它小的值(dp[i - j])决定,所以遍历顺序从小到大,从i = 3开始,直到i等于输入值
打印dp数组
这一步是根据dp结果来判断错误在哪儿,是递推公式有问题?初始化有问题?遍历顺序有问题?
int integerBreak(int n){
vector<int> dp(n + 1, 1);
for(int i = 3; i <= n; i++){
for(int j = 1; j < i - 1; j++){
dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]));
}
}
return dp[n];
}
不同的二叉搜索树
题目:96. 不同的二叉搜索树 - 力扣(LeetCode)
分析步骤:
dp数组及下标含义
dp[i]表示节点数为i的二叉搜索树的种数
递推公式
一棵节点数为i的二叉搜索树可以看成一个根节点+左子树为(j - 1)个节点的二叉搜索树+右子树为(i - j - 1)个节点的二叉搜索树
dp[i] += dp[j - 1] * dp[i - j], j为<= i 的正整数
这里为什么是相乘,而不是相加?这就是一个排列组合问题。比如,第一组[1, 2]和第二组[3, 4]两两搭配有多少种搭配方式。第一组的“1”可以和第二组的“3”和“4”分别搭配为[1, 3]和[1, 4],第一组的“2”同理。所以是2 * 2 = 4种。
初始化
j的最小值为1,根据递推公式,dp[j - 1]即dp[0]需要初始化。我们知道空节点是一种二叉搜索树,并且根据递推公式,左子树为空,二叉搜索树种数就应该等于右子树的种数,所以,dp[0]应该初始化为1
其他数可以不初始化,递推公式会修改其值
遍历顺序
根据递推公式,dp[i]由下标比它小的值决定,所以从小到大,从i = 1开始遍历,直到i等于输入的n
int numTrees(int n){
vector<int> dp(n + 1, 1);
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i; j++){
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
总结
根据动态规划五部曲走,可以清楚明白地解决绝大部分的动态规划问题,有的问题的递推关系的确难以想到。下一篇文章将介绍动态规划的经典类型——背包问题,这个也是一大难点。