![3a2eceb8c0180cb97a38b25d5fd476db.png](https://i-blog.csdnimg.cn/blog_migrate/87c09a4a63c72814375db05c73a28251.jpeg)
剑指Offer_编程题——剪绳子
题目描述:
给你一根长度为n的绳子,请把绳子剪成整数长的m段(m、n都是整数,n>1并且m>1,m<=n),每段绳子的长度记为k[1],…,k[m]。请问k[1]x…xk[m]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
输入描述:
输入一个数n,意义见题面。(2 <= n <= 60)
输出描述:
输出答案。
示例1
输入:8
输出: 18
具体要求:
时间限制: C/C++ 1秒,其他语言2秒
空间限制: C/C++32M,其他语言64M
具体实现:
背景知识介绍:
在做题之前,首先给大家介绍数据结构中典型的贪心算法以及动态规划。在维基百科中是这样介绍贪心算法和动态规划的。贪心算法(greedy algorithm),又称贪婪算法,是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是最好或最优的算法。比如在旅行推销员问题中,如果旅行员每次都选择最近的城市,那这就是一种贪心算法。贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是局部最优解能决定全局最优解。简单地说,问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。贪心法可以解决一些最优化问题,如:求图中的最小生成树、求哈夫曼编码……对于其他问题,贪心法一般不能得到我们所要求的答案。一旦一个问题可以通过贪心法来解决,那么贪心法一般是解决这个问题的最好办法。由于贪心法的高效性以及其所求得的答案比较接近最优结果,贪心法也可以用作辅助算法或者直接解决一些要求结果不特别精确的问题。不过在做贪心算法的时候,我们需要以下这些细节:
1、创建数学模型来描述问题。
2、把求解的问题分成若干个子问题。
3、对每一子问题求解,得到子问题的局部最优解。
4、把子问题的解局部最优解合成原来解问题的一个解。
一般而言,对于大部分的问题,贪心法通常都不能找出最佳解(不过也有例外),因为他们一般没有测试所有可能的解。贪心法容易过早做决定,因而没法达到最佳解。例如,所有对图着色问题。另外贪心法在系统故障诊断策略生成乃至高校的排课系统中都可使用。比如说我们在数据结构中常用的最小生成树的算法如Prim算法、Kruskal算法均为贪心算法,其中Prim算法是对图上的节点贪心,而Kruskal算法是对图上的边贪心。
动态规划(Dynamic programming)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子问题的解以得出原问题的解。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。动态规划在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。适合于动态规划的有:最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质(即满足最优化原理)。最优子结构性质为动态规划算法解决问题提供了重要线索。无后效性。即子问题的解一旦确定,就不再改变,不受在这之后、包含它的更大的问题的求解决策影响。以及最后的子问题重叠性质。子问题重叠性质是指在用递归算法自顶向下对问题进行求解时,每次产生的子问题并不总是新问题,有些子问题会被重复计算多次。动态规划算法正是利用了这种子问题的重叠性质,对每一个子问题只计算一次,然后将其计算结果保存在一个表格中,当再次需要计算已经计算过的子问题时,只是在表格中简单地查看一下结果,从而获得较高的效率,降低了时间复杂度。应用动态规划的典型案例就是背包问题。以及最长公共子序列、Floyd-Warshall算法以及Viterbi算法都是用到的是动态规划算法。
接下来,我们对比贪心算法和动态规划:
首先他们共有的特点就是:都是一种推导算法;都是分解成子问题来求解,都需要具有最优子结构。
区别在于:贪心算法每一步的最优解一定包含上一步的最优解,上一步之前的最优解则不作保留;而动态规划中的全局最优解中一定包含某个局部最优解,但不一定包含前一个局部最优解,因此需要记录之前的所有的局部最优解 ;贪心如果把所有的子问题看成一棵树的话,贪心从根出发,每次向下遍历最优子树即可(通常这个“最优”都是基于当前情况下显而易见的“最优”);这样的话,就不需要知道一个节点的所有子树情况,于是构不成一棵完整的树;而动态规划则自底向上,从叶子向根,构造子问题的解,对每一个子树的根,求出下面每一个叶子的值,最后得到一棵完整的树,并且最终选择其中的最优值作为自身的值,得到答案。因此,贪心不能保证求得的最后解是最佳的,一般复杂度低;而动态规划本质是穷举法,可以保证结果是最佳的,复杂度高。另外针对0-1背包问题:这个问题应比较选择该物品和不选择该物品所导致的最终方案,然后再作出最好选择,由此就导出许多互相重叠的子问题,所以用动态规划。
由上述的介绍可知,本题用到的就是贪心算法和动态规划。
思路一:
首先我们用动态规划思想来解决该题。根据上述介绍可知,动态规划f(n) = max(f(i)f(n-i)), 其中 0<i<n,从1到n/2遍历i,求出最大的f(n) 。为了避免重复计算,选择从下到上的顺序计算。其实在刚开始的时候我们是没有任何思路的,因为思路并不是很明确的。那么我们就只能从最简单的情况进行考虑,试着从简单的情况找出规律,以下就是n=1,2,3,4的情况:
1、当n=1时,最大乘积只能为0;
2、当n=2时,最大乘积只能为1;
3、当n=3时,最大乘积只能为2;
4、当n=4时,可以分为如下几种情况:1111,121,13,22,最大乘积为4;
……
接着在往下推的时候,就会发现:当n>=4时,可以把问题变成几个小问题,即:如果把长度n绳子的最大乘积记为f(n),则有:f(n)=max(f(i)*f(n-1)),0<i<n。所以思路就很容易出来了:从下往上推,先算小的问题,再算大的问题,大的问题通过寻找小问题的最优组合得到。其中的时间复杂度为O(n^2);
其实我们刚才找规律的这个过程就是动态规划的思想。以下是动态规划的几个特点:
求一个问题的最优解
整体问题的最优解依赖各子问题的最优解
小问题之间还有相互重叠的更小的子问题
为了避免小问题的重复求解,采用从上往下分析和从下往上求解的方法求解问题
接下来,我们用java和python两种语言进行实现。
1、首先我们用java将其实现:
public class Solution{
public int cutRope(int len){
if(len < 2)
return 0;
if(len == 2)
return 1;
if(len == 3)
return 2;
int []result = new int[len + 1];
result[0] = 0;
result[1] = 1;
result[2] = 2;
result[3] = 3;
int max = 0;
for(int i= 4; i <= len; i++){
max = 0;
for(int j = 1; j <= i /2; j++){
int tempResult = result[j] * result[i - j];
if(max < tempResult)
max = tempResult;
result[i] = max;
}
}
return max;
}
}
代码效果图如图所示:
![8649f1021704d2c2ac2d6fe9f17da011.png](https://i-blog.csdnimg.cn/blog_migrate/f05dafe73f76f4fbcaecce4bc02555d1.jpeg)
2、接下来我们用python将其实现:
class Solution:
def cutRope(self, length):
if length == 0:
return 0
if length == 1:
return 1
if length == 2:
return 2
if length == 3:
return 3
if length == 4:
return 4
result = [0,1,2,3]
for i in range(4, length+1):
max = 0
for j in range(1, i // 2 + 1):
temp = result[j] * result[i - j]
if temp > max:
max = temp
result.append(max)
return result[length]
代码效果图如图所示:
![3249293f889a9a30d0efdf5a36f901c4.png](https://i-blog.csdnimg.cn/blog_migrate/be9677f2061d68d62397c32f76040465.jpeg)
思路二:
接下来我们用贪心算法解决该题。当n大于等于5时,尽可能剪长度为3的绳子,当剩下长度为4时,把绳子剪成2段长度为2的绳子。证明:n≥5时,2(n-2)>n, 3(n-3)>n, 并且3(n-3)≥2(n-2);n=4时, 2×2>3×1。贪婪算法依赖于数学证明,当绳子大于5时,尽量多地剪出长度为3的绳子是最优解。这里需要我们注意的是:我们应当把绳子剪成尽量多的3,让剩下的都是2这样的组合。贪婪算法的时间复杂度为:O(1)。具体我们用java和python两门语言将其实现。
1、首先我们用java将其实现:
public class Solution{
public int cutRope(int len){
if (len < 2)
return 0;
if (len == 2)
return 1;
if (len == 3)
return 2;
int timeOfThree = len / 3;
if(len - timeOfThree * 3 == 1)
timeOfThree -= 1;
int timeOfTwo = (len - timeOfThree * 3) / 2;
return (int)((Math.pow(3, timeOfThree)) * (Math.pow(2, timeOfTwo)));
}
}
代码效果图如图所示:
![d40a158b97ee1579d1bfa429f19a2b75.png](https://i-blog.csdnimg.cn/blog_migrate/64fbb3976b05a359ca46d36fb667cbe4.jpeg)
2、接下来用Python将其实现:
class Solution:
def cutRope(self, length):
if length <= 4:
return length
timeOfThree = length // 3
if length - timeOfThree * 3 == 1:
timeOfThree -= 1
timeOfTwo = (length - timeOfThree * 3) // 2
return pow(3, timeOfThree) * pow(2, timeOfTwo)
代码效果图如图所示:
![c4cad5adf19ceea2c78b94e187d93840.png](https://i-blog.csdnimg.cn/blog_migrate/6af77ecd9e62a2a7fcb66805d5fbf7dc.jpeg)
这里需要我们注意的是:尽管贪婪算法效率更高了。不过并不是所有问题都可以采用贪婪策略得到最优解。因此我们在使用前必须先证明,一个问题可以通过贪婪策略得到最优解,最后只有证明了这个问题的可行性之后才能采取这种方法。
总结
本题通过剪绳子一个实际问题考察大家对贪心算法以及动态规划的理解与掌握情况。本文在解答之前给大家首先就介绍了动态规划以及贪心算法的相关内容,另外,还对比了动态规划和贪心算法的同异行。本文给出了两种解题思路:即动态规划与贪心算法。并且分别用java和python两门编程语言将其实现。这里需要我们注意的是:尽管贪婪算法效率更高了。不过并不是所有问题都可以采用贪婪策略得到最优解。因此我们在使用前必须先证明,一个问题可以通过贪婪策略得到最优解,最后只有证明了这个问题的可行性之后才能采取这种方法。。因此,我们在做题的时候,应该多次尝试各种方法,扩展自己的思维,写出优质的代码。总之,我们要继续加油,争取早日找到工作,Good Luck!!!
参考文献
[1]乖乖的函数
[2]hh_zheng
[3]贪心算法
[4]动态规划
[5]贪心算法和动态规划的区别与联系