剪绳子
题目:给你一根长度为n的绳子,请把绳子剪成m段(m,n都是整数,并且m>1,n>1),每段绳子的长度记为k[0],k[1],···,k[m]。
请问k[0]×k[1]×···×k[m]可能的最大乘积是多少?例如:当绳子长度为8时,我们把它剪成长度2,3,3的三段,此时得到的最大乘积是18。
我们有两种不同的方法解决这个问题,先用常规的O(n²)时间和O(n)空间的动态规划的思路,接着用只需要O(1)时间和空间的贪婪算法来分析解决这个问题。
动态规划
首先定义函数f(n)为把长度为n的绳子剪成若干段后各段长度的乘积最大值,在剪第1刀的时候,我们有n-1种长度可能的选择,也就是剪出来的第一段绳子的可能长度分别为1,2,···,n-1。因此f(n)=Max(f(i)*f(n-i)),其中0<i<n。这是一个从上至下的递归公式,中间有大量不必要的计算,更好的办法是从下而上的方式,想不清楚的时候可以画图,或者穷举一些简单的数据作为辅助如下表:
绳子长度n | 切割次数I(0~n) | 长度乘积穷举 | 最大乘积 |
---|---|---|---|
2 | 1 | 1x1 | 1 |
3 | 1 | 1x2 | 2 |
2 | 1x1x1 | ||
4 | 1 | 1x3,2x2 | 4 |
2 | 1x1x2 | ||
3 | 1x1x1x1 | ||
5 | 1 | 1x4,2x3 | 6 |
2 | 1x1x3,1x2x2 | ||
3 | 1x1x1x2 | ||
4 | 1x1x1x1x1 |
public static int maxCordCut(int length) {
//长度小于2,没法下刀
if (length < 2) {
return 0;
}
//长度等于2,只能切1刀,每段最小长度为1
if (length == 2) {
return 1;
}
//长度等于3,可以切2刀,乘积为2,切3刀,每段为1,乘积还是1
if (length == 3) {
return 2;
}
//穷举下长度为4,每段绳子的可能刀长度值长度值
int ans[] = new int[length + 1];
ans[0] = 0;
ans[1] = 1;
ans[2] = 2;
ans[3] = 3;
int max = 0;
//长度2,3已知,长度从4开始算
for (int i = 4; i <= length; ++i) {
//切多少刀
for (int j = 1; j <= i / 2; ++j) {
//计算乘积,比较大小
int result = ans[j] * ans[i - j];
if (max < result) {
max = result;
}
ans[i] = max;
}
}
max = ans[length];
return max;
}
贪婪算法
如果我们按照如下的策略剪绳子,则得到的各段绳子的长度将最大:
当n≥5时,我们尽可能的多剪长度为3的绳子,当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子,这种思路的数学逻辑是这样的,当n≥5时,我们可以证明2x(n-2)>n并且3x(n-3)>n 和3x(n-3)>n≥2x(n-2);因此我们应该尽可能的多剪长度为3的绳子,那么当绳子的长度为4呢?在长度为4的绳子上剪一刀,那么只有两种可能1x3,2x2;且2x2>1x3。上代码:
public static double maxCordCutNew(int length) {
//长度小于2,没法下刀
if (length < 2) {
return 0;
}
//长度等于2,只能切1刀,每段最小长度为1
if (length == 2) {
return 1;
}
//长度等于3,可以切2刀,乘积为2,切3刀,每段为1,乘积还是1
if (length == 3) {
return 2;
}
//尽可能多切长度为3的绳子,求可以切几刀
double timesOfThree = length / 3;
//当绳子最后的长度为4时,不能再去剪去长度为3的绳子段,更好的方法是剪成绳子长度为2的两段,因为2x2>3x1
if (length - timesOfThree * 3 == 1) {
timesOfThree = 1;
}
//尽可能多切长度为2的绳子,减去已切(长度为3*切长度为3的次数),求可以切几刀
double timesOfTwo = (length - timesOfThree * 3) / 2;
return Math.pow(3.0, timesOfThree) * Math.pow(2.0, timesOfTwo);
}
个人理解
1.和斐波那契数列的从下而上的递归方式相同,但又不太相像。
2.可以画图穷举一些数据,来辅助自己抽象化逻辑。