高樓扔雞蛋問題 動態規劃 反推演繹

2021-08-21 06:53:37 字數 4193 閱讀 7959

對於高樓扔雞蛋問題,本文嘗試反其道而行之:首先描述乙個普適的高樓扔雞蛋問題,然後利用動態規劃法解決扔雞蛋次數的問題,最後由獲取次數的答案反推出扔雞蛋的方法。

這種由次數答案反推出方法的演繹方式令人有點震驚,似乎不同於常見的人類思考方式,有點像「先假設再證明」一樣。感覺這個題目還有繼續深挖的可能,比如找出數學上的證明或直接的扔雞蛋方法。

有m層樓和n個雞蛋,要找到扔下雞蛋而雞蛋不碎的最低樓層(也稱為「臨界樓層」),最少嘗試多少次就一定能找到這個「最低樓層」? 

這個題意其實並不那麼直觀,有以下的幾個注意點:

1. 因為雞蛋數量有限,所以不可能使用二分法解題。比如,有100層樓和1個雞蛋,那麼只好從第1層開始,逐層往上試。所以,在最壞情況下,要試驗100次,即答案為100.

2. 不能說「最少只需要嘗試2次就能找出最低摔不碎的樓層」,因為「2次就找到」的情況並不能保證「一定」找到,只是碰巧找到而已。

3. 具體使用什麼方法扔雞蛋,現在可以不知道!但又要知道的一點是,從不同層開始第一次扔雞蛋所得到的最壞情況下的最少嘗試次數是不同的。

這一點可能比較難以理解。這等於是:

我們首先假設必然存在一種方法,該方法一定能夠找出臨界樓層,但是該方法以不同樓層為第一次扔雞蛋的樓層時,最壞情況下所需的扔雞蛋次數是不同的。比如,100層樓+2個雞蛋,我以第50層為初始樓層,在其最壞情況下獲得臨界樓層,所需扔雞蛋次數為a次;而以第14層為初始樓層,在其最壞情況下獲得臨界樓層,所需扔雞蛋次數為b次。

a不等於b;且50層初始樓層和14層初始樓層其各自的最壞情況也是不同的。最終答案是以每一層為初始樓層時所有答案中的最小值。

本題採用動態規劃法解題。簡單來說,動態規劃解題分為2步:

1. 定義狀態轉移方程

2. 利用狀態轉移方程,自底向上求解

首先,假設樓層數為m,雞蛋數為n,函式為 f(m, n), 函式的返回值為答案,即「以各樓層為初始樓層時,在其各自最壞情況下的嘗試次數的最小值」。

假設初始樓層為第x層,在該層摔雞蛋,

1> 若沒有碎,則繼續嘗試更高的 m-x 層,因此 f(m, n) = 1 + f(m-x, n)

2> 若破碎了,則繼續嘗試更低的 x-1 層,因此 f(m, n) = 1 + f(x-1, n-1)

所以,以第x層為初始樓層的最壞情況就是以上二者的最大值,即:

1 + max(f(m-x, n), f(x-1, n-1))  (當x為初始樓層)
因為每層樓都可能作為初始樓層來扔雞蛋,所以,實際上存在某一樓層為初始樓層,其嘗試次數可以達到最少。也就是說:

f(m, n) = min(1 + max(f(m-x, n), f(x-1, n-1)))   # x 屬於 [1, m]
這樣,就可以寫程式了。當找到嘗試次數的最小值的時候,也就找到了初始樓層。這就為我們反推出扔雞蛋的方法也提供了方法。程式如下(對於100層樓+2個雞蛋,當然,樓的層數和雞蛋的個數是可以設定的):

cache = dict()

floors_result = dict()  # each floor's max trying number

def f(m, n, first_level=false):

if m == 0:

return 0

if m == 1:

return 1

if n == 1:

return m

m_cache = cache.get(m)

if m_cache and m_cache.get(n):

return cache[m][n]

result_list = list()

for i in range(m):

i += 1

result = max(f(m-i, n), f(i-1, n-1)) + 1

if first_level:

floors_result[i] = result

result = min(result_list)

if not cache.get(m):

cache[m] = dict()

cache[m][n] = result

return result

def main():

m = 100

n = 2

result = f(m, n, true)

for flr, res in floors_result.items():

if res == result:

first_floor = flr

break

print("f(%s, %s) = %s, first floor is %s" % (m, n, result, first_floor))

print("{}".format(floors_result))

main()

程式的列印結果如下:

$ python 1.py

f(100, 2) = 14, first floor is 9

由上可知,

對於100層樓+2個雞蛋,首次可以在第9層至第14層中的任何一層扔都可以!最壞情況下需要扔14次雞蛋才能確定臨界樓層。

假設第一次是在第14層扔的,沒有碎,那麼第二次扔雞蛋應該在哪層呢?

這個時候就很簡單了:

如果在第14層扔雞蛋破碎了,那麼後面就從第1層開始逐層向上嘗試,最多嘗試13次,即 f(13, 1) = 13

如果在第14層扔雞蛋沒有碎,那麼相當於要求解 f(100-14, 2) = f(86,2) 由程式中的cache可知,f(86, 2) = 13, 所以第二次嘗試的樓層應該為 14+13=27 層。 然後,27層又分碎了和沒碎兩種情況,可按照如上的方法繼續推理,直至找到臨界樓層。

於是,以上就確定了扔雞蛋的方法了。

那麼,對於100層樓+2個雞蛋,可以使用以下程式得到「一直摔不碎第乙個雞蛋的情況下的所有嘗試樓層」,故以下程式不適用大於2個雞蛋的情況,但對於大於2個雞蛋的情況,通過上一段描述的方法,也可以得到答案。

cache = dict()

floors_result = dict()  # each floor's max trying number

def f(m, n, first_level=false):

if m == 0:

return 0

if m == 1:

return 1

if n == 1:

return m

m_cache = cache.get(m)

if m_cache and m_cache.get(n):

return cache[m][n]

result_list = list()

for i in range(m):

i += 1

result = max(f(m-i, n), f(i-1, n-1)) + 1

if first_level:

floors_result[i] = result

result = min(result_list)

if not cache.get(m):

cache[m] = dict()

cache[m][n] = result

return result

def main():

m = 100

n = 2

floor_list = list()    

result = f(m, n, true)

floor = -1

for flr, res in floors_result.items():

if res == result:

floor = flr

break

while floor <= m - 1:

result = f(m-floor, n)

floor += result

print(floor_list)

main()

執行結果是:

[9, 22, 34, 45, 55, 64, 72, 79, 85, 90, 94, 97, 99, 100]
我們平常搜尋「高樓扔雞蛋」,看到解答一般都是100層+2個雞蛋對應的都是從14層開始扔,最多14次找到臨界樓層。但是由以上的分析可知,其實從9-14層的任何一層,都可以是最多14次找到臨界樓層。

(完)

扔雞蛋 (動態規劃)

你將獲得k個雞蛋,並可以使用一棟從1到n共有n層樓的建築。每個蛋的功能都是一樣的,如果乙個蛋碎了,你就不能再把它掉下去。你知道存在樓層f,滿足0 f n任何從高於f的樓層落下的雞蛋都會碎,從f樓層或比它低的樓層落下的雞蛋都不會破。你的目標是確切地知道f的值是多少。無論f的初始值如何,你確定f的值的最...

每日一道演算法題 高樓扔雞蛋問題 動態規劃問題

題目是這樣 你面前有一棟從 1 到n共n層的樓,然後給你k個雞蛋 k至少為 1 現在確定這棟樓存在樓層0 f n,在這層樓將雞蛋扔下去,雞蛋恰好沒摔碎 高於f的樓層都會碎,低於f的樓層都不會碎 現在問你,最壞情況下,你至少要扔幾次雞蛋,才能確定這個樓層f呢?首先我們來看一下,什麼叫做最壞情況下,至少...

雞蛋掉落問題 動態規劃

leetcode第887題雞蛋掉落 題目描述 思路1 這個題的問題是 如何在最小的移動次數內確定雞蛋可以掉碎的最低樓層?很明顯這是乙個最優化問題,也就是求f k,n 的最小值。既然時求最優解,我們就可以考慮使用動態規劃的方法來解。將情況分為以下幾種 1 樓層數為0或者雞蛋數為0,那最少的移動次數就是...