扔鸡蛋问题全解(Egg_Drop_Puzzle)

扔鸡蛋问题全解(Egg_Drop_Puzzle)

参考网站

注:本文解题思路参照了上述链接中的内容,在此表示感谢。

问题描述

原题来源于谷歌面试题目:
假设你有2颗鸡蛋,和一栋36层高的楼,如今你想知道在哪一层楼之下,鸡蛋不会被摔碎,应该怎样用最少的測试次数对于不论什么答案楼层都可以使问题得到解决。
现将其一般化,即给定n个鸡蛋,对于k层楼,如果想知道在哪一层楼,鸡蛋不会被摔碎,问最少的次数是多少?
** 注意 **:最高层楼不一定会碎。

问题分析

目的是要找出连续的两层楼i,i+1在楼层i鸡蛋没有摔碎,在楼层i+1鸡蛋碎了,问题的关键之处在于,測试之前,你并不知道鸡蛋会在哪一层摔碎,你须要找到的是一种測试方案,这样的測试方案,不管鸡蛋会在哪层被摔碎,都至多仅仅须要m次測试,在全部这些測试方案中,m的值最小。

对1个鸡蛋的情况:

只能从第1层楼开始,一层层向上扔,直到鸡蛋摔碎为止(或直到顶层,)

对2个鸡蛋的情况:

还是分析原问题。
你可能会考虑先在第18层扔一颗,假设这颗碎了,则你从第1层,到第17层,依次用第2颗鸡蛋測试,直到找出答案。假设第1颗鸡蛋没碎,则你依旧能够用第1颗鸡蛋在27层进行測试,假设碎了,在第19~26层,用第2颗鸡蛋依次測试,假设没碎,则用第1颗鸡蛋在32层进行測试,……,如此进行(有点类似于二分查找)。这个解决方式的最坏情况出如今结果是第17/18层时,此时,你须要測试18次才干找到终于答案,所以该方案,解决36层楼问题的測试次数是18.
在此,你也可能想到,是不是可以缩小间隔,比如,原36层的楼,可将其分为4份,每部分9层,则第一次在第9层扔,如果碎了,则从第1层开始依次往上;如果没碎,在第18层开始扔,......, 依此类推。最坏的情况出现在第35/36层时,共需扔9 + 3 = 12次。
那是否会更简单的方法呢?
假设最多的次数为m次。
那第一层,最多应该从m层楼扔,如果碎了,则另一个鸡蛋从第1层开始往上扔,如果没碎,则变为2个鸡蛋,k - m层楼,最多扔m - 1次的问题, 则此时,最多从m-1层楼开始扔, ...... , 依此类推。
则只要满足:

 m + (m - 1) + (m - 2) + ... + 1 = 36

求解,可得 m = 8。即最多扔8次,就可以解决:第1次从8楼扔,如果碎了,从第1层依次往上;如果没碎,则从 8 + (8 - 1) = 15楼开始扔,... ,依此类推。

一般情况

将这种问题简记为W(n,k),当中n代表可用于測试的鸡蛋数,k代表被測试的楼层数。
则借鉴2个鸡蛋问题中的思路:
假设第一次从第i层楼开始扔,如果碎了,则问题变为W(n-1, i-1);如果没碎,问题变为W(n , k-i),取两者最大的,为第一次从第i层楼仍时所需的次数;则对于所有的i,选取最小的,即可解决。
用数学公式描述就是:
W(n, k) = 1 + min{max(W(n -1, i -1), W(n, k - i))}, i in {2, 3, ……,k},当中i是第一次的測试的楼层位置。

当中W(1,k) = k(相当于1颗鸡蛋測试k层楼问题),W(0,k) = 0,W(n, 0) = 0

反问题分析

聪明如你,做完上述题目后,肯定会问:给你两个鸡蛋,最多扔8次,则最多能解决多少层楼的问题呢?

一般的,将问题表述为:n个鸡蛋,測试m次(简记为D(n,m)),最大能够解决几层楼的问题。
一般的,我们可以知道:
D(1,m) = m

D(n,m){m <= n} = D(m,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)
根据上述递推式,则可以一般地求解。

Python编程实现

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from math import sqrt
from math import ceil
from math import log2
from math import pow
import numpy as np


class ThrowEgg(object):
    # 对特定的情形,保存输出的方案
    out_matrix = 0
    out_order = 0
    out_floor = 0
 

    def __get_out_matrix(self, floor_num, egg_num):
        
     self.out_matrix = np.zeros((floor_num + 1, egg_num + 1))
        self.out_order = np.zeros((floor_num + 1, egg_num + 1))
        # 循环遍历填充每一个元素
        for i in range(floor_num + 1):
            for j in range(egg_num + 1):
                self.out_matrix[i, j] = self.__cursive_throw_egg(i, j)

    def __cursive_throw_egg(self, floor_num, egg_num):
        """
            将问题定义为T(n, k),即n个鸡蛋k层楼的问题
            其中,
            T(0,i) = NULL 无穷大次  i >0
            T(1,k) = k
            T(a,b) = T(b,b) = b  其中a>=b

            """
        if floor_num == 0:
            self.out_order[floor_num, egg_num] = 0
            return 0

        if floor_num == 1:
            self.out_order[floor_num, egg_num] = 1
            return 1

        if egg_num == 0:
            self.out_order[floor_num, egg_num] = 0
            return 0

        if egg_num == 1:
            self.out_order[floor_num, egg_num] = 1
            return floor_num

        a = self.out_matrix[floor_num, egg_num]
        if a != 0:
            return a

        min_value = float("inf")
        first_throwing_muli = []
        for i in range(1, floor_num + 1):
            a = self.__cursive_throw_egg(i - 1, egg_num - 1)
            b = self.__cursive_throw_egg(floor_num - i, egg_num)
            max_sub = max(a, b)
            if max_sub <= min_value:  # 为什么相同的情况要,要用后出现的呢?
                min_value = max_sub
                first_throwing_muli.append(i)
        # 好像在此处,可以随意处理,正好也说明测试方案并不唯一
        first_throwing = (first_throwing_muli[0] + first_throwing_muli[-1]) / 2
        self.out_order[floor_num, egg_num] = ceil(first_throwing)

        return min_value + 1

    def throw_egg(self, floor_num, egg_num, k):
        # 仍然是通过递归求解?:不是递归,只需要将各子问题的第一个元素取出即可
        # 由于此方法中使用了矩阵保存所需要的数据,因此,需执行主方法
        floor_index = floor_num
        egg_index = egg_num
        k_index = k
        # 设置变量,用于记录楼层在变为子问题时,其基础的楼层数
        floor_agg = 0
        out_list = []
        throw_times = 0
        upper = floor_num
        lower = 1
        while floor_index >= 1:
            i = int(self.out_order[floor_index, egg_index])
            throw_times += 1
            out_list.append(lower - 1 + i)  # 需要减去1
            if i < k_index:
                # 如果鸡蛋没碎,变为楼层数floor - i , egg_num 个鸡蛋的子问题
                floor_index -= i
                k_index -= i
                lower += i

            else:
                upper = i - 1
                floor_index = i - 1
                egg_num -= 1

        return throw_times, out_list

    def throw_egg_binary(self, floor_num, certain_num):

        # 二分法求解楼层(不限制鸡蛋个数)
        out_list = []
        lower = 1
        upper = floor_num
        throw_times = 0
        eggs_needed = 0

        # 小心循环终止条件的判断
        while lower <= upper:
            middle = (lower + upper) // 2
            out_list.append(middle)
            throw_times += 1
            if middle >= certain_num:
                upper = middle - 1
                eggs_needed += 1
            else:
                lower = middle + 1

        return eggs_needed, throw_times, out_list

    def run(self, floor_num, egg_num=None, comp=False):
        """
        
        :param floor_num: 
        :param egg_num: 
        :param comp: 输出模式,为真时,对两个结果进行比较,如果为假,则自动根据
        egg_num是否有值选择执行方式 
        :return: 
        """

        if comp and not egg_num:
            print("egg_num is needed!")
            return

        if egg_num:
            self.__get_out_matrix(floor_num, egg_num)

        for i in range(1, floor_num + 1):
            if comp:
                eggs_needed, throw_times, out_list = self.throw_egg_binary(
                    floor_num, i)
                print(
                        "using binary method, if the very floor is %d, "
                        "it needs to throw %d times, and the throwing order is %s"
                        ", and %d eggs are needed" % (
                            i, throw_times, out_list, eggs_needed))
                throw_times, out_list = self.throw_egg(floor_num, egg_num, i)
                print(
                        "using general method, if the very floor is %d, "
                        "it needs to throw %d times, and "
                        "the throwing order is %s" % (i, throw_times, out_list))

            else:
                if egg_num:
                    throw_times, out_list = self.throw_egg(floor_num, egg_num,
                                                           i)
                    print(
                            "using general method, if the very floor is %d, "
                            "it needs to throw %d times, and "
                            "the throwing order is %s" % (
                            i, throw_times, out_list))
                else:
                    eggs_needed, throw_times, out_list = self.throw_egg_binary(
                        floor_num, i)
                    print(
                            "using binary method, if the very floor is %d, "
                            "it needs to throw %d times, and the throwing order is %s"
                            ", and %d eggs are needed" % (
                                i, throw_times, out_list, eggs_needed))

    def __cursive_throw_egg_reverse(self, egg_num, throw_times):
        """
        反问题,求解给定鸡蛋,给定次数,所能解决的最大楼层数k
        分析:用D(n,m)表示此问题,其中,n为鸡蛋个数,m为扔的次数。
        则重点在于分析出什么情况下所能确定的楼层数最大。
        假设我们在第i层楼扔第一个鸡蛋,则如果能保证D(n-1, m-1)可以解决i-1层楼,
        D(n, m-1)可以解决k-i层楼,则可以取得最大楼层数k
        即递推式:D(n, m) = D(n-1, m-1) + 1 + D(n, m-1)
        其中,
        n==0 or m == 0时,令结果为0;
        n==1 , 为m
        n >= m 时,为D(m,m)相同
        仍采用全局变量记录计算的中间过程
        :param egg_num: 
        :param throw_times: 
        :return: 能解决的数量
        """
        pass
        if egg_num == 0:
            self.out_floor[egg_num, throw_times] = 0
            return 0

        if throw_times == 0:
            self.out_floor[egg_num, throw_times] = 0
            return 0

        if egg_num == 1:
            self.out_floor[egg_num, throw_times] = throw_times
            return throw_times

        if self.out_floor[egg_num, throw_times] != 0:
            return self.out_floor[egg_num, throw_times]

        if egg_num > throw_times:
            temp = self.__cursive_throw_egg_reverse(throw_times, throw_times)
            self.out_floor[egg_num, throw_times] = temp
            return temp

        temp = (self.__cursive_throw_egg_reverse(egg_num - 1, throw_times - 1)
                + 1 + self.__cursive_throw_egg_reverse(egg_num, throw_times - 1))
        self.out_floor[egg_num, throw_times] = temp
        return temp

    def throw_egg_reverse(self,egg_num, throw_times):
        self.out_floor = np.zeros((egg_num + 1, throw_times + 1))
        # 循环遍历填充每一个元素
        for i in range(egg_num + 1):
            for j in range(throw_times + 1):
                self.__cursive_throw_egg_reverse(i, j)
        pass
    
    
if __name__ == '__main__':
    egg = ThrowEgg()
    # 计算楼层数与鸡蛋
    egg.run(36, 4, comp=True)
    np.savetxt("out_matrix.txt", egg.out_matrix[1:, 1:], fmt="%d",
               delimiter=",")
    np.savetxt("out_order.txt", egg.out_order[1:, 1:], fmt="%d",
               delimiter=",")
    # 计算给定鸡蛋与扔次数,所能解决的最大楼层数
    egg.throw_egg_reverse(2, 15)
    np.savetxt("out_floor.txt", egg.out_floor[1:, 1:], fmt="%d",
               delimiter=",")

更多的规律,可以在上述输出的文档中进一步的分析,留给读者自行思考。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343