【剑指Offer-14】剪绳子
问题
给你一根长度为n的绳子,请把绳子剪成整数长度的m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1]...k[m-1]
。请问k[0]*k[1]*...*k[m-1]
可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
示例
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
解答1:递归
class Solution {
public:
int cuttingRope(int n) {
if (n < 3) return n - 1;
int res = 0;
for (int i = 1; i < n; i++)
res = max(res, max(cuttingRope(n - i) * i, (n - i) * i));
return res;
}
};
重点思路
我们先把一根绳子分成两半,具体比例由for循环中的i控制。然后再考虑是否将这两段继续拆分下去,所以有max(cuttingRope(n - i) * i, (n - i) * i)
这一句代码。
解答2:带记忆递归
class Solution {
public:
int memo[59] = {0}; // 题目中要求n不超过58
int cuttingRope(int n) {
if (memo[n]) return memo[n]; // 读取到已计算中间值则直接返回
if (n < 3) return n - 1;
int res = 0;
for (int i = 1; i < n; i++)
res = max(res, max(cuttingRope(n - i) * i, (n - i) * i));
memo[n] = res;
return memo[n];
}
};
重点思路
朴素的递归会超时,由于在本题递归中有大量重复计算,我们对中间值使用memo
进行存储。
解答3:动态规划
class Solution {
public:
int cuttingRope(int n) {
int dp[59] = {0, 1, 1};
for (int i = 1; i <= n; i++)
for (int j = 1; j < i; j++)
dp[i] = max(dp[i], max(dp[i - j] * j, (i - j) * j));
return dp[n];
}
};
重点思路
dp[i]
记录长度为i
的绳子的乘积最大值。状态转移方程与递归中的形式不能说是基本相同,只能说是完全一致。实际上,带记忆的递归算法就是动态规划的另一种表现形式,本解法的dp数组与上一个解法的memo数组存的东西是相同的。
解答4:数学方法
class Solution {
public:
int cuttingRope(int n) {
if (n < 4) return n - 1;
int a = n / 3, b = n % 3;
if (b == 0) return pow(3, a);
if (b == 1) return pow(3, a - 1) * 4; // 余数为1的情况
return pow(3, a) * 2;
}
};
重点思路
要使用数学方法,可以得到2个证明:
- 长度一定时,每段长度相同时乘积最大。均值不等式证明即可;
- 当每段长度一定时,每段长度为3时能得到最大。设绳总长度为\(a\),则乘积为\(x^{\frac{a}{x}}\),所以我们只需要计算\(y=x^{\frac{1}{x}}\)的最大值即可。求导计算得到极大值点为\(x=e\),带入求得\(x=3\)时,取的最大值。
当余数为1时,我们相当于把长度为4的绳子切割为了1和3,这显然小于2和2,所以对于该情况我们特殊讨论。