【JZ67】给你一根长度为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。
知识点:递归,动态规划
难度:☆☆
题目蕴含了两个问题:分成多少段+每段多长。分段数量和每段长度都直接影响乘积,因此,没办法按照直觉的方法找出因果关系。
由题意可知,n和m的最小取值为2。
当n=2时,分段策略可以是{(1,1)}
,此时乘积对应为{1}
;
当n=3时,分段策略可以是{(1,1,1),(1,2)}
,此时乘积对应为{1,2}
;
当n=4时,分段策略可以是{(1,1,1,1),(2,1,1),(2,2)}
,此时乘积对应为{1,2,4}
;
题目要求是最大乘积,因此,我们尽量不会考虑平均分成每一段都为1的情况,尽量排除这个情况,那么就变为:
当n=2时,分段策略只能是{(1,1)}
,此时乘积对应为{1}
;
当n=3时,分段策略可以是{(1,2)}
,此时乘积对应为{2}
;
当n=4时,分段策略可以是{(1,1,2),(2,2)}
,此时乘积对应为{2,4}
;
也就是说,当n从4开始,乘积结果就不唯一了,需要各种策略讨论了。
当n大于等于4时,如果我第一段长度固定为1,那么此时要得出最大乘积,只能靠后面n-1长度的绳子进行某个策略分段达到最大乘积。同理,如果我第一段绳子长度固定为2,此时要得到最大乘积也只能靠后面n-2长度的绳子进行某个策略分段达到最大乘积。
因此,当n大于等于4时,我们可以定义一个递归函数int backTrack(int n)
,这个函数的功能是:求长度为n的数,最后分段后的最大乘积。
对于长度为n (n>3)
的绳子:
如果第一段长度为1,则最大乘积为:1*backTrack(n-2)
;
如果第一段长度为2,最大乘积为:2*backTrack(n-3)
;
。。。。。。
如果第一段长度为n-1,最大乘积为:(n-1)*backTrack(1)
。
代码见本章3.1小结。
见3.1小结的代码,其实代码存在大量的重复运算上面,比如绳子长度为7时:
其中f(n)为backTrack()
函数,红色部分就是重复计算,我们可按照动态规划的思想,对每一个f(i)进行缓存并复用。
动态规划的思路就是:建立状态转移方程f(n)
、缓存并复用以往结果和按顺序从小往大算。
其中最关键的就是建立状态转移方程f(n)
,由上面分析可知:
当n=2时,最大乘积为1;
当n=3时,最大乘积为2;
当n从4开始,就有多个不为1的乘积结果,我们定义一个函数f(n)
,作用是:对于输入的绳子长度n,如果不分段则返回本身,若分段,从所有的分段策略中,返回每段相乘结果最大的那个最大乘积。
即我们的状态转移方程f(n)
并不是题目要的在n>2且m>2下的最大乘积,而是n>0且m>0下的最大乘积。
已知:
X1 + X2 + … + Xm = n(n和m均为正整数,且n>1,m>1),
S(n) = X1 * X2 * … * Xm
求:maxS(n)
解:
∵ S(n) = X1 * X2 * … * Xm ≤ (X1 + X2 + … + Xm)2/4
当且仅当 X1 = X2 = … = Xm时等号成立
∴ 当平均分成m段时,S(n)的值最大
设 X1 = X2 = … = Xm = X
则 maxS(n) = Xm
又∵ mX = n
∴ maxS(n) = Xn/X
对 maxS(n) 求导,得:maxS`(n) =
可以看出,当 1-ln(x) = 0时, maxS(n)有最大值
解得:x = e
又∵ x为整数,取x = 3
当 n = 2时,maxS(n) = 2
当 n = 3时,maxS(n) = 3
当 n > 3时,分为以下三种情况:
n刚好被3整除:maxS(n) = 3n/3
n除以3还余1:maxS(n) = 3n/3-1 * 4
n除以3还余2:maxS(n) = 3n/3 * 2
至此,整个数学方法流程分析完毕。
package pers.klb.jzoffer.medium;
/**
* @program: JZoffer
* @description: 剪绳子(暴力递归)
* @author: Meumax
* @create: 2020-06-18 10:35
**/
public class CutRope {
public int cutRope(int target) {
if (target == 2) {
return 1;
} else if (target == 3) {
return 2;
} else {
return backTrack(target);
}
}
private int backTrack(int n) {
if (n <= 4) {
return n;
} else {
int result = 0;
for (int i = 1; i < n; i++) {
result = Math.max(result, i * backTrack(n - i));
}
return result;
}
}
}
时间复杂度:O(n2)
空间复杂度:O(n)
package pers.klb.jzoffer.medium;
/**
* @program: JZoffer
* @description: 剪绳子(记忆化递归)
* @author: Meumax
* @create: 2020-06-18 10:35
**/
public class CutRope {
public int cutRope(int target) {
if (target == 2) {
return 1;
} else if (target == 3) {
return 2;
} else {
// 用于保存不同target下的最大乘积
int[] mark = new int[target + 1];
// 初始化每一项为-1
for (int i = 1; i <= target; i++) {
mark[i] = -1;
}
return backTrack(target, mark);
}
}
private int backTrack(int n, int[] mark) {
if (n <= 4) {
return n;
}
// 如果这个值已经计算过,就不用再计算,直接返回
if (mark[n] != -1) {
return mark[n];
} else { // 如果mark[n]没有计算过,那就计算
int result = 0;
for (int i = 1; i < n; i++) {
result = Math.max(result, i * backTrack(n - i, mark));
}
// 保存最大乘积到数组中
mark[n] = result;
return result;
}
}
}
时间复杂度:O(n2)
空间复杂度:O(n)
package pers.klb.jzoffer.medium;
/**
* @program: JZoffer
* @description: 剪绳子
* @author: Meumax
* @create: 2020-06-18 10:35
**/
public class CutRope {
public int cutRope(int target) {
switch (target) {
case 2:
return 1;
case 3:
return 2;
default:
// 定义一个数组f用于保存每一个target情况下的最大乘积(可以不分段)
int[] f = new int[target + 1];
// 初始化为-1
for (int i = 0; i <= target; i++) {
f[i] = -1;
}
// 长度为1、2、3、4时最大乘积就是其本身
for (int j = 1; j <= 4; j++) {
f[j] = j;
}
// 从小到大,依次计算出n很大时的最大乘积
for (int i = 5; i <= target; i++) {
// 当绳子总长度为i,第一段长度为j时,最大乘积为 j * f[i - j]
for (int j = 1; j < i; j++) {
f[i] = Math.max(f[i], j * f[i - j]);
}
}
return f[target];
}
}
}
时间复杂度:O(n2)
空间复杂度:O(n)
package pers.klb.jzoffer.medium;
/**
* @program: JZoffer
* @description: 剪绳子
* @author: Meumax
* @create: 2020-06-18 10:35
**/
public class CutRope {
public int cutRope(int target) {
if (target == 2) {
return 1;
} else if (target == 3) {
return 2;
}
if (target % 3 == 0) {
return (int) Math.pow(3, target / 3);
} else if (target % 3 == 1) {
return 4 * (int) Math.pow(3, target / 3 - 1);
} else {
return 2 * (int) Math.pow(3, target / 3);
}
}
}
时间复杂度:O(1)
空间复杂度:O(1)
这道题对于新手来说着实有点难度,因为分析比较长,且不直观。做这种题,实在没办法的情况下可以从最小值开始,列出来,可能就看出规律能初步写出递归,然后再进行优化。
最难想到的就是动态规划的状态转移方程,总结的结论就是,当我们的状态转移方程起始条件不是从0开始,往往需要一些额外处理。
最后,如果能把一个编程题转成标准的数学题,也是厉害的了,其实大部分可以动态规划的题目,往下继续找规律,可以找到更简单的解析式。但是还是符合那个原理:人类看的明白的,计算机执行效率低,人类看不明白的,计算机执行效率高。