鸡蛋掉落问题

鸡蛋掉落问题

 假定你手上有 n 个鸡蛋,现在有一栋 k 层的建筑物。在任意一层丢下一个鸡蛋,该鸡蛋都有可能会破,如果鸡蛋破了,那么就不能够再使用它。现要求得到一个临界楼层,从该楼层丢下的鸡蛋刚好不会破,而高于该层的楼层丢下的鸡蛋都会破,求最少要丢几次鸡蛋才能得到该临界楼层。
 比如说,现在手上有1个鸡蛋,现在的建筑物一共有2层,那么至少就需要丢两次鸡蛋才能确定临界楼层。这是因为如果在第二层丢鸡蛋,如果破了那么就需要再丢一次鸡蛋才能确定临界楼层。因此至少需要丢两次鸡蛋才能确定临界楼层。

--------------------------------------------------------------------分割线-------------------------------

诡异的问题。。。。。。

**

方法一 递归

**
基本思路:
 定义函数 eggDrop(n, k),其中 n 是现在手上能用的鸡蛋的数量,k 是当前需要考虑的楼层数量。可以得到

eggDrop(n, k) = 1 + min(eggDrop(n - 1, x), eggDrop(n, k - x)),其中 x = {1, 2, 3, 4, .......};

 在 x 楼丢下鸡蛋的情况,如果鸡蛋破了,那么就只剩下 n - 1 个鸡蛋,同时楼层也只剩下 x 层需要考虑的情况;如果鸡蛋没有破,那么剩下 n 个鸡蛋,k - x 层的楼层需要考虑。不断地递归,可以得到该问题地最终解。
 该问题的递归终点在于楼层的高度和鸡蛋的数量。如果楼层的数量为 0,那么就不需要再丢鸡蛋了,同样的,如果只剩下一层楼需要考虑,那么就只需要再丢一次鸡蛋了;而如果现在手头上的鸡蛋只有一个了,那么没办法,只能从之前的没有破的楼层开始一次一次地丢鸡蛋,才能得到临界楼层的确切值。

具体实现代码如下所示:

#include <stdio.h>
#include <limits.h>
#include <stdlib.h>

int max(const int a, const int b){
        return (a > b) ? a: b;
}

/**
 * 计算有n个鸡蛋,k层k楼的情况下最少需要的丢鸡蛋次数
 *
 * @param n: 现在可用的鸡蛋数量
 * @param k:需要考虑的楼层数量
 *
 * @return : 最少需要的丢鸡蛋次数
 */
int eggDrop(const int n, const int k) {
        /*
         如果当前需要考虑的楼层数量为 1 或 0,如果为 0 的情况,那么就不需要再丢鸡蛋了,因此直接返回 0;
         而如果楼层的数量为 1,那么只需要再丢一次即可得到确切的结果。
         因此直接返回 k 的楼层数
        */
        if (1 == k || 0 == k) return k;

        /*
         如果手上只有一枚鸡蛋可以使用了,那么就只能从上往下一层一层地丢了,因此也是返回楼层地数量 k。
        */
        if (1 == n) return k;

        int min = INT_MAX;
        int foo = 0;

        /*
         * 从第一层开始不断地尝试,考虑出现的所有情况,得到需要的最小尝试次数
         *
         * x 是当前的试验楼层
         */
        for (int x = 1; x <= k; ++x) {
                foo = max(eggDrop(n - 1, x - 1), eggDrop(n, k - x));

                if (foo < min) min = foo;
        }

        return min + 1;
}

int main(int argc, char ** argv) {
        if (argc < 3) {
                fprintf(stderr, "Please enter number of eggs and floors k\n");
                exit(1);
        }

        int n = atoi(argv[1]);
        int k = atoi(argv[2]);

        fprintf(stdout, "Minimum number of trails int wrost case with %d eggs and %d floors is %d\n", n, k, eggDrop(n, k));

        exit(0);
}

复杂度分析:
  时间复杂度:每次都需要遍历楼层的层数进行递归操作,因此时间复杂度为 O ( \lparen ( n k \def\foo{n^k} \foo nk ) \rparen )
 空间复杂度:只需要常数级别的变量存储这些结果,因此空间复杂度为 O ( \lparen ( 1 ) \rparen )

**

方法二 动态规划

**
 以上以递归的方式实现时间复杂度过高,这是由于重复的子问题很多都被重复地再次计算了一次,因此可以使用动态规划的方式来减低时间复杂度。

 使用动态规划的方式,这里需要的是一个二维数组来存储之前的状态信息,定义该二维数组如下:对于该二维数组的每一行索引 i,代表当前可用的鸡蛋数量,对于每一列的索引 j,代表当前的楼层数。

 根据上文递归方式的实现中的函数,很容易得到该二维数组的状态转移函数:

dp[i][j] = 1 + max(dp[i - 1][x - 1], dp[i][j - x]) (x={1, 2, 3, ....j})

 具体的解释:dp[i - 1][x - 1] 表示的是在 x 层丢鸡蛋破了的情况,那么这时可以再丢的鸡蛋数量为 i - 1,同时楼层的高度也可以减一,由于之前已经计算了这种情况,因此就无须再次进行计算。(注意这个二维数组中 i 表示可用的鸡蛋的数量)如果鸡蛋没有破,那么需要测试的楼层区间变为 j - x。(j 表示当前的总楼层,x 表示测试楼层)。由于要能够确定确切的临界楼层,因此必须考虑最坏的情况。

具体实现代码如下所示:

#include <stdio.h>
#include <limits.h>
#include <stdlib.h>

int max(const int a, const int b){
        return (a > b) ? a: b;
}
/*
 * 使用动态规划的方式解决鸡蛋掉落问题
 *
 * @param n : 给予测试的鸡蛋总数
 * @param k : 当前测试的建筑物的高度
 */
int eggDropDP(const int n, const int k) {
        // 存储状态的二维数组
        int dp[n + 1][k + 1];

        /*
         * 初始化状态数组的边界值,
         *
         * 对于 0 个鸡蛋的情况,无法通过丢鸡蛋得到确切的临界楼层,因此无须考虑这种情况
         *
         * 对于建筑物的楼层为 0 的情况,无须通过丢鸡蛋得到确切的临界楼层,因此实验次数为 0
         *
         * 对于只有一个鸡蛋的情况,要得到确切的临界楼层只能从第一层依次向上不断丢鸡蛋得到临界楼层
         */
        for (int i = 1; i <= n; ++i) {
                dp[i][1] = 1;
                dp[i][0] = 0;
        }

        for (int j = 1; j <= k; ++j) {
                dp[1][j] = j;
                dp[0][j] = 0;
        }
        // 初始化状态二维数组结束

        int ans;

        /**
         * i 代表鸡蛋的个数,j 代表当前要测试的楼层总高度,x 代表丢鸡蛋时出现结果的测试楼层高度
         */
        for (int i = 2; i <= n; ++i) {
                for (int j = 2; j <= k; ++j) {
                        dp[i][j] = INT_MAX;
                        for (int x = 1; x <= j; ++x) {
                                ans = 1 + max(dp[i - 1][x - 1], dp[i][j - x]);

                                if (ans < dp[i][j]) dp[i][j] = ans;
                        }
                }
        }
        // 由于状态转换的关系,最终可以得到有 n 个鸡蛋、k 层楼的情况下需要实验的最下次数
        return dp[n][k];
}

int main(int argc, char ** argv) {
        if (argc < 3) {
                fprintf(stderr, "Please enter number of eggs and floors k\n");
                exit(1);
        }

        int n = atoi(argv[1]);
        int k = atoi(argv[2]);
        
        fprintf(stdout, "Minimum number of trails int wrost case with %d eggs and %d floors is %d\n", n, k, eggDropDP(n, k));


        exit(0);
}

复杂度分析:
 时间复杂度:由于需要不断遍历楼层,因此最终的时间复杂度为 O ( \lparen ( n ∗ k 2 \def\fpp{n*}\fpp\def\foo{k^2} \foo nk2 ) \rparen )
 空间复杂度:需要使用一个二维数组来存储之前的状态,因此空间复杂度为 O ( \lparen ( n ∗ k \def\fpp{n*}\fpp\def\foo{k} \foo nk ) \rparen )

**

方法三 二项式系数与二分查找

**
 使用动态规划的方式解决了递归存在的时间复杂度过高的问题,但是时间复杂度还是很高。有个数学家 (Michael Boardman) 专门为此问题提出了一个更加有效的解决办法。
 总的思路就是将问题进行转换,将原问题 n 个鸡蛋,k 层楼丢最少多少次能够得到确定的临界楼层转换为:最少需要做多少次实验才能确定临界楼层,同时将整个楼层的高度都给覆盖?

 这个似乎有点绕,估计一般正常人也想不到。。。。

 引入函数 : f ( d , n ) f\lparen\def\d{d}\d, \def\n{n}\n\rparen f(d,n) 表示在有 n 个鸡蛋的条件下,做 d 次实验能够覆盖的楼层高度。如此一来,只要求 f ( d , n ) > = k f\lparen\def\d{d}\d, \def\n{n}\n\rparen \gt= k f(d,n)>=k 的最小 d 值即可。

f ( d , n ) = 1 + f ( d − 1 , n − 1 ) + f ( d − 1 , n ) f\lparen\def\d{d}\d, \def\n{n}\n\rparen = 1 + f\lparen\d - 1, \n - 1\rparen + f \lparen\d - 1, \n\rparen f(d,n)=1+f(d1,n1)+f(d1,n)

引入辅助函数 g ( d , n ) = f ( d , n + 1 ) − f ( d , n ) g\lparen\def\d{d}\d, \def\n{n}\n\rparen = f\lparen\d, \n + 1\rparen - f \lparen\d, \n\rparen g(d,n)=f(d,n+1)f(d,n)
那么:

g ( d , n ) = f ( d , n + 1 ) − f ( d , n ) = 1 + f ( d − 1 , n ) + f ( d − 1 , n + 1 ) − [ 1 + f ( d − 1 , n − 1 ) + f ( d − 1 , n ) ] = g ( d − 1 , n ) + g ( d − 1 , n − 1 ) g\lparen\def\d{d}\d, \def\n{n}\n\rparen = f\lparen\d, \n + 1\rparen - f \lparen\d, \n\rparen = 1 + f\lparen\d - 1, \n\rparen + f \lparen\d - 1, \n + 1\rparen -\lbrack1 + f\lparen\d - 1, \n - 1\rparen + f \lparen\d - 1, \n\rparen \rbrack\\ =g(\d-1, n) + g(\d - 1, n - 1) g(d,n)=f(d,n+1)f(d,n)=1+f(d1,n)+f(d1,n+1)[1+f(d1,n1)+f(d1,n)]=g(d1,n)+g(d1,n1)


由排列组合公式:

C ( n , k ) = C ( n − 1 ) + C ( n − 1 , k − 1 ) C(n, k) = C(n - 1) + C(n - 1, k - 1) C(n,k)=C(n1)+C(n1,k1)

因此:
g ( d , n ) = ( d n ) g(d, n) =\dbinom{d}{n} g(d,n)=(nd)

但是这里存在一个问题,对于 f ( 0 , n ) f(0, n) f(0,n) g ( 0 , n ) g(0, n) g(0,n) 它们应当一直是相等的且为 0(一次实验都不做,对于任意数量的鸡蛋都无法覆盖任意楼层)。而对于 n = 0 的情况, g ( 0 , 0 ) = ( 0 0 ) = 1 g(0, 0) = \dbinom{0}{0} = 1 g(0,0)=(00)=1 与之矛盾。

要解决这个问题,可以定义 g ( d , n ) = ( d n + 1 ) g(d, n) = \dbinom{d}{n + 1} g(d,n)=(n+1d) 这依旧是有效的(可以证明,但是我不会,望大佬告知)。

现在 f ( d , n ) = [ f ( d , n ) − f ( d , n − 1 ) ] + [ f ( d , n − 1 ) + f ( d , n − 1 ) ] + . . . . + [ f ( d , 1 ) − f ( d , 0 ) ] + f ( d , 0 ) f(d, n) = [f(d, n) - f(d, n -1)] + [f(d, n - 1) + f(d, n - 1)] + .... + [f(d, 1) - f(d, 0)] + f(d, 0) f(d,n)=[f(d,n)f(d,n1)]+[f(d,n1)+f(d,n1)]+....+[f(d,1)f(d,0)]+f(d,0)

由于 f ( d , 0 ) = 0 f(d, 0) = 0 f(d,0)=0 (没有鸡蛋丢的情况下肯定无法覆盖楼层),因此:
f ( d , n ) = g ( d , n − 1 ) + g ( d , n − 2 ) + . . . + g ( d , 0 ) = ( d n ) + ( d n − 1 ) + . . . + ( d 1 ) = ∑ i = 0 n ( d i ) f(d, n) = g(d, n - 1) + g(d, n - 2) + ... + g(d, 0)\\ =\dbinom{d}{n} + \dbinom{d}{n - 1} + ... + \dbinom{d}{1}\\ =\displaystyle\sum_{\mathclap{i=0}}^n\dbinom{d}{i} f(d,n)=g(d,n1)+g(d,n2)+...+g(d,0)=(nd)+(n1d)+...+(1d)=i=0n(id)


最终要解决的问题: f ( d , n ) > = k f(d, n) >= k f(d,n)>=k
即:
∑ i = 0 n ( d i ) > = k \displaystyle\sum_{\mathclap{i=0}}^n\dbinom{d}{i} >= k i=0n(id)>=k
中 d 的最小值。

具体实现代码如下所示:

#include <stdio.h>
#include <stdlib.h>
/**
 * 计算对应的二项式系数表达式总和
 *
 *
 * @param x : 当前丢鸡蛋的试验次数
 * @param n : 总的鸡蛋个数
 * @param k : 楼层的总高度
 */
int binomial(const int x, const int n, const int k) {
        int val = 1, sum = 0;

        /**
         * 由于在这个问题中只需要判断是否能够覆盖整层楼,
         * 因此无需计算整个二项式系数总和的具体值
         */
        for (int i = 1; i <= n && sum < k; ++i) {
                val *= x - i + 1;
                val /= i;

                sum += val;
        }

        return sum;
}

/**
 * 使用二项式系数和二分查找解决鸡蛋掉落问题
 *
 * 不管手上由多少个鸡蛋,总的实验次数不会超过楼层的总高度
 * 同样的,不可能一次实验都不做就直接得到实验总数,
 * 由于楼层的高度是一个有序的,因此可以采用二分查找的方式解决
 *
 * @param n : 能够实验的鸡蛋的总个数
 * @param k : 试验的建筑物的总层数
 */
int eggDropBinomial(const int n, const int k) {
        int lo = 1, hi = k;
        int mid = 0;

        while (lo < hi) {
                mid = (lo + hi) / 2;
                if (binomial(mid, n, k) < k) lo = mid + 1; // 当前的试验次数不能覆盖整个楼层,因此必须加一
                else hi = mid; // 当前的试验次数可以覆盖整个楼层,因此进一步细化试验次数范围
        }

        return lo;
}

int main(int argc, char ** argv) {
        if (argc < 3) {
                fprintf(stderr, "Please enter number of eggs and floors k\n");
                exit(1);
        }

        int n = atoi(argv[1]);
        int k = atoi(argv[2]);
        
        fprintf(stdout, "Minimum number of trails int wrost case with %d eggs and %d floors is %d\n", n, k, eggDropBinomial(n, k));

        exit(0);
}

这里采用的二项式系数计算公式如下所示:
在这里插入图片描述
复杂度分析:
 时间复杂度:主要时间花费在计算二项式系数上,结合二分查找的时间复杂度,总的时间复杂度为 O ( n log ⁡ 2 k ) O(n\log_2k) O(nlog2k)
 空间复杂度:只需要几个临时变量存储计算的中间值,因此空间复杂度为 O ( 1 ) O(1) O(1)

这应该是目前已知的最有效的解决该问题的算法。

参考:
https://www.geeksforgeeks.org/egg-dropping-puzzle-dp-11/
https://www.geeksforgeeks.org/eggs-dropping-puzzle-binomial-coefficient-and-binary-search-solution/
https://brilliant.org/wiki/egg-dropping/
http://www.cs.umd.edu/~gasarch/BLOGPAPERS/eggold.pdf

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值