如果面试题是求一个问题的最优解(通常是求最大值或最小值),而且
1.该问题能分解成若干个子问题。
2.子问题也存在最优解,如果把小问题的最优解组合起来能够得到整个问题的最优解,即整体问题的最优解依赖于各个子问题的最优解。
3.这些小问题之间有重叠的问题,即在分解大问题的过程中反复出现相同子问题。
就可以使用动态规划来解决这个问题。
动态规划算法中为了避免重复子问题的求解,我们可以用从下往上的顺序,先计算小问题的最优解并存储下来,再以此为基础求取大问题的最优解。
贪婪算法每一步都作出当前最优的选择。
面试题14:剪绳子。给你一段长度为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(i)*f(n-i)),其中0<i<n。这是一个从上至下的递归公式,由于递归会产生很多重复的子问题,从而有大量不必要的计算。更好的方法是从下至上计算,也就是先得到f(2)、f(3),再得到f(4)、f(5),直到得到f(n):
#include <iostream>
using namespace std;
int MaxProductAfterCutting(int length) {
if (length < 2) { //绳子长度小于2时不符合题意
return 0;
}
if (length == 2) { //绳子长为2时只能剪成1*1
return 1;
}
if (length == 3) { //绳长为3时只能剪成1*2
return 2;
}
int* products = new int[length + 1]; //存放子问题的最优解,其实只需要length长度的数组
//如输入绳长为5,求f(5),只需f(1)~f(5)
//但为了书写方便,如需要f(3)时只需找数组下标为3的元素而不用找下标为2的元素
//需要将数组长扩充一位,其中第0个元素不会使用到
products[0] = 0; //0号元素随便初始化,不会用到
products[1] = 1;
products[2] = 2;
products[3] = 3; //下标为1~3的存放绳长即可,下标4~length存放的才是绳长为下标时的最优解
//因为在绳长为1时,不能再分割
//绳长为2时,若分割,只能分成1*1=1,不分割时为2,此时最优解为2
//绳长为3时,若分割,只能分成1*2=2,不分割时为3,此时最优解为3
//由于题目要求必须分割一次,因此当输入绳长大于3时,用到绳长为1~3时的最优解时,必定已经被分割过一次了
int max; //存放绳长为a时的最大值,a为任意中间子问题解
for (int i = 4; i <= length; ++i) { //从绳长为4开始
max = 0; //每次开始找绳长为a的最优解时先初始化max
for (int j = 1; j <= i / 2; ++j) {
int product;
product = products[j] * products[i - j];
if (max < product) {
max = product;
}
}
products[i] = max;
}
delete[] products;
return max;
}
int main(int argc, char **argv) {
cout << MaxProductAfterCutting(8) << endl;
return 0;
}
动态规划的时间复杂度为O(n²),空间复杂度为O(n)。
方法二:贪婪算法。按以下策略剪绳子,得到的各段绳子的长度的乘积将最大:
当n>=5时,尽可能多地剪长度为3的绳子;当剩下的绳子长度为4时,把绳子剪成两段长度为2的绳子。
数学证明比较简单,由于我们知道当绳段长为2或3时不用再分割,因为绳长2或3时再分割反而不如不分割(如2分割为1*1,最后结果为1<2,3时同理),并且当n>=5时,2(n-2)>n恒成立且3(n-3)>n恒成立,而4(n-4)>n并非恒成立,这说明当绳长大于等于5时,当我们分割后有一段长为2或3的绳子的情况下的乘积要比不分割效果要好,并且3(n-3)>2(n-2),说明分割后有一段长为3的绳子要比有一段长为2的绳子效果要好,因此在n>=5时要尽可能多地剪长为3的绳子段。在n为4时,易知剪成2*2和不剪效果一样,然而题目中要求必须剪一刀,因此剪成2*2:
#include <iostream>
using namespace std;
int MaxProductAfterCutting(int length) {
if (length < 2) { //绳子长度小于2时不符合题意
return 0;
}
if (length == 2) { //绳子长为2时只能剪成1*1
return 1;
}
if (length == 3) { //绳长为3时只能剪成1*2
return 2;
}
int timesOf3 = length / 3; //当前输入的length长能分出几段长为3的绳段
if (length - 3 * timesOf3 == 1) { //如果每次取一段长为3绳段,最后能剩下一段长为4的绳段时
--timesOf3; //最后的长为4的绳段不再分为1和3
}
int timesOf2 = (length - timesOf3 * 3) / 2; //最后剩下的是2或4时能分为几个2
return (int)pow(3, timesOf3) * (int)pow(2, timesOf2);
}
int main(int argc, char **argv) {
cout << MaxProductAfterCutting(8) << endl;
return 0;
}