本文參考自《劍指offer》一書,代碼采用Java語言。
題目
給你一根長度為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。
思路
本題采用動態規划或者貪婪算法可以實現。一開始沒有思路時,可以從簡單的情況開始想,試着算以下比較短的繩子是如何剪的。
當n=1時,最大乘積只能為0;
當n=2時,最大乘積只能為1;
當n=3時,最大乘積只能為2;
當n=4時,可以分為如下幾種情況:1*1*1*1,1*2*1,1*3,2*2,最大乘積為4;
往下推時,發現n≥4時,可以把問題變成幾個小問題,即:如果把長度n繩子的最大乘積記為f(n),則有:f(n)=max(f(i)*f(n-1)),0<i<n。所以思路就很容易出來了:從下往上推,先算小的問題,再算大的問題,大的問題通過尋找小問題的最優組合得到。
其實這就是動態規划法,以下是動態規划法的幾個特點:
1.求一個問題的最優解
2.整體問題的最優解依賴各子問題的最優解
3.小問題之間還有相互重疊的更小的子問題
4.為了避免小問題的重復求解,采用從上往下分析和從下往上求解的方法求解問題
貪婪算法依賴於數學證明,當繩子大於5時,盡量多地剪出長度為3的繩子是最優解。
測試用例
1.功能測試(長度大於5)
2.邊界值測試(長度1,2,3,4)
完整Java代碼
(含測試代碼,測試代碼參考:CuttingRope.cpp)
/**
*
* @Description 面試題14:剪繩子
*
* @author yongh
* @date 2018年9月17日 上午9:37:41
*/
// 題目:給你一根長度為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。
public class CuttingRope {
// ======動態規划======
public int maxProductAfterCutting_solution1(int length) {
if (length <= 1)
return 0;
if (length == 2)
return 1;
if (length == 3)
return 2;
int[] product = new int[length + 1]; // 用於存放最大乘積值
// 下面幾個不是乘積,因為其本身長度比乘積大
product[0] = 0;
product[1] = 1;
product[2] = 2;
product[3] = 3;
// 開始從下到上計算長度為i繩子的最大乘積值product[i]
for (int i = 4; i <= length; i++) {
int max = 0;
// 算不同子長度的乘積,找出最大的乘積
for (int j = 1; j <= i / 2; j++) {
if (max < product[j] * product[i - j])
max = product[j] * product[i - j];
}
product[i] = max;
}
return product[length];
}
// =======貪婪算法========
public int maxProductAfterCutting_solution2(int length) {
if (length <= 1)
return 0;
if (length == 2)
return 1;
if (length == 3)
return 2;
int timesOf3 = length / 3;
int timesOf2 = 0;
if (length - timesOf3 * 3 == 1) {
timesOf3--;
// timesOf2=2; //錯誤!
}
timesOf2 = (length - timesOf3 * 3) / 2;
return (int) (Math.pow(3, timesOf3) * Math.pow(2, timesOf2));
}
// =====測試代碼======
void test(String testName, int length, int expected) {
if (testName != null)
System.out.println(testName + ":");
if (maxProductAfterCutting_solution1(length) == expected) {
System.out.print(" 動態規划:" + "passed ");
} else {
System.out.print(" 動態規划:" + "failed ");
}
if (maxProductAfterCutting_solution2(length) == expected) {
System.out.println("貪婪算法:" + "passed ");
} else {
System.out.println("貪婪算法:" + "failed ");
}
}
void test1() {
test("test1", 1, 0);
}
void test2() {
test("test2", 2, 1);
}
void test3() {
test("test3", 3, 2);
}
void test4() {
test("test4", 4, 4);
}
void test5() {
test("test5", 5, 6);
}
void test6() {
test("test6", 10, 36);
}
void test7() {
test("test7", 50, 86093442);
}
public static void main(String[] args) {
CuttingRope demo = new CuttingRope();
demo.test1();
demo.test2();
demo.test3();
demo.test4();
demo.test5();
demo.test6();
demo.test7();
}
}
test1:
動態規划:passed 貪婪算法:passed
test2:
動態規划:passed 貪婪算法:passed
test3:
動態規划:passed 貪婪算法:passed
test4:
動態規划:passed 貪婪算法:passed
test5:
動態規划:passed 貪婪算法:passed
test6:
動態規划:passed 貪婪算法:passed
test7:
動態規划:passed 貪婪算法:passed
收獲
1.最優解問題,經常使用動態規划法,關鍵要刻畫最優解的結構特征(本題的f(n)),從下往上計算最優解的值,沒有思路時,從簡單情況先算一下。
2.動態規划法中,子問題的最優解一般存放於一個數組中。
3.本題貪婪規划的代碼中,timeOf2別忘記等於1的情況。
復習時補充:
1. 動態規划法可以直接令 f(n)=max{f(n-2)*2,f(n-3)*3} 就可以了。
2. 貪婪算法,第51-58行可改為
int timesOf3=n/3; if(n%3==0) return (int)Math.pow(3, timesOf3); if(n%3==1) return (int)Math.pow(3, timesOf3-1)*4; //是乘以4,不是2 return (int)Math.pow(3, timesOf3)*2;
