扔鸡蛋问题全解(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=",")

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

你可能感兴趣的:(扔鸡蛋问题全解(Egg_Drop_Puzzle))