1. 确定dp数组(dp table)以及下标的含义
dp[i]:分拆数字i,可以得到的最大乘积为dp[i]
2. 确定递推公式
其实可以从1遍历j,然后有两种渠道得到dp[i].
一个是j * (i - j) 直接相乘。
一个是j * dp[i - j]
j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。
dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j});
3. dp的初始化
只有从dp[2]=1的初始化才有意义
4. 确定遍历顺序
确定遍历顺序,先来看看递归公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。
for (int i = 3; i <= n ; i++) {
for (int j = 1; j <= i / 2; j++) {
dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
}
}
因为拆分一个数n 使之乘积最大,那么一定是拆分成m个近似相同的子数相乘才是最大的
所以n/2之后一定不是最大值
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n+1];
dp[2]=1;
for(int i=3;i<=n;i++){
for(int j = 1;j<=i/2;j++){
dp[i]=max(dp[i],j*(i-j),j*dp[i-j]);
}
}
return dp[n];
}
public int max(int i,int j,int p){
int res = Math.max(i,j);
res = Math.max(p,res);
return res;
}
}
还有一种解释:
class Solution {
public int integerBreak(int n) {
if (n <= 3) {
return n - 1;
}
int[] dp = new int[n + 1];
// Set base cases
for (int i = 1; i <= 3; i++) {
dp[i] = i;
}
for (int num = 4; num <= n; num++) {
int ans = num;
for (int i = 2; i < num; i++) {
ans = Math.max(ans, i * dp[num - i]);
}
dp[num] = ans;
}
return dp[n];
}
}
Let's say we have an integer num
and split it into two integers: i
and num - i
. The highest product possible would be i * BEST
, where BEST
is the highest product possible from splitting up num - i
.
Notice that the variable BEST
represents the original problem with a different input (num - i
). This allows us to think in terms of dynamic programming. Let's define a function dp(num)
that returns the highest possible product from splitting num
up.
可以理解为这个dp(num)中的num本来就被拆解过一次了,所以存在不拆解直接返回的情况
We have the following base cases for this function:
num == 1
, then it isn't possible to split the number up, so we just return 1
.num == 2
, then it would be better to not split the number at all, since the only possible split 1 * 1
is less than 2
, so just return 2
. The exact same argument can be made for num == 3
: the only possible split 1 * 2
is less than 3
itself, so just return 3
.Otherwise, we have two options:
ans = num
.i
from 2
until num
. For each value of i
, try to update ans
with i * dp(num - i)
if it is larger.You may be thinking: what about the constraint where we need to have at least 2
integers? We need to check for 2 separate cases before performing the recursion.
n == 2
, we immediately return 1
. The only possible split is 1 * 1
.n == 3
, we immediately return 2
. The only possible split is 1 * 2
.由此可见integerBreak(n)与dp[n]并不相同,小于3时要单独拎出来分析,必须要拆分
dp(3)作为已经被拆解过的对象,不需要被再次拆解,是可以直接返回3本身的,这也是为什么n<=3时dp[3]=n
但是对于integerBreak(n) 来说,是找到n的拆解项乘积的最大值有这样的关系:integerBreak(2)=dp(1)*dp(1)
integerBreak(3)= max(dp(1)*dp(2))
integerBreak(4) = max( dp(1)*dp(3) , dp(2)*dp(2) )
We need to explicitly check for these cases before going into the recursion, otherwise, we would incorrectly return a larger answer since we initialize ans = num
.
For all other values of n
, the recursion will work. Take a look at the first few numbers:
num = 4
, we can do 2 * 2 = 4
, which is not less than 4
itself.num = 5
, we can do 2 * 3 = 6
, which is not less than 5
itself.num = 6
, we can do 3 * 3 = 9
, which is not less than 6
itself.可见当dp[n] (n>=4)时,dp[n] == integerBreak(n)因为dp作为被拆分项,他的Best是对其接着拆分
BST有多少个节点,就会有多少种可能的情况
1. 确定dp数组(dp table)以及下标的含义
dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]。
也可以理解是i个不同元素节点组成的二叉搜索树的个数为dp[i] ,都是一样的。
2.确定递推公式
在上面的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量]
j相当于是头结点的元素,从1遍历到i为止。
所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
3.dp数组如何初始化
初始化,只需要初始化dp[0]就可以了,推导的基础,都是dp[0]。
dp[0]其实就是空节点:初始化为1
dp[1]可以用dp[0]推导,其实不用初始化
4. 确定遍历顺序
首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。 所以从前往后遍历
那么遍历i里面每一个数作为头结点的状态,用j来遍历。
class Solution {
public int numTrees(int n) {
//初始化 dp 数组
int[] dp = new int[n + 1];
//初始化0个节点和1个节点的情况
dp[0] = 1;
//dp[1] = 1;
//for (int i = 2; i <= n; i++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= i; j++) {
//对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加
//一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
}