前言
大一的時候藍橋杯省賽遇到過(作為非編程題的壓軸題),這次看的別人的面經也多次出現,就寫篇博文總結一下。
題目
有一棟樓共100層,一個雞蛋從第N層及以上的樓層落下來會摔破, 在第N層以下的樓層落下不會摔破。給你2個雞蛋,設計方案找出N,並且保證在最壞情況下,最小化雞蛋下落的次數。
解析
無腦二分法(最多人想到的偽解法)
當時省賽沒注意審題,就想的這種方法,首先需要確定的是,在最壞的情況下,求最小化嘗試次數,所以肯定不是無腦二分那么簡單了。
例如,你第一次扔第50層,碎了如果你再選擇二分,直接到25層又碎的話,兩個雞蛋就都沒了,接下來你咋試啊?
所以你接下來只能從1層到49層一個一個試了,最終嘗試次數為50次。
假設法
首先,假設答案,也就是最小嘗試次數為x,此時從第x層開始扔,有兩種情況:
- 碎了,那么只能從1到x-1一個一個試了,加上前面扔的一次,總結果為x次,符合,這也是為什么選擇第x層的原因,如果選擇其他層,又碎了的話,則最小嘗試次數肯定不等於x,這就與假設相悖了。
- 沒碎,那么直接把第1到x層拋棄掉,當作不存在(因為雞蛋不會在這范圍內碎掉),我們把第x+1層當成第1層,嘗試次數為x-1(因為剛剛扔了1次,最小嘗試次數減1),此時就從第x-1層(真實層數為x+x-1)開始扔,同樣又會出現兩種情況:
- 第二次碎了,則是從第1層到第x-2層開始扔 ,總嘗試次數同樣是x
- 第二次沒碎,還是之前原理,這次從x-2層開始,以此類推,一直扔到最后一層或碎了為止。
最終結果就是\(x+(x-1)+(x-2)...+1 = 100\),解得\(x = 14\)。
題目升級版本
樓層M,雞蛋數N,求最壞情況下的最小次數。
動態規划法
理解了上面的假設法,再學過動態規划的話,這里應該就問題不大了。
狀態轉移方程如下:
解釋:當有n個雞蛋時,所需嘗試的樓層數為m,此時將雞蛋扔在第k層,則有兩種情況
- 碎了,那么接下來只需要嘗試1到k-1層,雞蛋數為n-1,此時問題不就轉化成了樓層數k-1,雞蛋數n-1,求最壞情況下的最小次數嗎?
- 沒碎,那么直接把第1到k層拋棄掉,只需要嘗試第k+1到m層,雞蛋沒碎,所以扔為n,此時問題不就轉化成了樓層數m-k,雞蛋數n,求最壞情況下的最小次數嗎?
- 為什么取MAX?因為是最壞的情況,所以取碎了與沒碎中的最大情況。
代碼如下:
int superEggDrop(int egg,int floor){
int ans[floor+1][egg+1];
for(int m = 1;m <= floor;m++)
for(int n = 1;n <= egg;n++)
ans[m][n] = m;//最壞的情況下,自然是所有樓層試一遍,同時這也是雞蛋數為1時的答案
for(int m = 1;m <= floor;m++)
for(int n = 2;n <= egg;n++)//n必須從2開始,如果是1,就會出現ans[k-1][1-1=0],顯然不存在0雞蛋的情況
for(int k = 1;k <= m-1;k++)
ans[m][n] = min(ans[m][n],1+max(ans[k-1][n-1],ans[m-k][n]));
return ans[floor][egg];
}
然而,該解法的時間復雜度為\(O(km^2)\),空間復雜度為\(O(mn)\),顯然還可以繼續優化。
動態規划+二分優化
對於\(f[k-1][n-1]\)和\(f[m-k][n]\),當在第三重循環中,\(m,n\)不變,我們可以將其當作系數,只有\(k\)在\([1,m-1]\)的范圍內一直增加,而\(k\)又與樓層數有關,顯然,當樓層數增加時,測試次數一定增加(100層和101層,顯然100更有利吧?)。
而\(f[k-1][n-1]\)和\(f[m-k][n]\),前者\(k\)系數為正,后者\(k\)系數為負,一個遞增,一個遞減,我們就可以找二分它們的交點,使得無論碎不碎,它們的測試結果都相同,使得時間復雜度為\(O(kmlogm)\)。
int superEggDrop(int egg,int floor){
int ans[floor+1][egg+1];//雞蛋數只需要考慮兩種情況
for(int m = 1;m <= floor;m++)
for(int n = 1;n <= egg;n++)
ans[m][n] = m;//最壞的情況下,自然是所有樓層試一遍,同時這也是雞蛋數為1時的答案
for(int m = 2;m <= floor;m++)//當樓層數為1時,結果必然是1
for(int n = 2;n <= egg;n++){//n必須從2開始,如果是1,就會出現ans[k-1][1-1=0],顯然不存在0雞蛋的情況
int l = 1,r = m;//范圍是[l,r)
while(l+1 < r){
int k = (l+r)/2;
int l_value = ans[k-1][n-1];
int r_value = ans[m-k][n];
if (l_value == r_value){
l = k;
break;
}
else if (l_value > r_value) r = k;
else l = k+1;
}
ans[m][n] = min(ans[m][n],1+max(ans[l-1][n-1],ans[m-l][n]));
}
return ans[floor][egg];
}
當然了,現在還不是最優解,由於時間問題,這里就不再贅述,有興趣的可以自行百度。