一篇文章帶你搞定經典面試題之扔雞蛋問題

leetcode-0887_雞蛋掉落git

概述

扔雞蛋問題是一道很是經典的面試題,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)。微信

image

方法二:平方根法

如何讓第一枚雞蛋和第二枚雞蛋的嘗試次數儘量均衡呢?函數

很簡單,作一個平方根運算,100的平方根是10。測試

所以,咱們嘗試每10層扔一次,第一次從10層扔,第二次從20層扔,第三次從30層......一直扔到100層。優化

這樣的最好狀況是在第10層碎掉,嘗試次數爲 1 + 9 = 10次。spa

最壞的狀況是在第100層碎掉,嘗試次數爲 10 + 9 = 19次。

image

這裏有一個優化點,好比咱們能夠從15層開始扔,接下來25,35....一直到95層,最快狀況下是第95層碎掉,嘗試次數爲 9+9 = 18次

方法三:解方程法

中學開始,同窗們都學過方程,假設存在一個未知數X知足條件,根據已知條件列出一元n次方程,求解,下面咱們根據題目描述,推出這個方程式

假設問題存在最優解(扔雞蛋過程),這個解的最壞狀況嘗試次數是x次,那麼,咱們第一次扔雞蛋該選擇哪一層?

偏偏是從第x層開始扔,選擇更高一層或是更低一層都不合適

image

爲何第一次扔就要選擇第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

image

小夥伴們,到此看出了規律沒?根據總結,能夠列出一個樓層數的方程式:

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 。

進階版面試題

leetcode

題目描述

你將得到 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

image

這個規律在三個以上雞蛋的條件下還可否適用呢?

假設有三個雞蛋,100層樓,第一個雞蛋扔在第10層並摔碎了。這時候咱們還剩下兩個雞蛋,所以第二個雞蛋沒必要從底向上一層一層扔,而是能夠選擇在第5層扔。若是第二個雞蛋也摔碎了,那麼第三個雞蛋才須要老老實實從第1層開始一層一層扔。

這樣一來,總的嘗試次數是1+1+4 = 6 < 10(最少次數)。

所以,最優解的最壞狀況下嘗試次數是 X,雞蛋首次扔出的樓層也是 X 這個規律再也不成立。

那麼,咱們如何尋找規律呢?

在這裏,咱們把M層樓/N個雞蛋的問題,抽象成一個黑盒子函數F(M,N),樓層數M和雞蛋數N是函數的兩個參數,函數的返回值是最優解的最大嘗試次數

image

假設咱們第一個雞蛋扔出的位置在第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層樓的狀況爲例來進行演示。

image

根據動態規劃的狀態轉移方程式和自底向上的求解思路,咱們須要從1個雞蛋1層樓的最優嘗試次數,一步一步推導後續的狀態,直到計算出3個雞蛋4層樓的嘗試次數爲止。

首先,咱們能夠填充第一個雞蛋在各個樓層的嘗試次數,以及任意多雞蛋在1層樓的嘗試次數。

緣由很簡單:

1.只有一個雞蛋,因此沒有任何取巧方法,只能從1層扔到最後一層,嘗試次數等於樓層數量。

2.只有一個樓層,不管有幾個雞蛋,也只有一種扔法,嘗試次數只多是1。

image

按照上面的方程式,代入計算,得出下面的結果。具體計算過程就不細說了

image

代碼實現

根據剛纔的思路,代碼初步實現:

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),做者:吳名

image

相關文章
相關標籤/搜索