题面
给你一根长度为
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。答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。
示例 1:
输入: 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1
示例 2:
输入: 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36
提示:
2 <= n <= 1000
分析
该题在第14题“剪绳子”的基础上,增加了大数取余部分,下面探讨C++的4种解题思路:
动态规划、数学推导、数学推导优化、贪心。
最后会再次印证一句古老的话:艺术与科学终将在某一点相遇
1 动态规划(行不通)
在第14题代码基础上,每次从第i-1个状态计算第i个状态时,加入取余运算,会得出错误的结果。理由如下:
动态规划的特点是,第i个状态依赖于第i-1个状态,最后一个状态(目标状态)依赖于倒数第二个状态,如果在状态转移间进行取余,会出现有的转移需要取余,有的转移不需要取余:
假设a和b是两个数,a>b
,a需要取余,b不需要取余,a取余之后的值为a1,可能出现a1<b
的情况,这就会误导后面的状态转移,最终导致错误的结果。
这是无法得出正确结果的动态规划的代码,注意,为了不产生溢出,需要将dp数组和若干中间值改成long类型(详见代码注释):
class Solution {
public:
int cuttingRope(int n)
{
int base = 1000000007;
if(n==2)
{
return 1;
}
if(n==3)
{
return 2;
}
// 将dp数组改成long类型
long dp[n+1][n+1];
for(int i=0; i<n+1; i++)
{
for(int j=0; j<n+1; j++)
{
dp[i][j] = -1;
}
}
for(int i=0; i<n+1; i++)
{
dp[i][i] = 1;
dp[i][1] = i;
}
//dp[i][j],长度为i的绳子,切成j段,乘积的最大值
for(int tmp=3; tmp<=n; tmp++)
{
int now_len = tmp;
for(int sp=2; sp<now_len; sp++)
{
int split = sp;
// 将x1改成long类型
long x1 = 1;
// 将dp数组候选值tmp_max改成long类型
long tmp_max = -1;
bool overflow = false;
while((x1<now_len) && (now_len-x1)>=(split-1))
{
// 将乘积multi改成long类型
long multi = (x1*dp[now_len-x1][split-1]);
if(!overflow){
if(multi>base){
multi = multi%base;
tmp_max = multi;
overflow = true;
}
else{
if(multi>tmp_max){
tmp_max = multi;
}
}
}
else{
if(multi>base){
multi = multi%base;
if(multi>tmp_max){
tmp_max = multi;
}
}
}
x1++;
}
dp[now_len][split] = tmp_max;
}
}
int final_max = -1;
for(int p=1; p<=n; p++)
{
if(dp[n][p]>final_max)
{
final_max = dp[n][p];
}
}
return final_max;
}
};
2 数学推导
务必先看此处的数学推导过程。
要点如下:
- 对n%3的值进行分类讨论
- 写出一个递归求余的函数
代码如下(注释部分即为思路):
class Solution {
public:
//递归求余的函数
long figure(int k){
//特判,边界条件
if(k==0){
return 1;
}
//特判,边界条件
if(k==1){
return 3;
}
//注意temp的值必须为long,否则leetcode上会报溢出的错误
long temp = (3*figure(k-1));
return temp%1000000007;
}
//分类讨论的函数
int cuttingRope(int n) {
//特判,n==2时返回1
if(n==2){
return 1;
}
//特判,n==3时返回2
if(n==3){
return 2;
}
//利用整除向下取整的特性,不管n%3是多少,k总是我们需要的值
int k = n/3;
long res = 0;
if(n%3==0){
//把绳子分割成k段,每段长度为3
res = figure(k)%1000000007;
}
else if(n%3 == 2){
if(k==0){
//此处只能是n=2
res = 1;
}
else{
long mid = (figure(k))*2;
res = mid%1000000007;
}
}
else{
if(k==0){
//此处只能是n=1,其实这一句可以不用,因为题目要求n>1
res = 1;
}
else{
long mid = (figure(k-1))*4;
res = mid%1000000007;
}
}
int result = res;
return result;
}
};
提交结果:
3 数学推导优化
上面的代码,可以用四个字概括它的特点:又臭又长
一堆if-else语句,这样写代码到了后期很难维护。
进一步推导,完全可以
对这根绳子一次切3个长度,一次切3个长度,当剩余长度小于某个值时,再分类讨论。
代码如下(注释部分即为思路):
class Solution {
public:
int cuttingRope(int n) {
int base = 1000000007;
//res的类型必须设置为long,否则会溢出
long res = 1;
//特判
if(n<3){
return 1;
}
//特判
if(n==3){
return 2;
}
//这里必须是n>5,因为n%3的余数可能是0、1、2
//0是我们最想要的结果,不用考虑特殊情况
//n%3=1时,最后一次切3之前,绳子长度为4,这时我们应该把4分成2+2,而不是1+3,因为(2*2)>(1*3)
//n%3=2时,最后一次切3之前,绳子长度为5,这时我们应该把5分成3+2
//所以当绳子目前的长度大于5时,可以随便切3,但是当等于5或者小于5时,就要分类讨论了
while(n>5){
res = (res*3)%base;//循环取余,防止溢出
n = n-3;
}
int fi;
//最后一次切3之前,绳子长度为5,这时我们应该把5分成3+2,3*2=6,故fi=6
if(n==5){
fi = 6;
}
//最后一次切3之前,绳子长度为4,这时我们应该把4分成2+2,2*2=4,故fi=4
if(n==4){
fi = 4;
}
//最后一次切3之前绳子长度为3,我们不应该分这根绳子,故fi=3
if(n==3){
fi = 3;
}
return (res*fi)%base;
}
};
提交结果:
4 贪心
上面的代码,简洁了很多,但是,从提交效果上看,和没有优化之前并无差别,为什么呢?
注意到,上面的代码在while循环之后,有3个if判断语句,编译器在底层,会对if语句的走向,进行猜测,但if的判断都是==判断,所以,编译器此时的猜测毫无意义,会浪费很多时间。
再去看一看上面的代码,我们会发现,当n<4的时候,返回的都是n-1;当n=4的时候,返回的是4。当n=5的时候,就要进行常规切3操作。
好,再去看上面代码中最后一部分:
while(n>5){
res = (res*3)%base;//循环取余,防止溢出
n = n-3;
}
int fi;
//最后一次切3之前,绳子长度为5,这时我们应该把5分成3+2,3*2=6,故fi=6
if(n==5){
fi = 6;
}
//最后一次切3之前,绳子长度为4,这时我们应该把4分成2+2,2*2=4,故fi=4
if(n==4){
fi = 4;
}
//最后一次切3之前绳子长度为3,我们不应该分这根绳子,故fi=3
if(n==3){
fi = 3;
}
return (res*fi)%base;
while循环的条件:n>5
。如果:
我们把它改成n>4
,去掉while循环后面的3个if语句,将最后一行return (res*fi)%base;
改成return (res*n)%base;
,我们会发现,再最终结果上,不会有任何变化,为什么不会有任何变化呢?继续分析:
while循环退出时,n有以下几种情况:
- n=4,最后一次切完3,绳子长度为4,这时我们应该把4分成2+2,2*2=4,恰好就是绳子本身的值。
- n=3,最后一次切完3,绳子长度为3,这时我们不应该分这根绳子,保持绳子本身的值。
- n=2,最后一次切完3,绳子长度为2,说明最后一次切3之前,绳子的长度为5,对于5,我们应该把5分成3+2,而2恰好就是目前绳子本身的值。
综上所述,优化代码如下:
class Solution {
public:
int cuttingRope(int n) {
int base = 1000000007;
long res = 1;
if(n<4){
return n-1;
}
if(n==4){
return 4;
}
while(n>4){
res = (res*3)%base;
n = n-3;
}
return (res*n)%base;
}
};
提交结果:
可以发现,执行时间显著降低了,因为while循环后面没有那一大堆if判断了。
如果有好好看此处的数学推导,会发现,上面的代码,完全可以按照贪心法的逻辑去理解。
艺术与科学终将在某一点相遇
Good luck!