鸡蛋问题

你将得到 K 个鸡蛋,并可使用一栋从 1N 共有 N 层楼的建筑。算法

每一个蛋的功能都是同样的,若是一个蛋碎了,你就不能再把它掉下去。编程

你知道存在楼层 F ,知足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。函数

每次移动,你能够取一个鸡蛋(若是你有完整的鸡蛋)并把它从任一楼层 X 扔下(知足 1 <= X <= N)。测试

你的目标是 确切地 知道 F 的值是多少。优化

不管 F 的初始值如何,你肯定 F 的值的最小移动次数是多少?code

示例1:blog

输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。若是它碎了,咱们确定知道 F = 0 。
不然,鸡蛋从 2 楼掉落。若是它碎了,咱们确定知道 F = 1 。
若是它没碎,那么咱们确定知道 F = 2 。
所以,在最坏的状况下咱们须要移动 2 次以肯定 F 是多少。

示例2:数学

输入:K = 2, N = 6
输出:3

示例1:class

输入:K = 3, N = 14
输出:4

提示:方法

  1. 1 <= K <= 100
  2. 1 <= N <= 10000

动态规划

什么是动态规划

动态规划英文 Dynamic Programming, 是求解决策过程最优化的数学方法,后来沿用到了编程领域。

动态规划的大体思路是把一个复杂的问题转化成一个分阶段逐步递推的过程,从简单的初始状态一步一步递推,最终获得复杂问题的最优解。

动态规划的过程

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

因而咱们能够用动态规划的方法来解决此鸡蛋问题。动态规划的关键就在于寻找到状态转移方程式。在此问题中,咱们能够把m层楼和n个鸡蛋的问题转化成一个函数F(m,n),其中楼层m和鸡蛋n是函数的两个参数,而函数的值则是最优解的最大尝试次数。

假设咱们第一个鸡蛋认出的位置在第x层(1<=x<=m),会出现两种状况:

  1. 鸡蛋没碎

那么剩余m-x层数,剩余n个鸡蛋,能够转化为如下的函数:

F(m-x, n) + 1, 1 <= x <= m

  1. 鸡蛋碎了

那么剩余x-1层数,剩余n-1个鸡蛋,能够转化为如下的函数:

F(x-1, n-1) + 1, 1 <= x <= m

根据这两个状态转移式,咱们能够得知要在最差状态下的到最优解,首先最差的状态就是

max{F(m-x, n) + 1,F(x-1, n-1) + 1}

最优解就是

min{max{F(m-x, n) + 1,F(x-1, n-1) + 1}, 1 <= x <= m}

算出此状态转移式后,咱们还须要知道一个边界状态,经过分析咱们能够发现,1. 只有一层楼时不管多少鸡蛋都只须要一次便可得出结论。2.只有一个鸡蛋时,有多少层就须要试多少层。因此有如下的式子:

F(1, n) = 1;
F(m, 1) = m;

有了这些完整的状态转移方程式,咱们就能够从底层开始逐渐向上求解,下面就用表格展现一下F(4,3):

F(4,3)初始状态

首先根据边界条件,能够填入出第一行和第一列的值:

填入边界状态

接下来就要计算F(2,2)了,套用上面的状态转移方程式:

F(2,2) = min{max{F(2-x, 2) + 1,F(x-1, 1) + 1}, 1 <= x <= 2}

计算后能够得出F(2,2) = 2,因而将2填入表格中:

填入边界状态

而后依次计算得出后面的值,最终能够获得F(4,3)的结果,以下表:

填入边界状态

最终得出F(4,3)=3,代码的实现以下

int superEggDrop(int eggs, int floors) {
    int i, j, k, t, max;
 
    int temp[eggs + 1][floors + 1];
 
    for(i = 0; i < floors + 1; ++i)
    {
        temp[0][i] = 0;
        temp[1][i] = i;
    }
 
    for(i = 2; i < eggs + 1; ++i)
    {
        temp[i][0] = 0;
        temp[i][1] = 1;
    }
 
    for(i = 2; i < eggs + 1; ++i)
    {
        for(j = 2; j < floors + 1; ++j)
        {
            for(k = 1, max = UINT_MAX; k < j; ++k)
            {
                t = temp[i][j - k] > temp[i - 1][k -1] ?  temp[i][j - k] : temp[i - 1][k -1];
 
                if(max > t)
                {
                    max = t;
                }
            }
 
            temp[i][j] = max + 1;
        }
    }
 
    return temp[eggs][floors];
}

该算法的空间复杂度是O(nm),时间复杂度是O(nm^2),很显然这个时间复杂度太高,没法知足题目要求,因此如今须要进一步优化此算法。

首先这个算法是咱们根据状态转移方程式实现的,因此要想在时间上进行优化,有很大的苦难。因而咱们就须要转换咱们的思惟,咱们先递推出30层楼,4个鸡蛋的表格:

填入边界状态

经过分析表格,咱们能够发现F(26,2)=7,F(27,2)=7,F(28,2)=7,此时若再加一层F(29,2)=8。2个鸡蛋,在22-28层时测试7次就能够解决问题,但是当到了第29层时就会须要多测试一次。因而咱们能够引出一下的问题:

n个鸡蛋,测试m次(简记为D(n,m)),最大能够解决几层楼的问题

经过对递推结果表格的观察,咱们能够获得以下结论:

  1. D(1,m) = m;
  2. D(n,n) = 2^n - 1;
  3. D(1,m){m <= n} = D(m,m);

对于第二点,以D(4,4)为例,咱们第1次在8楼扔下鸡蛋,若是碎了,则第二次在4楼扔下鸡蛋,不然在12楼扔下鸡蛋,对于在4楼扔下鸡蛋的状况,以后能够分别在2楼或者6楼扔下鸡蛋,如此进行,就能够找到答案楼层,方法与二分查找同样。例如答案楼层是5的状况,测试序列为8,4,6,5。

对于第三点,若是有5个鸡蛋让你测试3次,即便三次测试鸡蛋都碎了,剩下的2个鸡蛋也派不上用场,因此D(5,3) = D(3,3)

发现这些关系以后,咱们彷佛找到解决n个鸡蛋测试m次最大可以解决楼层数的方法。对于D(n,m){n < m}而言,对于其可以测试的最大楼层数k,咱们能够构造这样的场景,将第一颗鸡蛋仍在楼层i,使得第i + 1层到第k层是D(n,m-1)能够解决的最大楼层数,第1层到第i - 1层是D(n-1,m-1)能够解决的最大楼层数,由此获得递推关系D(n,m) = D(n -1,m-1) + 1 + D(n,m-1),而后对D(n,m-1),D(n-1,m-1)再按照上述公式分解,直到得出刚才所列的三种可计算状况(n = 1,或者m <= n)为止,再进行回溯累加,就能够获得D(n,m)的值,代码以下:

int DroppingMax(int eggs, int times)
{
    if(eggs == 1)
    {
        return times;
    }
 
    if(eggs >= times)
    {
        return (int)pow(2, times) - 1;
    }
 
    return DroppingMax(eggs, times -1) + DroppingMax(eggs -1, times - 1) + 1;
}

根据此算法,咱们能够得出D(2,5)=15,D(2,8)=36,也就是说,2个鸡蛋测试5次最多能够解决15层楼的问题,测试8次最多能够解决36层楼的问题。可见,出这个题的人并非随便找两个楼层数陪我们玩玩,而是对此问题认真研读后的结果。有了此利器以后,咱们解决扔鸡蛋问题的的方法将获得大幅简化,对于n个鸡蛋解决k层楼的问题咱们只需找到这样的值m,使得D(n,m-1)< k <=D(n,m),代码以下

int superEggDrop(int eggs, int floors)
{
    int times = 1;
 
    while(DroppingMax(eggs, times) < floors)
    {
        ++times;
    }
 
    return times;
}

该算法的时间和空间复杂度不太好分析,但都要好于传统的DP算法,有兴趣的读者能够推敲一下。

相关文章
相关标签/搜索