Leetcode每日刷题(十二)

223 阅读3分钟

题目:整数拆分

描述: 给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。

示例 1:

输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

说明: 你可以假设 2\leq n \leq 58

解法一:动态规划

对于正整数 n, 当 n \geq 2 时,可以拆分成至少两个正整数的和, 令 k 是拆分出的第一个正整数,则剩下的部分是 n-k, n-k 可以不继续拆分,或者拆分成至少两个正整数的和,由于每个正整数对应的最大乘积取决于比它小的正整数对应的最大乘积,因此可以使用动态规划求解。

创建数组 dp, 其中 dp[i] 表示将正整数 i 至少拆分成两个正整数的和之后,这些正整数的最大乘积。易得 dp[0]=dp[1]=0

i \geq 2 时,若 i 拆出的第一个正整数是 j(1 \leq j \leq i), 则有以下方案:

  • i 拆分成 ji-j 的和,且 i - j 不再拆分成多个正整数,此时的乘积是 j \times (i - j);
  • i 拆分成 ji-j 的和,且 i - j 继续拆分成多个正整数,此时的乘积是 j \times dp[i-j];

因此,当 j 固定时, 有 \text{dp}[i]=\max(j \times (i-j), j \times \text{dp}[i-j])。由于 j 的取值范围为 [1,i-1], 需要遍历所有的 j 得到 dp[i] 的最大值,因此可得到状态转移方程如下:

dp[i]= \max_{1 \leq j < i}\{max(j×(i−j),j×dp[i−j])\}

最终得到 dp[n] 的值为将正整数 n 拆分成至少两个正整数的和之后,这些正整数的最大乘积。

public static int integerBreak(int n) {
    int[] dp = new int[n + 1];

    for (int i = 2; i < n; i++) {
        // i 拆分后乘积最大值
        int curMax = 0;
        for (int j = 1; j < i; j++) {
            curMax = Math.max(curMax, Math.max(j * (j - 1), dp[i - j]));
        }
        dp[i] = curMax;
    }
    return dp[n];   
}

复杂度分析:

  • 时间复杂度:O(n^2),其中 n 是给定的正整数。对于从 2n 的每一个整数都要计算对应的 \text{dp} 值,计算一个整数对应的 \text{dp} 值需要 O(n) 的时间复杂度,因此总时间复杂度是 O(n^2)

  • 空间复杂度:O(n),其中 n 是给定的正整数。创建一个数组 \text{dp},其长度为 n+1

解法二:优化的动态规划

方法一中状态转移方程如下:

dp[i]= \max_{1 \leq j < i}\{max(j×(i−j),j×dp[i−j])\}

优化后的状态方程为(优化推理):

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

复杂度分析:

  • 时间复杂度:O(n),其中 n 是给定的正整数。和方法一相比,计算每个整数对应的 \text{dp} 的值的时间复杂度降到 O(1),因此总时间复杂度降到 O(n)

  • 空间复杂度:O(n),其中 n 是给定的正整数。创建一个数组 \text{dp},其长度为 n+1

解法三:数学推理

作者:Krahets

结果:

拆分规则:

  1. 最优: 3。把数字 n 可能拆为多个因子 3 ,余数可能为 0,1,2 三种情况。
  2. 次优: 2。若余数为 2 ;则保留,不再拆为 1+1
  3. 最差: 1。若余数为 1 ;则应把一份 3 + 1 替换为 2 + 2,因为 2 \times 2 > 3 \times 1

算法流程:

  1. n \leq 3 时,按照规则应不拆分,但由于题目要求必须拆分,因此必须拆出一个因子 1 ,即返回 n - 1
  2. n>3 时,求 n 除以 3 的 整数部分 a 和 余数部分 b (即 n = 3a + b ),并分为以下三种情况:
    • b = 0 时,直接返回 3^a
    • b = 1 时,要将一个 1 + 3 转换为 2+2,因此返回 3^{a-1} \times 4
    • b = 2 时,返回 3^a \times 2
class Solution {
    public int integerBreak(int n) {
        if(n <= 3) return n - 1;
        int a = n / 3, b = n % 3;
        if(b == 0) return (int)Math.pow(3, a);
        if(b == 1) return (int)Math.pow(3, a - 1) * 4;
        return (int)Math.pow(3, a) * 2;
    }
}