问题描述
题目:给你一根长度为n的绳子,请把绳子剪成m段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0], k[1], … ,k[m]。请问 k[0]✖k[1]✖…✖k[m] 可能的最大乘积是多少? 例如,当绳子的长度为8时,我们把它剪成长度分别为 2、3、3 的三段,此时得到的最大乘积是 18。
解题思路
对于这道题目,适宜用动态规划的思想来解决。假设 f(n) 为把长度为n的绳子剪成若干段之后各段长度乘积的最大值。在剪第一刀的时候,我们有n-1种选择,也就是剪出来的绳子长度可能取值为 1, 2, …, n-1。所以 f(n) = max(f(n-i)✖f(i)),其中 i 取值范围为 1~n-1。
所以这道题的本质就变成了求子项的乘积的最大值,我们可以自下而上地去求取这个问题的结果。代码如下所示:
public class Main {
public static int Solution(int n){
if(n < 2){
return 0;
}
if(n == 2){
return 1;
}
if(n == 3){
return 2;
}
int[] lens = new int[n+1];
lens[0] = 0;
lens[1] = 1;
lens[2] = 2;
lens[3] = 3;
for (int i = 4; i <= n; i++){
int max = 0;
for (int j = 1; j < i/2+1; j++){
if(lens[j] * lens[i-j] >= max){
max = lens[j] * lens[i-j];
}
}
lens[i] = max;
}
return lens[n];
}
public static void main(String[] args) {
System.out.println(Solution(8));
}
}
在自下而上的过程中,我们首先判断绳子的长度是否小于4。当绳子长度小于4时,它们的最长乘积从0~3分别为0、0、1、2;而当长度大于等于4时,则采用一个数组保存前面长度(0 ~ n-1)的绳子可取得的乘积最大值,然后再求取长度为n的绳子的乘积最大值即可。
通过这个问题,可以大致总结出动态规划问题的使用场景:
- 最优化的子结构属性:一个问题的最优解包含在一系列的子问题集的最优解就叫做最优化的子结构属性。
- 重叠的子问题集:当一个递归算法再次访问同一个问题时,这种事重复出现,我们说这个最优化问题有重叠的子问题集。
接下来我们再来通过下面2道题来加强下对动态规划的印象。
题目一
给定两个字符串 x 和 y, 求它们的公共最长子序列具有的长度。
输入:
ABCBDAB
BDCABA
输出:
4
问题分析
这道题具有一定的迷惑性,注意:公共最长子序列并不是指子序列必须是相邻的。在上面的输入例子中,它们的最长子序列就是 BCAB,也就是下面标红的部分:
ABCBDAB
BDCABA
那么接下来我们分析一下这道题是否能用动态规划的方法解决:
- 首先我们定义一个状态函数 s(n, m),其中 n 表示的是 x 的前n个子字符串,m表示的是y的前m个子字符串,其中 s(n, m) 的值表示 x 的前n个字符串和 y 的前m个子字符串的公共子序列长度。
- 我们将问题由后往前分析,假设 x[n-1] == y[m-1],那么 x 和 y 的公共子序列长度就是 s(n-1, m-1) + 1。
- 如果 x[n-1] != y[m-1],那么 x 和 y 的公共子序列长度则为Max{s(n-1,m), s(n,m-1)}。
可以看到,这个问题是符合动态规划的条件的,即最优化的子结构属性和重叠的子问题集。代码如下所示,笔者将其分为了递归和非递归两个版本:
public class Main {
/**
* 递归版本
*/
public static int Solution(String str1, String str2){
return Solution(str1, str2, str1.length()-1, str2.length()-1);
}
private static int Solution(String str1, String str2, int i, int j) {
// 当i或j为-1时,说明没有可比较的子字符串,直接返回0
if(i == -1 || j == -1){
return 0;
}
// 相等的时候为 s(n-1, m-1) + 1
if(str1.charAt(i) == str2.charAt(j)){
return Solution(str1, str2, i-1,