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

TomorrowWu發表於2019-02-16

leetcode-0887_雞蛋掉落

概述

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

image

方法二:平方根法

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

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

因此,我們嘗試每10層扔一次,第一次從10層扔,第二次從20層扔,第三次從30層……一直扔到100層。

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

最壞的情況是在第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

相關文章