剑指offer学习笔记 动态规划与贪婪算法

如果面试题是求一个问题的最优解(通常是求最大值或最小值),而且
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;
}
发布了193 篇原创文章 · 获赞 11 · 访问量 6万+

猜你喜欢

转载自blog.csdn.net/tus00000/article/details/104356909
今日推荐