动态规划基础部分07整数拆分

260 阅读5分钟

整数拆分

力扣343. 整数拆分 - 力扣(LeetCode)
力扣剑指 Offer 14- I. 剪绳子 - 力扣(LeetCode)
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。返回你可以获得的最大乘积。
示例 1:
输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
提示:
2 <= n <= 58 看到这道题⽬,都会想拆成两个呢,还是三个呢,还是四个....我们一起来看⼀下如何使⽤动规来解决。

动规五部曲:

1. 确定dp数组以及下标的含义

dp[i]:当数字为i时,分拆数字i可以得到的最⼤乘积为dp[i]。dp[i]的定义讲贯彻整个解题过程,下⾯哪⼀步想不懂了,就想想dp[i]究竟表示的是啥!

2. 确定递推公式

dp[i]最⼤乘积是怎么得到的呢?由动态规划的定义:动态规划中每⼀个状态⼀定是由上⼀个状态推导出来和dp数组以及下标的含义我们可以知道想要得到dp[i]的最⼤乘积肯定要先得到dp[1]~dp[i-1]的最大乘积,我们假设我们已经知道了dp[1]~dp[i-1]的最大乘积那么dp[i]的最大乘积不就是j * dp[i-j] (j < i - 1)
这里可能会有朋友不理解为什么,可以先回想一下dp数组的定义,我们再来看一下这段代码实现:

        for(int j = 1; j < i - 1; j++){
            dp[i] = Math.max(dp[i], j * dp[i - j]);
        }

看完这段代码我相信很多朋友应该已经理解并且知道递推公式就是:dp[i] = Math.max(dp[i], j * dp[i - j]) 其实就是将dp[i]拆分为j 和 dp[i - j]两部分,由于dp[i - j]已经是最大值了,所以只需要将j从1~i-1枚举出来既可以找到最大值,那还有一个问题j为什莫不是小于i而是小于i - 1呢?仔细想想就知道了当就j = i - 1的时候, dp[i] = (i -1) * dp[1] = i - 1 < dp[i]。

3. dp数组如何初始化

由于为了递推公式的后续计算我们不得不将dp数组初始化为dp[1] = 1;dp[2] = 2; dp[3] = 3;并且在程序开始要先判断n是否小于等于3并返回n - 1;这明显是不符合动态规划的定义的。别着急我们先继续往下看。

4. 确定遍历顺序

确定遍历顺序,先来看看递归公式:dp[i] = max(dp[i], j, dp[i - j] * j);dp[i] 是依靠 dp[i - j]的状态,所以遍历i⼀定是从前向后遍历,先有dp[i - j]再有dp[i]。枚举j的时候,是从1开始的。i是从4开始,所以遍历顺序为:

        for(int i = 4; i <= n; i++){
            for(int j = 1; j < i - 1; j++){
                dp[i] = Math.max(dp[i - j] * j , dp[i]);
            }
        }

5. 举例推导dp数组

举例当n为10 的时候,dp数组⾥的数值,如下:

image.png
动规五部分分析完毕,对应Java代码如下:

class Solution {
    public int integerBreak(int n) {
        if(n <= 3) return n - 1;
        int[] dp = new int[n + 1];
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        for(int i = 4; i <= n; i++){
            for(int j = 1; j < i - 1; j++){
                dp[i] = Math.max(dp[i - j] * j , dp[i]);
            }
        }
        return dp[n];
    }
}

前面我们在初始化 dp数组时就发现⾃相⽭盾了,dp[1]为什么⼀定是1呢?根据dp[i]的定义,dp[2]也不应该是2,dp[3]也不应该是3啊。虽然代码在初始位置有⼀个判断if (n <= 3) return 1 * (n - 1);,保证n<=3 结果是正确的,但代码后⾯⼜要给dp[1]赋值1 和 dp[2] 赋值 2,这其实就是⾃相⽭盾的代码,违背了dp[i]的定义!

不少朋友应该疑惑,dp[0] dp[1]应该初始化多少呢?有的题解⾥会给出dp[0] = 1,dp[1] = 1的初始化,但解释⽐较牵强,主要还是因为这么初始化可以把题⽬过了。严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。拆分0和拆分1的最⼤乘积是多少?这是⽆解的。这⾥我只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最⼤乘积是1,这个没有任何异议!

当我们将代码修改为:

        dp[2] = 1;
        for(int i = 3; i <= n; i++){
            for(int j = 1; j < i - 1; j++){
                dp[i] = Math.max(dp[i - j] * j, dp[i]);
            }
        }

当你再次提交时你又会发现结果出错了,哈哈哈朋友们肯定想说你这不是误导人吗还在这写博客。其实我们的新的初始化dp数组是绝对正确的,其实是递推公式不对,不卖关子了我先将正确的递推公式写出来:dp[i] = Math.max(dp[i], Math.max((i - j) * j, dp[i - j] * j));其实也是很简单的其实是有两种渠道得到dp[i].⼀个是j * (i - j) 直接相乘。⼀个是j * dp[i - j]。只不过在错误的初始化dp数组时我们将 dp[1] = 1; dp[2] = 2;p[3] = 3;导致我们可以将条件j * (i - j)忽略。那么正确的Java代码如下:

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 - 1; j++){
                dp[i] = Math.max(Math.max(dp[i - j] * j, (i -j) * j) , dp[i]);
            }
        }
        return dp[n];
    }
}