从本案例中有助于理解动态规划中的重叠子问题和最优子结构。
题目:
给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。
示例 1:
输入: 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
示例 2:
输入: 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。
思路分析
这题先从暴力解法上入手,对数字不断分割,从1开始,直到 (n-1) 不能再分割为止。
拿4这个数字来分析,分割如下:
第一种情况,f(4) = 1 * f(3),分割成 1 与 3 的乘积最大值
继续分割3
第一种情况,f(3) = 1 * f(2),分割成1 与 2 的乘积最大值
继续分割2
f(2) = 1 * f(1),分割成1与1的乘积最大值
第二种情况,f(3) = 2 * (1),分割成2与1的乘积最大值
第二种情况,f(4) = 2 f(2),分割成 2 与 2 的乘积最大值
继续分割2
f(2) = 1 f(1),分割成1与1的乘积最大值
第二种情况,f(4) = 3 * f(1),分割成 3 与 1 的乘积最大值
1不能再分割了
上述分析中,到1的时候便停止分割,1不能分割成别的数了。
通过示意图来看分析过程,实际上就是一棵树的结构:
如果是 n 的数,则把 4 替换成 n 即可:
**
通过画成树形结构可以清晰的看到拆分过程。
解题可以选择自上而下的递归思路,把大问题拆分成一个个小问题,枚举每一种可能性。其中标记为蓝色的部分的块在重复计算,归为 重叠子问题 ,可采用「备忘录」(记忆化搜索)把算好的结果缓存起来直接使用。
代码如下:
/**
* @param {number} n
* @return {number}
*/
var integerBreak = function(n) {
if(n === 1) return 1
let cache = {}
// 对整数n进行分割两个数,求获得的最大乘积
const loop = (n) => {
if(n === 1) return 1;
// 如果已求值,则直接返回
if(cache[n]) return cache[n]
// 结果
let res = -1
for(let i = 1; i <= n - 1; i++){
// 找最大值
res = Math.max(res, i * (n - i), i * loop(n - i))
}
// 计算后,缓存起来
cache[n] = res
return res
}
return loop(n)
};
通过上面的代码,还有所画的树形示意图,每次递归调用函数进行循环,循环是树中每一层的拆分,从每一层中找到乘积最大值,就可以找到最终为的乘积最大值。
值得注意是这段代码:
res = Math.max(res, i * (n - i), i * loop(n - i))
为什么是三个值比较呢?
res 是存的最大值,在循环过程中不断要去和其他值比较,从而再次确定最大值,直到循环结束。
i * (n - i) 是每次分割成两个数的乘积,在题目要求中是可以分割成两个数的,所以不能忽视分割成两数的乘积
i * loop(n - i) 分割成 n-i 这个节点的乘积。
动态规划实现
通过上述的分析和代码可以看出,不断的拆分成子问题,然后在这些子问题中找到最优解,通过找到子问题的最优解,也就解决了原问题的最优解。并且包含着重叠子问题,我们可以采用动态规划自下而上的方式实现,只用循环不用递归。
思路就是从1开始,不断的趋向求n的值,在这个过程中,不断的对其中的数字进行拆分.
代码
var integerBreak = function(n) {
if(n === 1) return 1
let cache = {}
cache[1] = 1 //
// 求出从 1 → n 之间每个数的结果
for(let i = 2; i <= n; i++){
// 求解 i 的值
// 把 i 进行从 1 到 i-1 的分割
for(let j = 1; j <= i - 1; j++){
// 分割成两部分 j + ( i - j )
cache[i] = Math.max( cache[i] || -1, j * (i-j), j * cache[i-j])
}
}
return cache[n]
}
cache 是存储从 1 - n 之间每个数字求出来的结果。
1 的结果就是1 ,不需要计算;循环从2开始。
第一层循环 i 拿到 1 - n 之间的每一个数字,例如4,拿到的就是 2,3, 4。求出每一个数字的结果。
第二层循环用来拆分每一个数字,从 1 开始,到 i - 1 ,例如 i 循环到的数字是3时,要拆分为 1,2。
未完待续….