动态规划解决扔鸡蛋问题

Posted 程序猿小黑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划解决扔鸡蛋问题相关的知识,希望对你有一定的参考价值。

在上一篇文章中,小黑介绍了一道有趣的智力题:




那么,如何利用动态规划来求出扔鸡蛋问题的通解呢?


换句话说,有M层楼 / N个鸡蛋,要找到鸡蛋摔不碎的临界点,需要尝试几次?



本篇会为大家详细讲述。



先回顾一下上一篇2个鸡蛋100层楼的情况:

看到这个题目,最保险的方法就是一层一层试验。但这样只需要一个鸡蛋就可以了。我们现在有两个鸡蛋,完全可以用有更快的方法。

进一步呢?可能试验的方法是二分查找,例如,第一个鸡蛋在50层扔下,如果碎了,第二个鸡蛋从1-49逐层试验;如果没碎,第一个鸡蛋在75层扔下,如果碎了,第二个鸡蛋从51-74逐层试验…但是,这个方法,很容易悲剧,例如,当正好49层是可以安全落下的,需要尝试50次。比只有一个鸡蛋的情况,效果还要差。

上面的分析都是从鸡蛋的角度出发的,想要得到最少的尝试次数,似乎比较难。那如果我们换个角度,从每个高度的楼层来看呢?如果,某个楼层是可以安全落下的,那么最少需要多少次尝试呢?

在我们解决问题的过程中,如果遇到最优问题的时候,往往可以先尝试一下动态规划的方法。而动态规划的方法,首要我们要找到构成这个最优问题的最优子问题。


动态规划解决扔鸡蛋问题



动态规划解决扔鸡蛋问题



什么是动态规划?


一、基本概念

    动态规划过程是:每次决策依赖于当前状态,又随即引起状态的转移。一个决策序列就是在变化的状态中产生出来的,所以,这种多阶段最优化决策解决问题的过程就称为动态规划。

二、基本思想与策略

    基本思想与分治法类似,也是将待求解的问题分解为若干个子问题(阶段),按顺序求解子阶段,前一子问题的解,为后一子问题的求解提供了有用的信息。在求解任一子问题时,列出各种可能的局部解,通过决策保留那些有可能达到最优的局部解,丢弃其他局部解。依次解决各子问题,最后一个子问题就是初始问题的解。

   由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。

    与分治法最大的差别是:适合于用动态规划法求解的问题,经分解后得到的子问题往往不是互相独立的(即下一个子阶段的求解是建立在上一个子阶段的解的基础上,进行进一步的求解)。

三、适用的情况

能采用动态规划求解的问题的一般要具有3个性质:

    (1) 最优化原理:如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构,即满足最优化原理。

    (2) 无后效性:即某阶段状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

   (3)有重叠子问题:即子问题之间是不独立的,一个子问题在下一阶段决策中可能被多次使用到。(该性质并不是动态规划适用的必要条件,但是如果没有这条性质,动态规划算法同其他算法相比就不具备优势)

动态规划解决问题的过程分为两步:


1.寻找状态转移方程式

2.利用状态转移方程式自底向上求解问题



动态规划解决扔鸡蛋问题



动态规划解决扔鸡蛋问题



如何找到状态转移方程式?


在上一篇文章中,两个鸡蛋100层楼的条件下,我们找到了一个规律:


假设存在最优解,在最坏情况下尝试次数是 X,那么第一个鸡蛋首次扔出的楼层也是 X 。


动态规划解决扔鸡蛋问题



这个规律在三个以上鸡蛋的条件上还能否适用呢?让我们来举个例子:


假设有三个鸡蛋,100层楼,第一个鸡蛋扔在第10层并摔碎了。这时候我们还剩下两个鸡蛋,因此第二个鸡蛋不必从底向上一层一层扔,而是可以选择在第5层扔。如果第二个鸡蛋也摔碎了,那么第三个鸡蛋才需要老老实实从第1层开始一层一层扔。


动态规划解决扔鸡蛋问题



这样一来,总的尝试次数是1+1+4 = 6 < 10。


因此,最优解的最坏情况下尝试次数是 X,鸡蛋首次扔出的楼层也是 X 这个规律不再成立。


那么,我们该怎么寻找规律呢?


我们可以把M层楼 / N个鸡蛋的问题转化成一个函数 F(M,N),其中楼层数M和鸡蛋数N是函数的两个参数,而函数的值则是最优解的最大尝试次数。


假设我们第一个鸡蛋扔出的位置在第X层(1<=X<=M),会出现两种情况:


1.第一个鸡蛋没碎

那么剩余的M-X层楼,剩余N个鸡蛋,可以转变为下面的函数:

 F(M-X,N)+ 1,1<=X<=M


2.第一个鸡蛋碎了

那么只剩下从1层到X-1层楼需要尝试,剩余的鸡蛋数量是N-1,可以转变为下面的函数:

F(X-1,N-1) + 1,1<=X<=M


整体而言,我们要求出的是 M层楼 / N个鸡蛋 条件下,最大尝试次数最小的解,所以这个题目的状态转移方程式如下:


 F(M,N)= Min(Max( F(M-X,N)+ 1, F(X-1,N-1) + 1)),1<=X<=M



动态规划解决扔鸡蛋问题



动态规划解决扔鸡蛋问题




如何进行求解?


状态转移方程式有了,如何计算出这个方程式的结果呢?


诚然,我们可以用递归的方式来实现。但是递归的时间复杂度是指数级的,当M和N的值很大的时候,递归的效率会变得非常低。


根据动态规划的思想,我们可以自底向上来计算出方程式的结果。


何谓自底向上呢?让我们以3个鸡蛋,4层楼的情况为例来进行演示。


请看下面的这张表:


动态规划解决扔鸡蛋问题



根据动态规划的状态转移方程式和自底向上的求解思路,我们需要从1个鸡蛋1层楼的最优尝试次数,一步一步推导后续的状态,直到计算出3个鸡蛋4层楼的尝试次数为止。



首先,我们可以填充第一个鸡蛋在各个楼层的尝试次数,以及任意多鸡蛋在1层楼的尝试次数。


原因很简单:

1.只有一个鸡蛋,所以没有任何取巧方法,只能从1层扔到最后一层,尝试次数等于楼层数量。

2.只有一个楼层,无论有几个鸡蛋,也只有一种扔法,尝试次数只可能是1。


动态规划解决扔鸡蛋问题



2个鸡蛋2层楼的情况,我们就需要带入状态转移方程式了:


F(2,2) = Min(Max( F(2-X,2)+ 1, F(X-1,2-1) + 1)),1<=X<=2


因为X的取值是1和2,我们需要对X的值逐一来尝试:


当X = 1时,

F(2,2) = Max( F(2-1,2)+ 1, F(1-1,2-1) + 1)) =  Max( F(1,2)+ 1, F(0,1) + 1) = Max(1+1, 0+1) = 2


当X = 2时,

F(2,2) = Max( F(2-2,2)+ 1, F(2-1,2-1) + 1)) =  Max( F(0,2)+ 1, F(1,1) + 1) = Max(0+1, 1+1) = 2


因此,无论第一个鸡蛋先从第1层扔,还是先从第2层扔,结果都是尝试2次。



动态规划解决扔鸡蛋问题



接下来我们看一看2个鸡蛋3层楼的情况:


F(2,3) = Min(Max( F(3-X,2)+ 1, F(X-1,2-1) + 1)),1<=X<=3


此时X的取值是1,2,3。我们需要对X的值逐一来尝试:


当X = 1时,

F(2,3) = Max( F(3-1,2)+ 1, F(1-1,2-1) + 1)) =  Max( F(2,2)+ 1, F(0,1) + 1) = Max(2+1, 0+1) = 3


当X = 2时,

F(2,3) = Max( F(3-2,2)+ 1, F(2-1,2-1) + 1)) =  Max( F(1,2)+ 1, F(1,1) + 1) = Max(1+1, 1+1) = 2


当X = 3时,

F(2,3) = Max( F(3-3,2)+ 1, F(3-1,2-1) + 1)) =  Max( F(0,2)+ 1, F(2,1) + 1) = Max(1, 2+1) = 3


因此在2个鸡蛋3层楼的情况,最优的方法是第一个鸡蛋在第2层扔,共尝试2次。


动态规划解决扔鸡蛋问题



依照上面的方式,我们计算出2个鸡蛋4层楼的最优尝试次数,结果是3次。


动态规划解决扔鸡蛋问题



同理,我们按照上面的方式,计算出3个鸡蛋在各个楼层的尝试次数,分别是2次,2次,3次。具体计算过程就不再细说。


动态规划解决扔鸡蛋问题



动态规划解决扔鸡蛋问题



动态规划解决扔鸡蛋问题



代码如何实现?


根据刚才的思路,让我们来看一看代码的初步实现:


public class Eggs{

 
   
   
 
  1. public int getMinSteps(int eggNum, int floorNum){

  2.    if(eggNum < 1 || floorNum < 1) {

  3.        return 0;

  4.    }

  5.    //备忘录,存储eggNum个鸡蛋,floorNum层楼条件下的最优化尝试次数

  6.    int[][] cache = new int[eggNum+1][floorNum+1];

  7.    //把备忘录每个元素初始化成最大的尝试次数

  8.    for(int i=1;i<=eggNum; i++){

  9.        for(int j=1; j<=floorNum; j++)

  10.            cache[i][j] = j;

  11.    }

  12.    for(int n=2; n<=eggNum; n++){

  13.        for(int m=1; m<=floorNum; m++){

  14.            for(int k=1; k<m; k++){

  15.                //扔鸡蛋的楼层从1到m枚举一遍,如果当前算出的尝试次数小于上一次算出的尝试次数,则取代上一次的尝试次数。

  16.                //这里可以打印k的值,从而知道第一个鸡蛋是从第几次扔的。

  17.                cache[n][m] = Math.min(cache[n][m], 1+Math.max(cache[n-1][k-1],cache[n][m-k]));

  18.            }

  19.        }

  20.    }

  21.    return cache[eggNum][floorNum];

  22. }


  23. public static void main(String[] args) {

  24.    Eggs e = new Eggs();

  25.    System.out.println(e.getMinSteps(5,500));

  26. }

}


那么这段代码的时间复杂度和空间复杂度是多少呢?



动态规划解决扔鸡蛋问题



动态规划解决扔鸡蛋问题



如何优化呢?


我们从状态转移方程式以及上面的表格可以看出,每一次中间状态的尝试次数,都只和上一层(鸡蛋数量-1)和本层(当前鸡蛋数量)的值有关联:


 F(M,N)= Min(Max( F(M-X,N)+ 1, F(X-1,N-1) + 1)),1<=X<=M



动态规划解决扔鸡蛋问题



比如我们想要求解3个鸡蛋3层楼的最优尝试次数,并不需要知道1个鸡蛋这一层的值,只需要关心2个鸡蛋和3个鸡蛋在各个楼层的值即可。


这样一来,我们并不需要一个二维数组来存储完整的中间状态记录,只需要利用两个一维数组,存储上一层和本层的尝试次数就足够了。


请看优化版本的代码:



public class EggsOptimized {

 
   
   
 
  1. public int getMinSteps(int eggNum, int floorNum){

  2.    if(eggNum < 1 || floorNum < 1) {

  3.        return 0;

  4.    }

  5.    //上一层备忘录,存储鸡蛋数量-1的floorNum层楼条件下的最优化尝试次数

  6.    int[] preCache =  new int[floorNum+1];

  7.    //当前备忘录,存储当前鸡蛋数量的floorNum层楼条件下的最优化尝试次数

  8.    int[] currentCache = new int[floorNum+1];

  9.    //把备忘录每个元素初始化成最大的尝试次数

  10.    for(int i=1;i<=floorNum; i++){

  11.        currentCache[i] = i;

  12.    }

  13.    for(int n=2; n<=eggNum; n++){

  14.        //当前备忘录拷贝给上一次备忘录,并重新初始化当前备忘录

  15.        preCache = currentCache.clone();

  16.        for(int i=1;i<=floorNum; i++){

  17.            currentCache[i] = i;

  18.        }

  19.        for(int m=1; m<=floorNum; m++){

  20.            for(int k=1; k<m; k++){

  21.                //扔鸡蛋的楼层从1到m枚举一遍,如果当前算出的尝试次数小于上一次算出的尝试次数,则取代上一次的尝试次数。

  22.                //这里可以打印k的值,从而知道第一个鸡蛋是从第几次扔的。

  23.                currentCache[m] = Math.min(currentCache[m], 1+Math.max(preCache[k-1],currentCache[m-k]));

  24.            }

  25.        }

  26.    }

  27.    return currentCache[floorNum];

  28. }


  29. public static void main(String[] args) {

  30.    EggsOptimized e = new EggsOptimized();

  31.    System.out.println(e.getMinSteps(5,500));

  32. }

}









以上是关于动态规划解决扔鸡蛋问题的主要内容,如果未能解决你的问题,请参考以下文章

经典动态规划:高楼扔鸡蛋(进阶篇)

扔鸡蛋问题详解(Egg Dropping Puzzle)

二分递归动态规划程序员

高楼扔鸡蛋,非常浅显易懂的方式,但是复杂度并不是最低啊

动态规划法鸡蛋掉落问题

经典面试题楼层丢鸡蛋问题的动态规划解法与数学解法