题目
整数拆分
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
说明: 你可以假设 n 不小于 2 且不大于 58。
1. 集合定义
对于一个整数 ( n ),将其拆分的所有方式记为集合 ( S(n) ):
S ( n ) = { ( a 1 , a 2 , … , a k ) ∣ a 1 + a 2 + ⋯ + a k = n , a i > 0 , k ≥ 2 } S(n) = \{(a_1, a_2, \dots, a_k) \mid a_1 + a_2 + \cdots + a_k = n, \, a_i > 0, \, k \geq 2\} S(n)={(a1,a2,…,ak)∣a1+a2+⋯+ak=n,ai>0,k≥2}
每种拆分方式对应一个乘积:
P ( a 1 , a 2 , … , a k ) = a 1 × a 2 × ⋯ × a k P(a_1, a_2, \dots, a_k) = a_1 \times a_2 \times \cdots \times a_k P(a1,a2,…,ak)=a1×a2×⋯×ak
我们需要在这些拆分方式中找到乘积最大的那种,记为 d p [ n ] dp[n] dp[n]:
d p [ n ] = max ( a 1 , a 2 , … , a k ) ∈ S ( n ) P ( a 1 , a 2 , … , a k ) dp[n] = \max_{(a_1, a_2, \dots, a_k) \in S(n)} P(a_1, a_2, \dots, a_k) dp[n]=(a1,a2,…,ak)∈S(n)maxP(a1,a2,…,ak)
2. 集合拆解
考虑将整数 n n n 拆分为两部分 j j j和 n − j n-j n−j,其中 j j j是拆分点:
n = j + ( n − j ) n = j + (n-j) n=j+(n−j)
此时可以将 S ( n ) S(n) S(n)拆解为两类子集合:
-
直接拆分:只将 n n n 拆分为两部分 j j j 和 n − j n-j n−j,对应的乘积为:
P ( j , n − j ) = j × ( n − j ) P(j, n-j) = j \times (n-j) P(j,n−j)=j×(n−j) -
递归拆分:进一步对 n − j n-j n−j继续拆分,对应的乘积为:
P ( j , S ( n − j ) ) = j × d p [ n − j ] P(j, S(n-j)) = j \times dp[n-j] P(j,S(n−j))=j×dp[n−j]
因此,集合 S ( n ) S(n) S(n)的最大值可以通过以下公式表示:
d p [ n ] = max 1 ≤ j < n { j × ( n − j ) , j × d p [ n − j ] } dp[n] = \max_{1 \leq j < n} \{j \times (n-j), j \times dp[n-j]\} dp[n]=1≤j<nmax{j×(n−j),j×dp[n−j]}
3. 动态规划状态转移公式
通过上述分析,可以总结出状态转移公式:
d p [ n ] = max 1 ≤ j < n { max ( j × ( n − j ) , j × d p [ n − j ] ) } dp[n] = \max_{1 \leq j < n} \{\max(j \times (n-j), j \times dp[n-j])\} dp[n]=1≤j<nmax{max(j×(n−j),j×dp[n−j])}
其中:
- j × ( n − j ) j \times (n-j) j×(n−j) 表示不递归拆分,直接将 n n n 拆为 j j j和 n − j n-j n−j。
- j × d p [ n − j ] j \times dp[n-j] j×dp[n−j] 表示递归使用之前的最优解。
4. 初始化条件
动态规划需要初始值:
- d p [ 1 ] = 1 dp[1] = 1 dp[1]=1(虽然通常不拆分,但作为递归的基准)。
- d p [ 2 ] = 1 dp[2] = 1 dp[2]=1(2 拆分为 1 和 1,乘积为 1)。
5. 集合视角的递推步骤
从小到大递推:
- 对于每个 n n n,枚举所有可能的拆分点 j j j。
- 计算拆分后的两个子问题:
- 不再拆分的乘积: j × ( n − j ) j \times (n-j) j×(n−j)。
- 继续递归的乘积: j × d p [ n − j ] j \times dp[n-j] j×dp[n−j]。
- 在所有拆分中找到最大值,赋值给 d p [ n ] dp[n] dp[n]。
示例推导 $n = 5 )
集合拆分视角分析:
- S ( 5 ) S(5) S(5):所有可能的拆分:
S ( 5 ) = { ( 4 , 1 ) , ( 3 , 2 ) , ( 3 , 1 , 1 ) , ( 2 , 2 , 1 ) , ( 2 , 1 , 1 , 1 ) , ( 1 , 1 , 1 , 1 , 1 ) } S(5) = \{(4, 1), (3, 2), (3, 1, 1), (2, 2, 1), (2, 1, 1, 1), (1, 1, 1, 1, 1)\} S(5)={(4,1),(3,2),(3,1,1),(2,2,1),(2,1,1,1),(1,1,1,1,1)} - 最大乘积来自以下两种情况:
- 直接拆分为两部分,枚举 j = 1 , 2 , 3 , 4 j = 1, 2, 3, 4 j=1,2,3,4。
- 递归拆分,利用 d p [ n − j ] dp[n-j] dp[n−j]。
动态规划计算:
- 初始化: d p [ 1 ] = 1 dp[1] = 1 dp[1]=1, d p [ 2 ] = 1 dp[2] = 1 dp[2]=1。
- 递推:
- d p [ 3 ] = max ( 1 × 2 , 1 × d p [ 2 ] ) = 2 dp[3] = \max(1 \times 2, 1 \times dp[2]) = 2 dp[3]=max(1×2,1×dp[2])=2
- d p [ 4 ] = max ( 1 × 3 , 1 × d p [ 3 ] , 2 × 2 ) = 4 dp[4] = \max(1 \times 3, 1 \times dp[3], 2 \times 2) = 4 dp[4]=max(1×3,1×dp[3],2×2)=4
- d p [ 5 ] = max ( 1 × 4 , 1 × d p [ 4 ] , 2 × 3 , 2 × d p [ 3 ] ) = 6 dp[5] = \max(1 \times 4, 1 \times dp[4], 2 \times 3, 2 \times dp[3]) = 6 dp[5]=max(1×4,1×dp[4],2×3,2×dp[3])=6
最终答案: d p [ 5 ] = 6 dp[5] = 6 dp[5]=6。
1. 优化状态转移
当前的状态转移公式的时间复杂度为 O ( n 2 ) O(n^2) O(n2),因为每次计算 d p [ n ] dp[n] dp[n] 都需要枚举所有可能的拆分点 j j j。可以考虑利用数学性质来减少不必要的计算。
优化思路:
-
减小计算量:拆分 n n n 为 j j j和 n − j n-j n−j 时,最大乘积通常出现在 j j j和 n − j n-j n−j较为均匀的情况。因此,可以优先考虑接近 n 2 \frac{n}{2} 2n 的拆分点。
-
剪枝:在实际实现中,可以通过观察某些 ( j ) 的拆分结果始终小于其他拆分的结果,从而剪去这些不必要的拆分点,加速计算。
2. 递归与迭代
动态规划可以通过递归或迭代来实现。递归形式易于理解,但迭代实现通常更高效,避免了函数调用的开销。
迭代实现的伪代码如下:
def integerBreak(n):dp = [0] * (n + 1)dp[1] = 1dp[2] = 1for i in range(3, n + 1):for j in range(1, i):dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j]))return dp[n]
补充
公式 j × ( n − j ) j \times (n-j) j×(n−j) 的由来,源于将整数 n n n 拆分成两部分时的直接乘积计算。它的推导过程如下:
1. 拆分的基本思想
整数拆分问题的核心是将一个整数 n n n 分成若干个整数的和:
n = a 1 + a 2 + ⋯ + a k n = a_1 + a_2 + \dots + a_k n=a1+a2+⋯+ak
为了最大化这些整数的乘积 P ( a 1 , a 2 , … , a k ) P(a_1, a_2, \dots, a_k) P(a1,a2,…,ak),我们需要枚举可能的拆分方式。
一种最简单的拆分方式是只把 n n n 拆成两个整数 j j j 和 n − j n-j n−j,其中 j j j 是一个拆分点。此时,乘积为:
j × ( n − j ) j \times (n-j) j×(n−j)
这直接反映了拆分点 j j j 的贡献和剩余部分 n − j n-j n−j 的乘积。
2. 从数学推导来看
对于一个整数 n n n,如果只考虑拆分为两部分 j j j 和 n − j n-j n−j,它的乘积公式自然就是:
P = j × ( n − j ) P = j \times (n-j) P=j×(n−j)
这里:
- j j j 是第一个拆分出的部分。
- n − j n-j n−j 是剩下的部分。
这个公式来源于简单的代数计算:如果将 n n n 拆分为两部分,二者的乘积就是这些部分相乘。
3. 从动态规划的角度来看
动态规划的问题需要从小到大逐步计算。对于 d p [ n ] dp[n] dp[n],我们尝试枚举拆分点 j j j,将 n n n 分为两部分:
- 一部分是 j j j,它不会再拆分。
- 另一部分是 n − j n-j n−j,它可以继续拆分。
直接拆分的乘积公式就是:
j × ( n − j ) j \times (n-j) j×(n−j)
如果只考虑这一步拆分,这是计算的直接结果。
4. 为何需要直接乘积 j × ( n − j ) j \times (n-j) j×(n−j)
在动态规划中,最大乘积有两种情况:
- 直接拆分的结果:
- 拆分为 j j j 和 n − j n-j n−j,只计算这两部分的乘积:
P = j × ( n − j ) P = j \times (n-j) P=j×(n−j) - 这是不进一步递归时的结果。
- 拆分为 j j j 和 n − j n-j n−j,只计算这两部分的乘积:
- 递归子问题的结果:
- 如果 n − j n-j n−j 可以进一步拆分,我们取其最大乘积 d p [ n − j ] dp[n-j] dp[n−j],然后乘以 j j j:
P = j × d p [ n − j ] P = j \times dp[n-j] P=j×dp[n−j]
两者都需要考虑,取其最大值。
- 如果 n − j n-j n−j 可以进一步拆分,我们取其最大乘积 d p [ n − j ] dp[n-j] dp[n−j],然后乘以 j j j:
5. 为什么需要枚举 j j j
在计算最大乘积时,拆分点 j j j 的选择对结果影响很大。例如:
- 当 j j j 较小时,剩余部分 n − j n-j n−j 较大,可能需要进一步拆分。
- 当 j j j 较大时,剩余部分较小,可能直接求最大乘积。
通过枚举 j j j,我们可以覆盖所有可能的拆分情况,确保找到最大值。
6. 直观理解 j × ( n − j ) j \times (n-j) j×(n−j) 的作用
- j × ( n − j ) j \times (n-j) j×(n−j) 是拆分后的直接乘积,体现了最简单的拆分方式。
- 这是一种边界条件(base case),即当剩余部分 n − j n-j n−j 不再进一步拆分时的结果。
- 它提供了动态规划的基础值,与 j × d p [ n − j ] j \times dp[n-j] j×dp[n−j] 共同构成状态转移的核心逻辑。
总结
公式 j × ( n − j ) j \times (n-j) j×(n−j) 的灵感来自于拆分的基本操作,即将整数 n n n 分为两部分的直接乘积:
- 是最简单的整数拆分形式。
- 作为动态规划的一种情况,帮助递归计算中找到最大乘积。
- 构成状态转移公式的一部分,与递归解法( j × d p [ n − j ] j \times dp[n-j] j×dp[n−j])相辅相成,确保覆盖所有可能的拆分情况。