雞蛋問題

你將得到 K 個雞蛋,並可使用一棟從 1N 共有 N 層樓的建築。算法

每一個蛋的功能都是同樣的,若是一個蛋碎了,你就不能再把它掉下去。編程

你知道存在樓層 F ,知足 0 <= F <= N 任何從高於 F 的樓層落下的雞蛋都會碎,從 F 樓層或比它低的樓層落下的雞蛋都不會破。函數

每次移動,你能夠取一個雞蛋(若是你有完整的雞蛋)並把它從任一樓層 X 扔下(知足 1 <= X <= N)。測試

你的目標是 確切地 知道 F 的值是多少。優化

不管 F 的初始值如何,你肯定 F 的值的最小移動次數是多少?code

示例1:blog

輸入:K = 1, N = 2
輸出:2
解釋:
雞蛋從 1 樓掉落。若是它碎了,咱們確定知道 F = 0 。
不然,雞蛋從 2 樓掉落。若是它碎了,咱們確定知道 F = 1 。
若是它沒碎,那麼咱們確定知道 F = 2 。
所以,在最壞的狀況下咱們須要移動 2 次以肯定 F 是多少。

示例2:數學

輸入:K = 2, N = 6
輸出:3

示例1:class

輸入:K = 3, N = 14
輸出:4

提示:方法

  1. 1 <= K <= 100
  2. 1 <= N <= 10000

動態規劃

什麼是動態規劃

動態規劃英文 Dynamic Programming, 是求解決策過程最優化的數學方法,後來沿用到了編程領域。

動態規劃的大體思路是把一個複雜的問題轉化成一個分階段逐步遞推的過程,從簡單的初始狀態一步一步遞推,最終獲得複雜問題的最優解。

動態規劃的過程

  1. 尋找狀態轉移方程式
  2. 利用狀態方程是自底向上求解問題

因而咱們能夠用動態規劃的方法來解決此雞蛋問題。動態規劃的關鍵就在於尋找到狀態轉移方程式。在此問題中,咱們能夠把m層樓和n個雞蛋的問題轉化成一個函數F(m,n),其中樓層m和雞蛋n是函數的兩個參數,而函數的值則是最優解的最大嘗試次數。

假設咱們第一個雞蛋認出的位置在第x層(1<=x<=m),會出現兩種狀況:

  1. 雞蛋沒碎

那麼剩餘m-x層數,剩餘n個雞蛋,能夠轉化爲如下的函數:

F(m-x, n) + 1, 1 <= x <= m

  1. 雞蛋碎了

那麼剩餘x-1層數,剩餘n-1個雞蛋,能夠轉化爲如下的函數:

F(x-1, n-1) + 1, 1 <= x <= m

根據這兩個狀態轉移式,咱們能夠得知要在最差狀態下的到最優解,首先最差的狀態就是

max{F(m-x, n) + 1,F(x-1, n-1) + 1}

最優解就是

min{max{F(m-x, n) + 1,F(x-1, n-1) + 1}, 1 <= x <= m}

算出此狀態轉移式後,咱們還須要知道一個邊界狀態,經過分析咱們能夠發現,1. 只有一層樓時不管多少雞蛋都只須要一次便可得出結論。2.只有一個雞蛋時,有多少層就須要試多少層。因此有如下的式子:

F(1, n) = 1;
F(m, 1) = m;

有了這些完整的狀態轉移方程式,咱們就能夠從底層開始逐漸向上求解,下面就用表格展現一下F(4,3):

F(4,3)初始狀態

首先根據邊界條件,能夠填入出第一行和第一列的值:

填入邊界狀態

接下來就要計算F(2,2)了,套用上面的狀態轉移方程式:

F(2,2) = min{max{F(2-x, 2) + 1,F(x-1, 1) + 1}, 1 <= x <= 2}

計算後能夠得出F(2,2) = 2,因而將2填入表格中:

填入邊界狀態

而後依次計算得出後面的值,最終能夠獲得F(4,3)的結果,以下表:

填入邊界狀態

最終得出F(4,3)=3,代碼的實現以下

int superEggDrop(int eggs, int floors) {
    int i, j, k, t, max;
 
    int temp[eggs + 1][floors + 1];
 
    for(i = 0; i < floors + 1; ++i)
    {
        temp[0][i] = 0;
        temp[1][i] = i;
    }
 
    for(i = 2; i < eggs + 1; ++i)
    {
        temp[i][0] = 0;
        temp[i][1] = 1;
    }
 
    for(i = 2; i < eggs + 1; ++i)
    {
        for(j = 2; j < floors + 1; ++j)
        {
            for(k = 1, max = UINT_MAX; k < j; ++k)
            {
                t = temp[i][j - k] > temp[i - 1][k -1] ?  temp[i][j - k] : temp[i - 1][k -1];
 
                if(max > t)
                {
                    max = t;
                }
            }
 
            temp[i][j] = max + 1;
        }
    }
 
    return temp[eggs][floors];
}

該算法的空間複雜度是O(nm),時間複雜度是O(nm^2),很顯然這個時間複雜度太高,沒法知足題目要求,因此如今須要進一步優化此算法。

首先這個算法是咱們根據狀態轉移方程式實現的,因此要想在時間上進行優化,有很大的苦難。因而咱們就須要轉換咱們的思惟,咱們先遞推出30層樓,4個雞蛋的表格:

填入邊界狀態

經過分析表格,咱們能夠發現F(26,2)=7,F(27,2)=7,F(28,2)=7,此時若再加一層F(29,2)=8。2個雞蛋,在22-28層時測試7次就能夠解決問題,但是當到了第29層時就會須要多測試一次。因而咱們能夠引出一下的問題:

n個雞蛋,測試m次(簡記爲D(n,m)),最大能夠解決幾層樓的問題

經過對遞推結果表格的觀察,咱們能夠獲得以下結論:

  1. D(1,m) = m;
  2. D(n,n) = 2^n - 1;
  3. D(1,m){m <= n} = D(m,m);

對於第二點,以D(4,4)爲例,咱們第1次在8樓扔下雞蛋,若是碎了,則第二次在4樓扔下雞蛋,不然在12樓扔下雞蛋,對於在4樓扔下雞蛋的狀況,以後能夠分別在2樓或者6樓扔下雞蛋,如此進行,就能夠找到答案樓層,方法與二分查找同樣。例如答案樓層是5的狀況,測試序列爲8,4,6,5。

對於第三點,若是有5個雞蛋讓你測試3次,即便三次測試雞蛋都碎了,剩下的2個雞蛋也派不上用場,因此D(5,3) = D(3,3)

發現這些關係以後,咱們彷佛找到解決n個雞蛋測試m次最大可以解決樓層數的方法。對於D(n,m){n < m}而言,對於其可以測試的最大樓層數k,咱們能夠構造這樣的場景,將第一顆雞蛋仍在樓層i,使得第i + 1層到第k層是D(n,m-1)能夠解決的最大樓層數,第1層到第i - 1層是D(n-1,m-1)能夠解決的最大樓層數,由此獲得遞推關係D(n,m) = D(n -1,m-1) + 1 + D(n,m-1),而後對D(n,m-1),D(n-1,m-1)再按照上述公式分解,直到得出剛纔所列的三種可計算狀況(n = 1,或者m <= n)爲止,再進行回溯累加,就能夠獲得D(n,m)的值,代碼以下:

int DroppingMax(int eggs, int times)
{
    if(eggs == 1)
    {
        return times;
    }
 
    if(eggs >= times)
    {
        return (int)pow(2, times) - 1;
    }
 
    return DroppingMax(eggs, times -1) + DroppingMax(eggs -1, times - 1) + 1;
}

根據此算法,咱們能夠得出D(2,5)=15,D(2,8)=36,也就是說,2個雞蛋測試5次最多能夠解決15層樓的問題,測試8次最多能夠解決36層樓的問題。可見,出這個題的人並非隨便找兩個樓層數陪我們玩玩,而是對此問題認真研讀後的結果。有了此利器以後,咱們解決扔雞蛋問題的的方法將獲得大幅簡化,對於n個雞蛋解決k層樓的問題咱們只需找到這樣的值m,使得D(n,m-1)< k <=D(n,m),代碼以下

int superEggDrop(int eggs, int floors)
{
    int times = 1;
 
    while(DroppingMax(eggs, times) < floors)
    {
        ++times;
    }
 
    return times;
}

該算法的時間和空間複雜度不太好分析,但都要好於傳統的DP算法,有興趣的讀者能夠推敲一下。

相關文章
相關標籤/搜索