视频讲解:(这两题真的非常的巧妙,自己也是真的没想到!!!)
343. 整数拆分
思路:这道题是真的很巧妙,反正自己是想不到递推公式是可以如此表示。一直卡壳在怎么样确定到底分成几个数。递推公式,我们可以这样理解,拆分一定是可以分成至少两个数的,所以后面很合理。另外既然最大乘积是由拆分的数所构成的,那么势必与拆分出的数的状态有关,所以状态转移过程可以明确,但是为什么是j*dp[i-j]?
其实我们不难发现 10 =3+3+4时取得最大值,11=4+4+3这样出现相等元素来的最大,那么而这里的dp[i-j]所要表达就是记录了所拆分出的若干元素的最大积,而j就是出现相同元素之后剩下的值,我们是在遍历小尾巴,让dp[i-j]尽可能是大的值,因此i-j越大,说明其中可拆分的情况就越多,肯定会越大。
另外dp[i-j]是从小开始往上所保存的值,其中记录的是最大值一定是将每个值的所有划分情况都计算过了,因此后续可以直接使用先前的状态来更新后续的,巧妙的避开了需要思考如何划分,真的非常的妙。
// 时间复杂度O(n)
// 空间复杂度O(n)
class Solution {
public int integerBreak(int n) {
// 定义dp数组,数组下标表示的正整数的值,而数组值表示的是当前正整数的最大化乘积
int[] dp = new int[n+1];
// 递推公式,我们可以这样理解,拆分一定是可以分成至少两个数的,所以后面很合理。另外既然最大乘积是由拆分的数所构成的,那么势必与拆分出的数的状态有关,所以状态转移过程可以明确,但是为什么是j*dp[i-j]?
// 其实我们不难发现 10 =3+3+4时取得最大值,11=4+4+3这样出现相等元素来的最大,那么而这里的dp[i-j]所要表达就是记录了所拆分出的若干元素的最大积,而j就是出现相同元素之后剩下的值,我们是在遍历小尾巴,让dp[i-j]尽可能是大的值,因此i-j越大,说明其中可拆分的情况就越多,肯定会越大
// 另外dp[i-j]是从小开始往上所保存的值,其中既然记录的是最大值,那么势必考虑到了划分成如何是最大的
// int res = Math.max(j*dp[i-j], j*(i-j));
// 初始化
dp[0] = 0;
dp[1] = 0;
dp[2] = 1;
// 开始遍历
for(int i=3; i<=n; i++){
for(int j=1; j<i; j++){
int res = Math.max(j*dp[i-j], j*(i-j));
dp[i] = res>dp[i] ? res:dp[i];
}
}
return dp[n];
}
}
96. 不同的二叉搜索树
思路:本题也没做出来,没用想到不同状态之间是如何进行转化的,想到dp数组所具备的含义,下标和数组值都可以明确,但是想不到串联起来的方法。看过题解之后,觉得最关键的就是本题隐藏了BST的构建元素是一定递增的,其次不同数量的节点,在构建时就是选取不同的节点作为根节点,然后再将其余节点安置于左右子树上所形成。而左右子树便可以使用前面已经计算完成的dp值直接进行计算。当然两个节点dp[2]不只是表示节点1和节点2,而是可以节点是任意的具有大小关系的值。
// 时间复杂度O(n)
// 空间复杂度O(n)
class Solution {
public int numTrees(int n) {
// 定义递归数组,数组下标的含义是当前存在i个节点,数组的值表示i个节点时的二叉树的个数
int[] dp = new int[n+1];
// 递推方程,dp[i] += dp[j]*dp[i-j-1]; 即由于数组本身就是递增的,因此选用不同的节点作为根,形成所有的不同二叉树,因此需要将不同的根结点的二叉树数量累加在一起,而这里的状态转换就是切换根结点
// 初始化,空树也是一种二叉树
dp[0] = 1;
dp[1] = 1;
// 开始递归
for(int i=2; i<=n; i++){
for(int j=0; j<i; j++){
dp[i] += dp[j]*dp[i-j-1];
}
}
return dp[n];
}
}