[2021校招必看之Java版《剑指offer》-67] 剪绳子

文章目录

  • 1、题目描述
  • 2、解题思路
    • 2.1 暴力递归
    • 2.2 记忆化递归
    • 2.3 动态规划
    • 2.4 数学方法
  • 3、解题代码
    • 3.1 暴力递归
    • 3.2 记忆化递归
    • 3.3 动态规划
    • 3.4 数学方法
  • 4、解题心得

1、题目描述

  【JZ67】给你一根长度为n的绳子,请把绳子剪成整数长的m段(mn都是整数,n>1并且m>1),每段绳子的长度记为k[0],k[1],...,k[m-1]。请问k[0]*k[1]*...*k[m-1]可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
  知识点:递归,动态规划
  难度:☆☆

2、解题思路

2.1 暴力递归

  题目蕴含了两个问题:分成多少段+每段多长。分段数量和每段长度都直接影响乘积,因此,没办法按照直觉的方法找出因果关系。
  
  由题意可知,n和m的最小取值为2。
  当n=2时,分段策略可以是{(1,1)},此时乘积对应为{1}
  当n=3时,分段策略可以是{(1,1,1),(1,2)},此时乘积对应为{1,2}
  当n=4时,分段策略可以是{(1,1,1,1),(2,1,1),(2,2)},此时乘积对应为{1,2,4}
  
  题目要求是最大乘积,因此,我们尽量不会考虑平均分成每一段都为1的情况,尽量排除这个情况,那么就变为:
  当n=2时,分段策略只能是{(1,1)},此时乘积对应为{1}
  当n=3时,分段策略可以是{(1,2)},此时乘积对应为{2}
  当n=4时,分段策略可以是{(1,1,2),(2,2)},此时乘积对应为{2,4}
  也就是说,当n从4开始,乘积结果就不唯一了,需要各种策略讨论了。
  
  当n大于等于4时,如果我第一段长度固定为1,那么此时要得出最大乘积,只能靠后面n-1长度的绳子进行某个策略分段达到最大乘积。同理,如果我第一段绳子长度固定为2,此时要得到最大乘积也只能靠后面n-2长度的绳子进行某个策略分段达到最大乘积。
  
  因此,当n大于等于4时,我们可以定义一个递归函数int backTrack(int n),这个函数的功能是:求长度为n的数,最后分段后的最大乘积。
  对于长度为n (n>3)的绳子:
  如果第一段长度为1,则最大乘积为:1*backTrack(n-2)
  如果第一段长度为2,最大乘积为:2*backTrack(n-3)
  。。。。。。
  如果第一段长度为n-1,最大乘积为:(n-1)*backTrack(1)
  代码见本章3.1小结。

2.2 记忆化递归

  见3.1小结的代码,其实代码存在大量的重复运算上面,比如绳子长度为7时:
[2021校招必看之Java版《剑指offer》-67] 剪绳子_第1张图片
  其中f(n)为backTrack()函数,红色部分就是重复计算,我们可按照动态规划的思想,对每一个f(i)进行缓存并复用。

2.3 动态规划

  动态规划的思路就是:建立状态转移方程f(n)、缓存并复用以往结果和按顺序从小往大算。
  其中最关键的就是建立状态转移方程f(n),由上面分析可知:
  当n=2时,最大乘积为1;
  当n=3时,最大乘积为2;
  当n从4开始,就有多个不为1的乘积结果,我们定义一个函数f(n),作用是:对于输入的绳子长度n,如果不分段则返回本身,若分段,从所有的分段策略中,返回每段相乘结果最大的那个最大乘积。
  即我们的状态转移方程f(n)并不是题目要的在n>2且m>2下的最大乘积,而是n>0且m>0下的最大乘积。

2.4 数学方法

  已知:
  X1 + X2 + … + Xm = n(n和m均为正整数,且n>1,m>1),
  S(n) = X1 * X2 * … * Xm
  求:maxS(n)
  解:
    ∵ S(n) = X1 * X2 * … * Xm ≤ (X1 + X2 + … + Xm)2/4
    当且仅当 X1 = X2 = … = Xm时等号成立
    ∴ 当平均分成m段时,S(n)的值最大
    设 X1 = X2 = … = Xm = X
    则 maxS(n) = Xm
    又∵ mX = n
    ∴ maxS(n) = Xn/X
    对 maxS(n) 求导,得:maxS`(n) = 在这里插入图片描述
    可以看出,当 1-ln(x) = 0时, maxS(n)有最大值
    解得:x = e
    又∵ x为整数,取x = 3
    当 n = 2时,maxS(n) = 2
    当 n = 3时,maxS(n) = 3
    当 n > 3时,分为以下三种情况:
      n刚好被3整除:maxS(n) = 3n/3
      n除以3还余1:maxS(n) = 3n/3-1 * 4
      n除以3还余2:maxS(n) = 3n/3 * 2
  至此,整个数学方法流程分析完毕。

3、解题代码

3.1 暴力递归

package pers.klb.jzoffer.medium;

/**
 * @program: JZoffer
 * @description: 剪绳子(暴力递归)
 * @author: Meumax
 * @create: 2020-06-18 10:35
 **/
public class CutRope {
    public int cutRope(int target) {
        if (target == 2) {
            return 1;
        } else if (target == 3) {
            return 2;
        } else {
            return backTrack(target);
        }
    }

    private int backTrack(int n) {
        if (n <= 4) {
            return n;
        } else {
            int result = 0;
            for (int i = 1; i < n; i++) {
                result = Math.max(result, i * backTrack(n - i));
            }
            return result;
        }
    }
}

  时间复杂度:O(n2)
  空间复杂度:O(n)

3.2 记忆化递归

package pers.klb.jzoffer.medium;

/**
 * @program: JZoffer
 * @description: 剪绳子(记忆化递归)
 * @author: Meumax
 * @create: 2020-06-18 10:35
 **/
public class CutRope {

    public int cutRope(int target) {
        if (target == 2) {
            return 1;
        } else if (target == 3) {
            return 2;
        } else {
            // 用于保存不同target下的最大乘积
            int[] mark = new int[target + 1];

            // 初始化每一项为-1
            for (int i = 1; i <= target; i++) {
                mark[i] = -1;
            }

            return backTrack(target, mark);
        }
    }

    private int backTrack(int n, int[] mark) {
        if (n <= 4) {
            return n;
        }

        // 如果这个值已经计算过,就不用再计算,直接返回
        if (mark[n] != -1) {
            return mark[n];
        } else {    // 如果mark[n]没有计算过,那就计算
            int result = 0;
            for (int i = 1; i < n; i++) {
                result = Math.max(result, i * backTrack(n - i, mark));
            }

            // 保存最大乘积到数组中
            mark[n] = result;

            return result;
        }
    }
}

  时间复杂度:O(n2)
  空间复杂度:O(n)

3.3 动态规划

package pers.klb.jzoffer.medium;

/**
 * @program: JZoffer
 * @description: 剪绳子
 * @author: Meumax
 * @create: 2020-06-18 10:35
 **/
public class CutRope {

    public int cutRope(int target) {

        switch (target) {
            case 2:
                return 1;

            case 3:
                return 2;

            default:
                // 定义一个数组f用于保存每一个target情况下的最大乘积(可以不分段)
                int[] f = new int[target + 1];

                // 初始化为-1
                for (int i = 0; i <= target; i++) {
                    f[i] = -1;
                }

                // 长度为1、2、3、4时最大乘积就是其本身
                for (int j = 1; j <= 4; j++) {
                    f[j] = j;
                }

                // 从小到大,依次计算出n很大时的最大乘积
                for (int i = 5; i <= target; i++) {
                    // 当绳子总长度为i,第一段长度为j时,最大乘积为 j * f[i - j]
                    for (int j = 1; j < i; j++) {
                        f[i] = Math.max(f[i], j * f[i - j]);
                    }
                }

                return f[target];
        }
    }
}

  时间复杂度:O(n2)
  空间复杂度:O(n)

3.4 数学方法

package pers.klb.jzoffer.medium;

/**
 * @program: JZoffer
 * @description: 剪绳子
 * @author: Meumax
 * @create: 2020-06-18 10:35
 **/
public class CutRope {

    public int cutRope(int target) {
        if (target == 2) {
            return 1;
        } else if (target == 3) {
            return 2;
        }
        if (target % 3 == 0) {
            return (int) Math.pow(3, target / 3);
        } else if (target % 3 == 1) {
            return 4 * (int) Math.pow(3, target / 3 - 1);
        } else {
            return 2 * (int) Math.pow(3, target / 3);
        }
    }
}

  时间复杂度:O(1)
  空间复杂度:O(1)

4、解题心得

  这道题对于新手来说着实有点难度,因为分析比较长,且不直观。做这种题,实在没办法的情况下可以从最小值开始,列出来,可能就看出规律能初步写出递归,然后再进行优化。
  最难想到的就是动态规划的状态转移方程,总结的结论就是,当我们的状态转移方程起始条件不是从0开始,往往需要一些额外处理。
  最后,如果能把一个编程题转成标准的数学题,也是厉害的了,其实大部分可以动态规划的题目,往下继续找规律,可以找到更简单的解析式。但是还是符合那个原理:人类看的明白的,计算机执行效率低,人类看不明白的,计算机执行效率高。

你可能感兴趣的:(剑指offer(Java语言))