news 2026/10/22 4:56:40

LC.343整数拆分:动态规划与数学推导求解最大乘积

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
LC.343整数拆分:动态规划与数学推导求解最大乘积

1. 先搞懂题目到底在问什么

LC.343 整数拆分,题目描述很短,但第一次见到它的人大概率会懵一下:给定一个正整数 n,把它拆分成至少两个正整数的和,问这些拆分出来的整数乘积最大能到多少。

举个例子,n = 10。你可以拆成 3 + 3 + 4,乘积 3 × 3 × 4 = 36;也可以拆成 2 + 2 + 2 + 2 + 2,乘积是 32;拆成 5 + 5,乘积是 25。显然不同的拆法差别很大,题目要的就是那个最大值。

这个问题看起来是小学算术题,实际上是一道典型的算法题,涉及两个方向:动态规划(DP)和数学贪心推导。很多初学者上来就套 DP 模板,结果初始化边界搞错;也有人凭感觉觉得拆得越碎越好,结果发现 1 分多了反而乘积变小。这篇博文会把两条路线都拆开讲透,包括为什么 dp 数组要那样初始化、数学推导里为什么 3 是最优拆分数、以及刷题时最容易踩的几个坑。

2. DP 解法的设计思路与推导过程

2.1 从“拆与不拆”建立状态转移

动态规划拿到手,第一步定义 dp[i]:正整数 i 拆分后能拿到的最大乘积。n=2 是边界,只能拆成 1+1,乘积是 1,所以 dp[2] = 1。

对于 dp[i],核心思路是枚举第一刀:把它拆成 j 和 i - j 两部分,其中 j 从 1 枚举到 i/2 就行(后半部分对称,枚举到一半足够)。关键是,i - j 这一部分要不要继续拆?

  • 不拆:乘积是 j × (i - j)
  • 继续拆:乘积是 j × dp[i - j]

为什么是 j × dp[i - j]?因为 dp[i - j] 已经定义成“把 i - j 这个数拆分成若干正整数后的最大乘积”,i - j 本身就是一个待拆分的子问题,符合最优子结构。

于是转移公式就出来了:

dp[i] = max( j * (i - j), j * dp[i - j] )

其中 j 从 1 取到 i / 2,每次更新最大值。

2.2 为什么 dp[i] 不是 max(j * dp[i-j], dp[j] * dp[i-j]) 之类的组合

有个常见的困惑:既然 i - j 可以继续拆,那 j 是不是也可以继续拆?比如 dp[j] * dp[i - j] 会不会更大?

答案是:不会。因为我们的枚举方式已经覆盖了所有可能。这里的关键是对称性——如果 j 也要继续拆,那“第一刀”划在哪其实可以移动。反过来看,无论最终拆分结果是怎样的,总能找到一种枚举顺序,让 j 恰好是最终拆出来的某个“不继续拆的碎块”,而其余部分全部放进 i - j 里继续递归处理。

只要 dp[i - j] 的定义是“最大乘积”,而不是“必须拆”,它天然包含了 i - j 只拆成一部分、另一部分不拆的情况。所以不用额外考虑 dp[j] × dp[i - j],那只会增加无意义的枚举,结果还一样。

2.3 初始化细节:为什么 dp[2] = 1 而 dp[1] 可以不设置

拆分的硬性要求是“至少两个正整数”,所以 i = 2 时只能拆成 1 + 1,dp[2] = 1。

i = 3 时,拆法有 1+2 和 1+1+1,最大是 1×2 = 2,也就是 dp[3] = 2。

i = 4 时,第一刀 j=1:1×3 = 3,1×dp[3] = 2,取较大值 3;j=2:2×2 = 4,2×dp[2] = 2,取较大值 4。所以 dp[4] = 4。

很多题解代码里会把 dp 数组提前初始化成 dp[i] = i,这其实是给“不拆”留了个底。比如 dp[3] 设置成 3,表示如果允许“不拆分”时的最大“乘积”是 3 本身,但题目明确要求必须拆,所以 dp[2] 和 dp[3] 必须单独处理成 1 和 2。如果统一初始化成 i,然后靠 max 来收敛,边界一错整个答案就飘。

我实际写的时候更倾向于这样:

  1. dp[2] = 1
  2. 从 dp[3] 开始往上推,循环内部第一刀 j 从 1 取到 i/2
  3. 内层循环保持 max 比较

2.4 DP 代码:Python 和 C++ 两种写法

Python 版本:

def integerBreak(n: int) -> int: if n == 2: return 1 if n == 3: return 2 dp = [0] * (n + 1) dp[2] = 1 for i in range(3, n + 1): # 先给 dp[i] 一个较低的下限 dp[i] = i - 1 # 最差是拆成 1 和 i-1 for j in range(1, i // 2 + 1): dp[i] = max(dp[i], j * (i - j), j * dp[i - j]) return dp[n]

C++ 版本:

class Solution { public: int integerBreak(int n) { if (n == 2) return 1; if (n == 3) return 2; vector<int> dp(n + 1, 0); dp[2] = 1; for (int i = 3; i <= n; i++) { dp[i] = i - 1; for (int j = 1; j <= i / 2; j++) { dp[i] = max(dp[i], max(j * (i - j), j * dp[i - j])); } } return dp[n]; } };

有同学会问:为什么 dp[i] 先初始化成 i - 1?因为 i 拆成 1 和 i-1 时乘积就是 i-1,这是最容易想到的拆分方案,可以当作保底值。即使后面所有 j 的组合都不如它大,也不会得 0 或者负数。

2.5 手推一遍 DP 过程

以 n = 6 为例:

  • dp[2] = 1
  • dp[3] = 2
  • dp[4] = 4
  • dp[5]:
    • j=1:1×4=4,1×dp[4]=4
    • j=2:2×3=6,2×dp[3]=4
    • 取 max = 6
  • dp[6]:
    • j=1:1×5=5,1×dp[5]=6
    • j=2:2×4=8,2×dp[4]=8
    • j=3:3×3=9,3×dp[3]=6
    • 取 max = 9

所以 n=6 最大乘积是 9,对应拆法 3+3。验算一下:3 × 3 = 9,正确。

这里能看到一层递进关系:dp[5] 时 j=2 提供 2×3=6,也就是“拆成 2 和 3”的想法;dp[6] 时 j=3 提供 3×3=9,是“拆成两个 3”。这个规律再往下观察,会引出数学解法。

3. 数学解法:为什么 3 是最优拆分数

3.1 从乘积增长率理解 2、3、4 的取舍

如果允许拆出任意多个正整数,且不考虑“整数”限制,用微积分可以知道:当所有拆分数尽量接近 e(约 2.718)时乘积最大。可惜这是整数场景,所以只能在 2 和 3 之间选,而且 3 比 2 更能带来乘积增长。

关键观察:6 拆成 3+3 的乘积是 9,拆成 2+2+2 的乘积是 8,拆成 4+2 的乘积是 8。为什么 3+3 更优?

因为对同一个固定和 k,多拆出一个 2 相当于把乘积乘以 2,但同时减少了一个“3”的位置。粗略估算:3 × 3 = 9,2 × 2 × 2 = 8,每多一个 2 都少了一个 3 的配对,收益会下降。所以尽量用 3 填满,只有剩下余数 1 时才会动 2 和 4。

3.2 数学推导与余数分类

把 n 拆成若干个 3 和一个余数 r(r ∈ {1, 2})。由于需要至少两个数,n 不能太小:

  • 如果余数 r = 2,直接用 2;
  • 如果余数 r = 1,说明拆成若干个 3 后多出一个 1。而单独的 1 放进乘积里没有增益,所以策略是把最后一个 3 拆成 2+1,与余数 1 合并成 2+2,整体乘积相当于把一个 3 换成了两个 2。因为 2×2 = 4 > 3,所以这个替换是赚的。

具体公式:

n % 3 == 0 -> pow(3, n / 3) n % 3 == 1 -> pow(3, n / 3 - 1) * 4 n % 3 == 2 -> pow(3, n / 3) * 2

n=5 验证:5 % 3 = 2,结果为 pow(3, 1) * 2 = 6。n=10 验证:10 % 3 = 1,结果为 pow(3, 2) * 4 = 36。都对得上。

3.3 数学解法的代码实现

def integerBreak(n: int) -> int: if n == 2: return 1 if n == 3: return 2 if n % 3 == 0: return 3 ** (n // 3) if n % 3 == 1: return 3 ** (n // 3 - 1) * 4 return 3 ** (n // 3) * 2

这个解法的时间复杂度是 O(n)(幂运算一般按 log n 计算),空间复杂度 O(1),明显优于 DP 的 O(n) 空间。

3.4 为什么 n=2 和 n=3 必须单独处理

有人会问:按公式走,n=2 时 n % 3 = 2,结果应该是 3^(0) * 2 = 2,但正确答案是 1。原因在于 2 必须拆成 1+1,乘积是 1,你不能“不拆”直接拿 2 当结果。

n=3 同理,3 % 3 = 0,按公式结果是 3,但 3 必须拆成 1+2,乘积是 2。要理解数学解法的前提是“拆分出的 3 和 2 本身不用再拆”,但题目强制至少拆成两个正整数,所以边界必须单独判断。

3.5 数学解法的严格证明思路

如果想在面试中把数学解法讲清楚,可以用“若存在大于 4 的拆分项,则可以继续拆分获得更大乘积”这条路:

  • 拆分数 k >= 5,则 (k - 2) * 2 = 2k - 4 >= k,所以 k 可以替换为 k-2 和 2;
  • 拆分数 4 等价于 2+2,因为 4 = 2+2 且乘积不变;
  • 因此最终只可能剩下 2 和 3;
  • 2 × 2 × 2 < 3 × 3,所以三个 2 应该替换成两个 3,也就是说 2 的数量最多不超过两个;
  • 剩余余数分类如上。

这样证明的优势在于每一步都直观,不会用到对数求导,适合面试口述。

4. 两种解法对比与 DP 结果观察

4.1 复杂度与实现难度对照

维度DP 解法数学解法
时间复杂度O(n^2)O(n)(幂运算按 log n 计)
空间复杂度O(n)O(1)
代码量较长极短
边界陷阱初始化易错2 和 3 必须特判
适合场景学习动态规划、扩展变形面试快速求答案

如果只为了过题,数学解法显然更快。但如果刷题目标是练 DP,这个题的 DP 递推不复杂,可以用它来理解“枚举第一刀 + 子问题复用”的思路,对后面的切绳子、戳气球、分裂回文串都有帮助。

4.2 从 DP 数组观察规律

把 n=2 到 n=12 的 dp 值列出来:

ndp[n]最佳拆法
211+1
321+2
442+2
562+3
693+3
7123+4
8183+3+2
9273+3+3
10363+3+4
11543+3+3+2
12813+3+3+3

能看到两个现象:

  1. 从 n=5 开始,dp 值就开始被“一堆 3 加上少量 2 或 4”主导;
  2. 规律跳跃明显:n 越大,比值越接近 ×3,除非遇到余数 1 时需要调整。

拿 11 来说,拆成 3+3+3+2,乘积 3×3×3×2=54;拆成 3+3+3+1+1 是 27;拆成 4+4+3 是 48。可见“把 1 合并成 4”确实很关键。

4.3 什么时候优先用 DP,什么时候优先用数学

面试官问这道题,意图一般不是让你 5 秒秒杀,而是想听你如何从 DP 过渡到数学优化。建议流程:

  1. 先给出 DP:直观、通用、容易证明正确性;
  2. 再提出数学观察:从 dp 表中看出 3 的主导地位,进而推导公式;
  3. 最后写数学解法,并说明 2、3、4 的边界处理。

如果是竞赛或刷题网站里赶时间,直接用数学法 5 行代码收工即可。如果目标是掌握动态规划,DP 过程写一遍比背公式更有价值。

5. 实操经验:调试、边界与扩展思考

5.1 n 很小时的特判是最大坑点

几乎所有提交错误都出在 n = 2 和 n = 3 上。很多人写完 DP 后以为 dp[2] = 1 就够了,结果 n=3 时进入内层循环,j 从 1 到 1,算出来 dp[3] = max(1×2, 1×dp[2]) = 2,其实没问题。但如果初始化 dp[i] = i,且没有特判 3,就会跑出 3 这个错误答案。

还有一种写法是 dp 数组长度开到 n+1 后,把 dp[1] 和 dp[2] 都设置成 1,然后从 i=3 开始。这样也能过,但语义上 dp[1] 其实用不到,建议不要为了省两行代码模糊了定义。

5.2 幂运算的溢出问题

题目在 LeetCode 上通常保证答案在 32 位整数内,但如果你在其他平台上看到 n 范围很大,比如 n 到 100 或更大,直接用 3 ** (n // 3) 可能会溢出。

应对策略:

  • 如果要求返回 int,可以直接用 int 运算,因为 3 的幂增长特别快,越界会变成负数或错误值;
  • 如果题目要求取模,比如对 1e9+7 取模,需要写快速幂或者循环取模;
  • 如果面试时讨论大数场景,要主动提到 BigInteger 或取模方案。

我在实际写代码时,会先确认数据范围,再决定要不要写快速幂。一上来就写快速幂确实显得专业,但如果题目明明只有 n=58,反而画蛇添足。

5.3 数学解法的实现细节:除法取整要小心

计算 3 的幂时,n // 3 是整数除法。但在 “n % 3 == 1” 的分支里,如果 n=4,n // 3 - 1 = 0,所以结果是 3^0 * 4 = 4,正确;n=1 时 n // 3 - 1 = -1,但 n=1 本身不是合法输入,不用管。

还有一个小坑:当 n 很大时,如果写pow(3, n / 3)在 C++ 中会用浮点数,可能因为精度问题在边界出错。正确的写法是用整数版pow(3, n / 3)或者手写循环乘法,不要用pow(3, n / 3.0),否则在超大数场景下可能得到不精确结果。

5.4 从这道题延伸到“切绳子”类问题

LC.343 的本质和“剪绳子”问题一模一样,只是场景不同。很多游戏数值策划和资源分配问题也会遇到类似建模:固定总量,拆成多份让总收益最大。

解决办法是可以抽象成:

  1. 定义子问题“总量剩余 i 时的最大收益”;
  2. 枚举第一段分配的份量;
  3. 子问题复用 dp 结果。

遇到需要“至少切一刀”“必须分成至少 k 份”的变种时,只要在初始化边界和状态定义里加一个维度即可。比如“必须分成至少 k 段”时,dp 需要变成二维:dp[i][k] = max(dp[i - j][k - 1] * j),这类扩展在面试题里也出现过,值得多练。

5.5 面试时怎么把一个简洁解法讲得有层次

如果面试官要求“给出最优解”,建议这样组织回答:

  • 第一步:说明从枚举角度,乘法会鼓励更大的拆分数;
  • 第二步:说明 3 是最优拆分的核心原因——2×2×2 < 3×3;
  • 第三步:讨论余数分类,补全数学公式;
  • 第四步:写代码,确认 n = 2、3、4 这几个边界。

其中第二步很关键,你可以画几个数感受一下:用 3 拆出来的乘积曲线上升得更快。面试官一般不会要求证明 e,但会希望看到对 2 和 3 的讨论。

5.6 我在实际练习中的两点体会

刷完这道题,我最大的感受有两点。

第一,数学解法和动态规划不要二选一。前几次刷题我只看 DP,虽然能过,但总觉得少了点“更底层”的理解。后来认真推导数学版本,再回头理解 dp 表,才明白为什么 j 枚举到 i/2、为什么 j=3 附近通常是最大值。两条路相互验证,比单纯背公式牢固很多。

第二,做算法题不要忽略数据范围。LC.343 这种小范围题用 DP 或者纯数学都没问题,但如果换成分治、矩阵快速幂或者大数取模场景,就得重新审视实现。多问自己一句“这个 n 最大能到多少”,能避开很多后续扩展问题。

6. 总结一下我在这次解题中的收获

说句实在话,这题难度本身不高,但它特别适合用来练习“从动态规划抽象到数学规律”的思维过程。很多 DP 题解直接给出 dp 递推式就算了,但这道题你只要停下来多看两眼 dp 表,就会发现一堆 3 的身影,进而推导出 O(1) 空间的最优解。

我建议初学者先手算 n=2 到 n=12 的 dp 值,形成对“乘积增长”的直觉,再去看数学解法的余数分类。这样比直接背公式有效得多。等熟悉了这套流程,以后碰到任何“切分求最大收益”的题,都能从 DP 起步、用数学优化收尾,整个思考路径会非常顺。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 14:45:35

告别Nginx和虚拟机:用Azure App Service快速部署Flask项目

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 13:20:33

守住轻薄本的冰凉与宁静:轻量大模型日常写作选型与调优全景复盘

十月十四日的下午&#xff0c;阳光透过窗外的百叶窗&#xff0c;在铝合金笔记本的外壳上切出两道温润的光斑。我把手指贴在键盘上方的金属面板上&#xff0c;触感温凉如玉&#xff1b;机身内部的风扇全程沉睡&#xff0c;只有音响里正流淌着一段安静的弦乐四重奏。 在过去整整两…

作者头像 李华
网站建设 2026/10/19 11:56:19

基于MP2642GR-Z与STM32F429ZI的串联电池主动均衡方案复盘

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 13:34:32

MBRL评测基准缺失与混乱:统一框架下的算法对比与复现指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 11:24:30

2S电池包主动均衡设计:MP2642GR-Z与PIC18F97J94软硬件协同

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 16:42:21

Bilibili m4s视频合并MP4的三大核心问题与解决方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华