扔雞蛋問題是一道很是經典的面試題,Google、百度、騰訊等大廠都使用過,此題有多個變體版本,擴展性很強,解決思路有多種,下面一塊兒來探討吧!github
有2個雞蛋,從100層樓上往下扔,以此來測試雞蛋的硬度。好比雞蛋在第9層沒有摔碎,在第10層摔碎了,那麼雞蛋不會摔碎的臨界點就是9層。 問:如何用最少的嘗試次數,測試出雞蛋不會摔碎的臨界點?
舉例:面試
舉個栗子,最笨的測試方法是什麼樣呢? 把其中一個雞蛋從第1層開始往下扔。 若是在第1層沒碎,換到第2層扔 若是在第2層沒碎,換到第3層扔 ....... 若是第59層沒碎,換到第60層扔 若是第60層碎了,說明不會摔碎的臨界點是第59層 在最壞狀況下,這個方法須要扔100次。
初看此題,部分同窗可能會以爲,這不就至關於從1-100中,找到某個數麼?採用二分法最快,下面咱們推演一番
採用相似於二分查找的方法,把雞蛋從一半樓層(50層)往下扔。算法
若是第一枚雞蛋在50層碎了,第二枚雞蛋就從第1層開始扔,一層一層增加,一直扔到第49層。
若是第一枚雞蛋在50層沒碎了,則繼續使用二分法,在剩餘樓層的一半(75層)往下扔......數組
這個方法在最壞狀況下,須要嘗試50次(100/2)。微信
如何讓第一枚雞蛋和第二枚雞蛋的嘗試次數儘量均衡呢?函數
很簡單,作一個平方根運算,100的平方根是10。測試
所以,咱們嘗試每10層扔一次,第一次從10層扔,第二次從20層扔,第三次從30層......一直扔到100層。優化
這樣的最好狀況是在第10層碎掉,嘗試次數爲 1 + 9 = 10次。spa
最壞的狀況是在第100層碎掉,嘗試次數爲 10 + 9 = 19次。
這裏有一個優化點,好比咱們能夠從15層開始扔,接下來25,35....一直到95層,最快狀況下是第95層碎掉,嘗試次數爲 9+9 = 18次
中學開始,同窗們都學過方程,假設存在一個未知數X知足條件,根據已知條件列出一元n次方程,求解,下面咱們根據題目描述,推出這個方程式
假設問題存在最優解(扔雞蛋過程),這個解的最壞狀況嘗試次數是x次,那麼,咱們第一次扔雞蛋該選擇哪一層?
偏偏是從第x層開始扔,選擇更高一層或是更低一層都不合適
爲何第一次扔就要選擇第x層呢?
這裏的解釋也是經過假設法,而後演繹,有些燒腦,小夥伴們堅持住:
假設第一次扔在第x+1層(比x大):
若是第一個雞蛋碎了,那麼第二個雞蛋只能從第1層開始一層一層扔,一直扔到第x層。
這樣一來,咱們總共嘗試了x+1次,和假設嘗試x次相悖。因而可知,第一次扔的樓層必須小於x+1層。
假設第一次扔在第x-1層(比x小):
若是第一個雞蛋碎了,那麼第二個雞蛋只能從第1層開始一層一層扔,一直扔到第x-2層。
這樣一來,咱們總共嘗試了x-2+1 = x-1次,雖然沒有超出假設次數,但彷佛有些過於保守。
假設第一次扔在第x層:
若是第一個雞蛋碎了,那麼第二個雞蛋只能從第1層開始一層一層扔,一直扔到第x-1層。
這樣一來,咱們總共嘗試了x-1+1 = x次,剛恰好沒有超出假設次數。
所以,要想盡可能樓層跨度大一些,又要保證不超過假設的嘗試次數x,那麼第一次扔雞蛋的最優選擇就是第x層。
以上都是假設+邏輯推理,並無通過嚴格的數學證實,咱們也不是數學家
若是第一次扔雞蛋沒有碎,咱們的嘗試消耗了一次,問題就轉化成了兩個雞蛋在100-x層樓往下扔,要求嘗試次數不得超過x-1次
因此第二次嘗試的樓層跨度是x-1層,絕對樓層是x+(x-1)層
同理,若是雞蛋尚未碎,第三次樓層跨度是x-2,第四次是x-3
小夥伴們,到此看出了規律沒?根據總結,能夠列出一個樓層數的方程式:
x + (x-1) + (x-2) + ... + 1 = 100
下面咱們來解這個這個方程:
(x+1)*x/2 = 100
最終x向上取整,獲得 x=14
所以,最優解在最壞狀況的嘗試次數是14次,第一次扔雞蛋的樓層也是14層。
最後,讓咱們把第一個雞蛋沒碎的狀況下,所嘗試的樓層數完整列舉出來:
14,27, 39, 50, 60, 69, 77, 84, 90, 95, 99, 100
舉個栗子驗證下: 假如雞蛋不會碎的臨界點是65層,那麼第一個雞蛋扔出的樓層是14,27,50,60,69。這時候啪的一聲碎了。 第二個雞蛋繼續,從61層開始,61,62,63,64,65,66,啪的一聲碎了。 所以獲得不會碎的臨界點65層,總嘗試次數是 6 + 6 = 12 < 14 。
你將得到 K 個雞蛋,並可使用一棟從 1 到 N 共有 N 層樓的建築。
每一個蛋的功能都是同樣的,若是一個蛋碎了,你就不能再把它掉下去。
你知道存在樓層 F ,知足 0 <= F <= N 任何從高於 F 的樓層落下的雞蛋都會碎,從 F 樓層或比它低的樓層落下的雞蛋都不會破。
每次移動,你能夠取一個雞蛋(若是你有完整的雞蛋)並把它從任一樓層 X 扔下(知足 1 <= X <= N)。
你的目標是確切地知道 F 的值是多少。
不管 F 的初始值如何,你肯定 F 的值的最小移動次數是多少?
示例1:
輸入:K = 1, N = 2 輸出:2 解釋: 雞蛋從 1 樓掉落。若是它碎了,咱們確定知道 F = 0 。 不然,雞蛋從 2 樓掉落。若是它碎了,咱們確定知道 F = 1 。 若是它沒碎,那麼咱們確定知道 F = 2 。 所以,在最壞的狀況下咱們須要移動 2 次以肯定 F 是多少。
示例2:
輸入:K = 2, N = 6 輸出:3
示例3:
輸入:K = 3, N = 14 輸出:4
提示:
1. 1 <= K <= 100 2. 1 <= N <= 10000
動態規劃(英語:Dynamic programming,簡稱DP)是一種在數學、管理科學、計算機科學、經濟學和生物信息學中使用的,經過把原問題分解爲相對簡單的子問題的方式求解複雜問題的方法。
動態規劃解決問題的過程分爲兩步:
1.尋找狀態轉移方程式
2.利用狀態轉移方程式自底向上求解問題
在標準版問題中,兩個雞蛋100層樓的條件下,咱們找到的規律:
假設存在最優解,在最壞狀況下嘗試次數是x,那麼第一個雞蛋首次扔出的樓層也是x
這個規律在三個以上雞蛋的條件下還可否適用呢?
假設有三個雞蛋,100層樓,第一個雞蛋扔在第10層並摔碎了。這時候咱們還剩下兩個雞蛋,所以第二個雞蛋沒必要從底向上一層一層扔,而是能夠選擇在第5層扔。若是第二個雞蛋也摔碎了,那麼第三個雞蛋才須要老老實實從第1層開始一層一層扔。 這樣一來,總的嘗試次數是1+1+4 = 6 < 10(最少次數)。 所以,最優解的最壞狀況下嘗試次數是 X,雞蛋首次扔出的樓層也是 X 這個規律再也不成立。
那麼,咱們如何尋找規律呢?
在這裏,咱們把M層樓/N個雞蛋的問題,抽象成一個黑盒子函數F(M,N),樓層數M和雞蛋數N是函數的兩個參數,函數的返回值是最優解的最大嘗試次數
假設咱們第一個雞蛋扔出的位置在第X層(1<=X<=M),會出現兩種狀況:
1.第一個雞蛋沒碎
那麼剩餘的M-X層樓,剩餘N個雞蛋,能夠轉變爲下面的函數:
F(M-X,N)+ 1,1<=X<=M
2.第一個雞蛋碎了
那麼只剩下從1層到X-1層樓須要嘗試,剩餘的雞蛋數量是N-1,能夠轉變爲下面的函數:
F(X-1,N-1) + 1,1<=X<=M
總體而言,咱們要求出的是 N層樓 / K個雞蛋 條件下,最大嘗試次數最小的解,因此這個題目的狀態轉移方程式以下:
X能夠爲1......N,因此有M個Max( F(N-X,K)+ 1, F(X-1,K-1) + 1)的值,最終F(N,K)是這M個值中的最小值,即最優解
F(N,K)= Min(Max( F(N-X,K)+ 1, F(X-1,K-1) + 1)),1<=X<=N
狀態轉移方程式有了,如何計算出這個方程式的結果呢?
誠然,咱們能夠用遞歸的方式來實現。可是遞歸的時間複雜度是指數級的,當M和N的值很大的時候,遞歸的效率會變得很是低。
根據動態規劃的思想,咱們能夠自底向上來計算出方程式的結果。
何謂自底向上呢?讓咱們以3個雞蛋,4層樓的狀況爲例來進行演示。
根據動態規劃的狀態轉移方程式和自底向上的求解思路,咱們須要從1個雞蛋1層樓的最優嘗試次數,一步一步推導後續的狀態,直到計算出3個雞蛋4層樓的嘗試次數爲止。
首先,咱們能夠填充第一個雞蛋在各個樓層的嘗試次數,以及任意多雞蛋在1層樓的嘗試次數。
緣由很簡單:
1.只有一個雞蛋,因此沒有任何取巧方法,只能從1層扔到最後一層,嘗試次數等於樓層數量。
2.只有一個樓層,不管有幾個雞蛋,也只有一種扔法,嘗試次數只多是1。
按照上面的方程式,代入計算,得出下面的結果。具體計算過程就不細說了
根據剛纔的思路,代碼初步實現:
func superEggDrop(K, N int) int { if K < 1 || N < 1 { return 0 } //備忘錄,存儲K個雞蛋,N層樓條件下的最優化嘗試次數 //cache := [K + 1][N + 1]int{} cache := make([][]int, K+1) //把備忘錄每一個元素初始化成最大的嘗試次數 for i := 0; i <= K; i++ { cache[i] = make([]int, N+1) for j := 1; j <= N; j++ { cache[i][j] = j } } for n := 2; n <= K; n++ { for m := 1; m <= N; m++ { //假設樓層數能夠是1---N, min := cache[n][m] for k := 1; k < m; k++ { //M層,N雞蛋,F(N,K)= Min(Max( F(N-X,K)+ 1, F(X-1,K-1) + 1)),1<=X<=N //(動態規劃) //雞蛋碎了 max := cache[n-1][k-1] + 1 if cache[n][m-k]+1 > max { max = cache[n][m-k] + 1 //雞蛋沒碎 } if max < min { min = max } } cache[n][m] = min } } return cache[K][N] }
三層循環,時間複雜度是O(K*N*N)
二維數組:空間複雜度是O(M*N)
時間複雜度過高,沒法經過leetcode的測試用例,一直超時
上面的解決辦法,時間複雜度至關高,那麼是否存在更快的算法呢?
上面的算法中,主要在於三層for循環,須要假設第一次扔雞蛋分別從第1.....N層
有沒有一種算法,結合概括演繹和動態規劃的思想,在這裏能夠進一步抽象?
假設移動x次,k個雞蛋,最優解的最壞條件下能夠檢測n層樓,層數n=黑箱子函數f(x,k) 假設從n0+1層丟下雞蛋, 1,雞蛋破了 剩下x-1次機會和k-1個雞蛋,能夠檢測n0層樓 2, 雞蛋沒破 剩下x-1次機會和k個雞蛋,能夠檢測n1層樓 那麼 臨界值層數F在[1,n0+n1+1]中的任何一個值,都都能被檢測出來 概括的狀態轉移方程式爲:f(x,k) = f(x-1,k-1)+f(x-1,k)+1,即x次移動的函數值能夠由x-1的結果推導,這個思路很抽象,須要花時間去理解,具體看代碼,對照着代碼理解 能夠簡化爲黑箱子函數的返回值只跟雞蛋個數k有關係: 本次fun(k) = 上次fun(k-1)+上次fun(k)+1
時間複雜度是O(K*moves),跟樓層數無關(樓層數N的值相對很大)
func superEggDrop(K, N int) int { moves := 0 dp := make([]int, K+1) // 1 <= K <= 100 // dp[i] = n 表示, i 個雞蛋,利用 moves 次移動,最多能夠檢測 n 層樓 for dp[K] < N { for i := K; i > 0; i-- { //逆序從K---1,dp[i] = dp[i]+dp[i-1] + 1 至關於上次移動後的結果,dp[]函數要理解成抽象出來的一個黑箱子函數,跟上一次移動時雞蛋的結果有關係 dp[i] += dp[i-1] + 1 // 以上計算式,是從如下轉移方程簡化而來 // dp[moves][k] = 1 + dp[moves-1][k-1] + dp[moves-1][k] // 假設 dp[moves-1][k-1] = n0, dp[moves-1][k] = n1 // 首先檢測,從第 n0+1 樓丟下雞蛋會不會破。 // 若是雞蛋破了,F 必定是在 [1:n0] 樓中, // 利用剩下的 moves-1 次機會和 k-1 個雞蛋,能夠把 F 找出來。 // 若是雞蛋沒破,假如 F 在 [n0+2:n0+n1+1] 樓中 // 利用剩下的 moves-1 次機會和 k 個雞蛋把,也能夠把 F 找出來。 // 因此,當有 moves 個放置機會和 k 個雞蛋的時候 // F 在 [1, n0+n1+1] 中的任何一樓,都可以被檢測出來。 } moves++ } return moves }