题目来源
题目描述
class Solution {
public:
int integerBreak(int n) {
}
};
题目解析
动态规划:推导状态转移方程
先仔细分析题目:
- 要求是:正整数 拆分 为 【正整数 + 正整数 + 正整数】
- 正整数包括:1、2、3、…
- 最少要拆成两个数(k >=2 )
- 那么,对于一个数,最多可以拆解成多少个数字呢?
- 对于3 = 1 + 1 + 1、4 = 1 + 1 + 1 + 1
- 因此,对于一个正整数N,最多可以拆解成N个1
- 也就是说: 对于正整数N,k的范围为:[2, N];而N,最小值为2
思路:
- 对于正整数n,当n >= 2时,可以拆分成至少两个正整数的和。
- 令k是拆分出的第一个正整数,则剩下的部分是n - k。可以不继续拆分,或者继续拆分成至少两个正整数的和(一个问题可以分解为相似的子问题因此想到动态规划)。
- 由于每个正整数对应的最大乘积取决于比它小的正整数对应的最大乘积,因此可以使用动态规划求解。
问题建模
(1)第一个问题:假设当前我们站在终点4,那么对于4,假设只需要拆解最后一步了没那么会有几种情况呢? 终点4,必须要拆分
-
4可能是从3来的,也就是最后只剩下了1,1不可以拆解,即
f(4) = 1 + f(3)
。 -
4可能是从2来的,也就是最后剩下了2,这时
f(4) = f(2) + f(2)
-
4可能是从1来的,也就是最后剩下了3,1不可以拆解,即
f(4) = 1 + f(3)
。 -
分析:
- 我们先单拿出一个情况分析:对于
f(4) = 1 + f(3)
,要知道拆解最大乘积,设为g(4) = 1 * g(3)
,如果g(4)
要最大,那么必须保证g(3)
得到的乘积最大,所以它符合动态规划的要求 - 上面的变化维度: 要拆成几个数、拆出来的数的最大乘积
- 最大乘积是目标,所以只有一个变化维度,因此定义一个一维数组dp(i)就可以表示
- dp[i]表示,对于正整数i,最少拆解为两个正整数时的最大乘积。
- 要拆成几个数会影响dp[i]的入参
- 我们先单拿出一个情况分析:对于
(2) 分析子问题:
- 为了解决自己的问题,它需要给别人制造另外两个问题,这两个问题就是子问题。
- 从上面我们可以看到,为了解决拆解4得到的最大乘积,它给另外三个人分别制造了3个问题
- 这3个问题的本质是一样的:对于正整数N,请将之拆解/不拆解为正整数之和,并返回得到的正整数们的最大乘积(之所以可以不拆解,是因为终点已经拆解了一次,剩下的部分可以选择拆解或者不拆解了;)
(3)最优子结构:
- 我们要从它制造出的子问题中选择出一个最好的答案出来(而且各个子问题之间是相互独立的),所以它符合最优子结构。
- 对于正整数n,它的最优子结构是什么呢?
- 我们知道,最少拆解2次,最多可以拆解n次
- 从上面可以可以看到,它一共有[2…n - 1]个最优子结构。
- 因此我们需要遍历[2…n - 1],假设遍历到i,那么最优子结构可以表示为:
- F(n - 1)时的最大乘积
- F(n - 2)时的最大乘积
- …
- F(2)时的最大乘积
(4)推导状态转移方程
- 已经将正整数n拆解为
i
,那么剩下的部分(n - i)
- 对于
(n - i)
,可以不拆分了 (因为要求拆解为两个以上的,如果我们只拆解为两次,那么现在已经拆解了一次了,剩下的只有一次了,不可以再次拆解了,所以要单独拿出来说),那么此时的最大乘积为i * (n - i)
- 对于
(n - i)
,还可以继续拆分,因此最大乘积为i * dp(n - i)
- 对于
(5)dp初始化
- 严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。
- 这里只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1
代码实现
- 确定dp数组以及下标的含义
dp[i]
表示将正整数i
拆分成至少两个正整数的和之后,这些正整数的最大乘积
- 确定状态转移方程
- 当i >= 2时,假设对正整数i拆分来的第一个正整数是j(1≤j<i),则有以下两种方案:
- 将i拆分成j和i - j的和,而且i - j不再拆分成多个正整数,此时的乘积为j * (i - j)
- 将j拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j×dp[i−j] 。
- 因此,将
j
固定时,有dp[i] = max(j * (i - j), j * dp[i - j])
。此时j
的取值范围为1
到i - 1
,需要遍历所有的j
得到dp[i]
- 当i >= 2时,假设对正整数i拆分来的第一个正整数是j(1≤j<i),则有以下两种方案:
- dp数组怎么初始化?
- 0不是正整数、1是最小的正整数,0和1都不能拆分,因此
dp[0] = dp[1] = 0
- 0不是正整数、1是最小的正整数,0和1都不能拆分,因此
- 遍历顺序:
- 由状态转移方程知道dp[i] 是从
j×(i−j)
和j×dp[i−j]
且j 的取值范围是 1 到 i−1 ,需要遍历所有的 j 得到dp[i]所以从前往后遍历。
- 由状态转移方程知道dp[i] 是从
- 返回值:
- 最终得到dp[n]的值即为将正整数n拆分成至少两个正整数的和之后,这些正整数的最大乘积。
- 举例推导dp数组
class Solution {
public:
int integerBreak(int n) {
std::vector<int> dp(n + 1);
dp[2] = 1;
for (int i = 3; i <= n; ++i) { // 从下向上推导,直到推导到n
int max_dp = 0; // 从最优子结构中选出一个最佳的
for (int j = 1; j < i ; ++j) { // 对于每一个i,它的最优子结构有[1......i - 1] 。
max_dp = std::max(max_dp, std::max(j * (i - j), j * dp[i - j]));
}
dp[i] = max_dp ;
}
return dp[n];
}
};