前言
首先看一下這個題目,是Leetcode的第887題"雞蛋掉落":
你將獲得 `K` 個雞蛋,並可以使用一棟從 `1` 到 `N` 共有 `N` 層樓的建築。
每個蛋的功能都是一樣的,如果一個蛋碎了,你就不能再把它掉下去。
你知道存在樓層 `F` ,滿足 `0 <= F <= N` 任何從高於 `F` 的樓層落下的雞蛋都會碎,從 `F` 樓層或比它低的樓層落下的雞蛋都不會破。
每次*移動*,你可以取一個雞蛋(如果你有完整的雞蛋)並把它從任一樓層 `X` 扔下(滿足 `1 <= X <= N`)。
你的目標是**確切地**知道 `F` 的值是多少。
無論 `F` 的初始值如何,你確定 `F` 的值的最小移動次數是多少?
分析
這道題曾經是作為Google面試的一道題目,不過當時問的是:一幢 200 層的大樓,給你兩個雞蛋。如果在第 n 層扔下雞蛋,雞蛋不碎,那么從第 n-1 層扔雞蛋,都不碎。這兩只雞蛋一模一樣,不碎的話可以扔無數次。最高從哪層樓扔下時雞蛋不會碎?
因為有兩個雞蛋,所以我們可以考慮粗調和細調,即通過第一個雞蛋的試錯來縮小答案的范圍。比如第一個雞蛋在k層碎了,那么我們就可以確定臨界樓層是\([1,k)\)之間;所以我們首先考慮的應該就是第一個雞蛋應該在哪里扔。
假設第一個雞蛋的樓層策略是\(k_{1},k_{2},k_{3}....k_{p}\),其中p是扔的總次數,樓高為N
第二個雞蛋肯定是在\(k_{i}\)之間進行遍歷,所以優化這個問題就是求一個策略組合使得p的值最小。舉個例子,如果我們用二分法,選了k1=50扔,沒有碎,好的我們進入下一個狀態,取k2=75,仍然沒有碎,進入下一個狀態k3=90,碎了,說明臨界不碎樓層是在\([76,89]\)之間的,我們總共實驗了3+14=17次。我們這里選用二分法的k1、k2、k3就是一種可選的策略,換成其他的也可以,但是要使這個策略的p盡量的小就是我們的目標。
所以我們再仔細觀察一下上圖,發現小圓圈其實都是第二個雞蛋遍歷的過程,都是O(n),所以可以等價為一個操作,這個圖實際上也就是一個樹形結構:
如果這個樹類似下面的結構的話,我們可以得到最優解:
我們的樹結構最好滿足的關系為:
我們來解析一下第一個式子:\(k_{1}=k_{2}-k_{1}+1\),這是因為\(k_{1}-1=(k_{2}-k_{1}-1)+1\),加1是因為k2這個節點。要滿足每棵子樹的樹高相等,\(k_{p}\)必須是一個遞減等差數列。所以我們可以令\(k_{p}\sim N\),所以就有\(\frac{k_{1}(k_{1}+1)}{2}=N\)
回到Google這道面試題上來的話,就是\(\frac{x()(x+1)}{2}=200\),解得\(x=14\),所以扔蛋的一種策略為14,27,39,50,60,69,77,84,90,95,99,一共需要嘗試11+(13+12+11+10+9+8+7+6+5+4)/10=11+8.5 -->=20次。
然后再回到Leetcode這道題上來,這道題用動態規划來做可能更加簡便,當然用我們剛才分析這道題的樹形結構來分析也是可以的。
動態規划
首先的先暫時拋開我們剛才樹形結構的分析,這里先不討論粗調與細調的概念,就是一個線性結構:
思路1:
我們需要求一個最優決策使得扔的次數最小,雖然實際扔的次數會隨着真實結果而變化,但是k個雞蛋來測N層可以借助於k-1個雞蛋測N層的結果。
我們用dp[n][k]
表示k個雞蛋測n層的扔的次數。如果i層的時候雞蛋碎了,剩下來的k-1個雞蛋用來測i-1層,也就是dp[n][k]=dp[i-1][k-1]+1
;如果i層的時候雞蛋沒有碎,那么剩下來的k個雞蛋用來測n-i個樓層。所以,在第i層扔,會用 max(dp[i-1][k-1], dp[n-i][k]) + 1
,即$$dp[n][k] = min(max(dp[i-1][k-1], dp[n-i][k]) + 1 ) (1 <= i <= n)$$
思路2:
我們可以改變一下求解的思路,求k個雞蛋在m步內可以測出多少層:
假設: dp[k][m] 表示k個雞蛋在m步內最多能測出的層數。
那么,問題可以轉化為當 k <= K 時,找一個最小的m,使得dp[k][m]
<= N。
我們來考慮下求解dp[k][m]
的策略:
假設我們有k個雞蛋第m步時,在第X層扔雞蛋。這時候,會有兩種結果,雞蛋碎了,或者沒碎。
如果雞蛋沒碎,我們接下來會在更高的樓層扔,最多能確定 X + dp[k][m-1]
層的結果;
如果雞蛋碎了,我們接下來會在更低的樓層扔,最多能確定 Y + dp[k-1][m-1]
層的結果 (假設在第X層上還有Y層)。
因此,這次扔雞蛋,我們最多能測出 dp[k-1][m-1]
(摔碎時能確定的層數) + dp[k][m-1]
(沒摔碎時能確定的層數) + 1 (本層) 層的結果。
另外,我們知道一個雞蛋一次只能測一層,沒有雞蛋一層都不能測出來。
因此我們可以列出完整的遞推式:
dp[k][0] = 0
dp[1][m] = m (m > 0)
dp[k][m] = dp[k-1][m-1] + dp[k][m-1] + 1 (k > 0, m>0)
先給出代碼:
class Solution {
public:
int superEggDrop(int K, int N) {
if(K==0) return 0;
if(K==1) return N;
int dp[N+2][K+2];
memset(dp,0,sizeof(dp));
dp[0][0]=0;
for(int i=1;i<=N;++i)
{
dp[i][0]=0;
for(int j=1;j<=K;++j)
{
dp[i][j]=dp[i-1][j]+dp[i-1][j-1]+1;
if(dp[i][j]>=N)
return i;
}
}
return N;
}
};
樹形結構
我們仍然可以從樹形結構的角度去想,即盡量的把雞蛋的數量往2上面去接近,扔一個雞蛋數量就少一個;而這個思路其實是和動態規划思路1是一致的。
在知乎上也有關於這道題的討論,見這里