LeetCode 343. 整数拆分
题目描述

题意分析
把整数
n写成至少两个正整数之和,使这些正整数的乘积最大,返回最大乘积。每一部分都必须大于零,拆成多少部分可以自行决定,不能把完全不拆的n直接当成答案。题目给定
2 <= n <= 58,枚举较小整数的最优拆分足以满足范围。关键是区分“某部分保持完整”和“某部分继续拆分”,二者可能产生不同的乘积。
解法:动态规划枚举第一段
核心思路
[!blue]
定义
dp[i]为把整数i至少拆成两部分后能得到的最大乘积。状态里不包含保持整个i不拆的情况,这个约定直接决定了转移需要比较哪些候选。任意一个合法拆分,都可以先拿出其中一个完整部分,记为
j,其余部分的和就是i - j。枚举1 <= j < i,剩余部分有两种选择:保持完整,得到j * (i - j);继续拆成至少两部分,得到j * dp[i - j]。两者取最大,再在所有j中取最大,就是dp[i]。不需要再单独拆开
j,因为它被定义为最终方案中拿出来的一整部分。任何方案总能选出这样一部分,其余部分要么只有一段,要么由更小规模的最优状态覆盖。因此这两类选择能覆盖所有合法拆法;同一方案被重复枚举也不会影响取最大值。从
i = 2向上计算时,依赖的dp[i - j]下标一定更小,已经计算完。dp[1] = 0表示正整数1无法继续拆成两个正数,但保留余数不拆的候选仍然有效,因此不会遗漏含有1的合法方案。所有转移都已拿出j和一个正的剩余部分,保证最终答案至少拆了两段。
解题步骤
- 创建长度为
n + 1的dp,初始值为零。- 从
i = 2到n依次计算,每次枚举第一部分j,范围为[1, i - 1]。- 分别计算
j * (i - j)与j * dp[i - j],对应剩余部分不拆和继续拆。- 用这两个候选更新
dp[i],最终返回dp[n]。
代码实现
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n + 1];
for (int i = 2; i <= n; i++) {
for (int j = 1; j < i; j++) {
// 固定剪出一段后,剩余部分可以保持完整,也可以继续拆分。
int noSplit = j * (i - j);
int split = j * dp[i - j];
dp[i] = Math.max(dp[i], Math.max(noSplit, split));
}
}
return dp[n];
}
}
func integerBreak(n int) int {
dp := make([]int, n+1)
for i := 2; i <= n; i++ {
for j := 1; j < i; j++ {
// 固定剪出一段后,剩余部分可以保持完整,也可以继续拆分。
noSplit := j * (i - j)
split := j * dp[i-j]
if noSplit > dp[i] {
dp[i] = noSplit
}
if split > dp[i] {
dp[i] = split
}
}
}
return dp[n]
}
复杂度分析
- 时间复杂度:$O(n^2)$。每个 $i$ 枚举 $i-1$ 种第一部分,累加为平方级。
- 空间复杂度:$O(n)$,保存从较小整数到目标整数的最优拆分值。
关键点总结
[!green]
dp[i]强制表示已经拆分,不能与不拆的整数i混为一谈。- 枚举一个完整部分,剩余部分同时考虑保持完整和继续拆分。
- 依赖只指向更小整数,递增计算即可满足顺序。
易错点总结
[!yellow]
- 只考虑
j * dp[i - j],会强迫余数继续拆,遗漏只有两部分的方案。- 只考虑
j * (i - j),就只能拆成两部分,无法找到需要更多部分的最优解。- 使用
dp[j] * dp[i - j]会强制两边都继续拆,无法覆盖所有合法拆分。j必须小于i,否则剩余部分为零,违反正整数要求。- 不能把
dp[i]初始化为i,这会把不拆分的非法候选混进答案。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 剑指 Offer 14- II. 剪绳子 II | 中等 | 最大乘积的拆分规律相同,原题范围更大且要取模,不能把取模后的值直接用于大小比较。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!