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

发布时间:2023年12月18日

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

文章来源:https://blog.csdn.net/AdrianLeon/article/details/135060307
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。