day41算法训练|动态规划part03343. 整数拆分

343. 整数拆分

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:

  1. If num == 1, then it isn't possible to split the number up, so we just return 1.
  2. If 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:

  1. Don't split the number up at all. We can initialize the answer as ans = num.
  2. Split the number. We can try all possible splits. Iterate 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.

  1. If n == 2, we immediately return 1. The only possible split is 1 * 1.
  2. If 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:

  • For num = 4, we can do 2 * 2 = 4, which is not less than 4 itself.
  • For num = 5, we can do 2 * 3 = 6, which is not less than 5 itself.
  • For 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是对其接着拆分

96.不同的二叉搜索树

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];
    }
}

你可能感兴趣的:(算法,动态规划)