概述
扔雞蛋問題是一道非常經典的面試題,Google、百度、騰訊等大廠都使用過,此題有多個變體版本,擴充套件性很強,解決思路有多種,下面一起來探討吧!
標準版面試題
題目描述
有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次。
最壞的情況是在第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
動態規劃求出扔雞蛋問題的通解 1
什麼是動態規劃?
動態規劃(英語: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的測試用例,一直超時
動態規劃求出扔雞蛋問題的通解 2
上面的解決辦法,時間複雜度相當高,那麼是否存在更快的演算法呢?
上面的演算法中,主要在於三層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
}
總結
- 對於類似的智力題,如果不能想出其它辦法,我們可以採用先假設存在某個滿足結果的最優解x,然後代入上下文進行分析問題,歸納演繹,找出規律
- 對於很複雜的問題,可能需要非常發散的思維,對演算法進行高度抽象化
- 思考問題的過程很燒腦,與君共勉!
GitHub
- 專案原始碼在這裡
- 筆者會一直維護該專案,對leetcode中的演算法題進行解決,並寫下自己的思路和見解,致力於人人都能看懂的演算法
個人公眾號
- 喜歡的朋友可以關注,謝謝支援
- 來自:吳名(微訊號:wm497735138),作者:吳名