开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情
343. 整数拆分 题目描述:给定一个 正整数 ,将其拆分为 个 正整数 的和(),并使这些整数的乘积最大化。求这个最大乘积。()
| 示例1 | 示例2 |
|---|---|
| 输入: 输出: 解释: | 输入: 输出: 解释: |
中规中矩的动态规划
题意很符合动态规划题目的必要条件,求最值、可枚举、无后效性,但是否存在最优子结构?我们试猜想,记 为整数 拆分 个正整数后,这些正整数的最大乘积。如果知道 的值,能否得到 的值?貌似并不能从 转移到 。
那换个思路,对于正整数 ,当 时,可以拆分成至少两个正整数的和。若 是 拆分出的第一个正整数,则剩下的部分是 ,这部分有两种情况
- 不能拆分,那么乘积就是
- 继续拆分,那么乘积就是 。
1、确定 dp 状态数组
表示正整数 拆分成至少两个正整数后,,这些正整数的最大乘积。
2、确定 dp 状态转移方程
设正整数 拆分出来的第一个正整数为 (其中,),则有以下两种情况:
-
正整数 拆分成 和 ,而且 不能再继续拆分成多个正整数,此时最大的乘积为
-
正整数 拆分成 和 ,而且 可以再继续拆分成多个正整数,此时最大的乘积为
那么状态转移方程为,。
3、确定 dp 初始状态
从 到 ,。
-
和 都不能正常拆分,保证 数组连续,故 ;
-
从 开始都可以正常拆分,而且 只能拆分成 ,故 。
4、确定遍历顺序
-
第一层循环从 到
-
第二层循环从 到
5、确定最终返回值
重温 数组定义:正整数 ,将其拆分为 个 正整数的和(),并使这些整数的乘积最大化,即为
6、代码示例
/**
* 时间复杂度 O(n^2)
* 空间复杂度 O(n)
*/
function integerBreak(n: number): number {
const dp = new Array(n + 1).fill(0);
dp[2] = 1;
for(let i = 3; i <= n; i++) {
for(let j = 1; j < i; j++) {
dp[i] = Math.max(dp[i], j * (i - j), j * dp[i - j]);
}
}
return dp[n];
};
思维升级-不等式
求证最大值不等式
在高中阶段我们接触过这样的不等式: ,当且仅当 时,乘积最大值。
设将数字 以因子 等分为 个,即 ,则乘积为 , 是常数,所以当 取最大值时,乘积取最大值。
令 ,公式两边取对数,得到 ,对 求导,得到 ,整理得到
另 , 恒大于 ,所以只能当 为 时, 才能为 ,得到极值点为 。当 时, 单调递增,当 时, 单调递减,所以 是极大值点。由于 要取正整数,所以 符合题意,代入方程得到 ,所以再拆分整数时,尽可能多拆一些 ,才能使得最终乘积最大。
当 时, 返回 , 返回 。
当 时,,并分为以下三种情况:
-
当 时,直接返回
-
当 时,要将一个 单独拿出来,和这个 组成 ,所以返回值为
-
当 时,直接返回
代码示例
/**
* 时间复杂度 O(1),看js引擎如何实现幂运算
* 空间复杂度 O(1)
*/
function integerBreak(n: number): number {
if (n <= 3) {
return n - 1; // n=2返回1,n=3返回2
}
const a = ~~(n / 3);
const b = n % 3;
if (b === 0 ) {
return 3 ** a;
}
if (b === 1) {
return (3 ** (a - 1)) * 4;
}
return (3 ** a) * 2;
};
回到动态规划
方才我们已经证明了,当时才会让乘积更大,所以我们在拆分整数时应该有尽可能多的 或者 的参与,因此我们改造一下“中规中矩动态规划”算法中的 状态转移方程,降低算法的时间复杂度。
在第二层循环,完全没有必要从 一直遍历到 ,我们只需要判断 、、、,即
代码示例
/**
* 时间复杂度 O(n)
* 空间复杂度 O(n)
*/
function integerBreak(n: number): number {
const dp = new Array(n + 1).fill(0);
dp[2] = 1;
dp[3] = 2;
for(let i = 4; i <= n; i++) {
dp[i] = Math.max(2 * (i - 2), 2 * dp[i - 2], 3 * (i - 3), 3 * dp[i - 3]);
}
return dp[n];
};