一道老生常談有意思的面試題思考
題目
有一棟樓共N層,一個雞蛋從第M層及以上的樓層落下來會摔破, 在第M層以下的樓層落下不會摔破。給你Q個雞蛋,設計方案找出M,並且保證在最壞情況下, 最小化雞蛋下落的次數。
這道題目經常在面試中問到,很多部落格也給出了答案,但總感覺不全面,沒有講透徹,依據前人經驗和自己的理解,從思路和實現兩個方面進行思考,看一看採取哪一種演算法合適。
為了簡化問題,先假定有2個雞蛋,100層樓。
假設最壞情況下,至多仍k次,那第一次需要在第k層仍下,會有兩種情況:
- 碎了。這時只剩下一個雞蛋,只能從1層,一層層往上仍,最壞情況下仍到第k-1層,如果在k-1層碎了,那N=k-1,總共仍了k次,如果沒碎,那N=k,總共也仍了k次。
- 沒碎。這時手上還有2個雞蛋,從k+1層開始往下仍,還可以仍k-1次,1到k層,最多仍k次,k-1次最多仍k-1層,所以第二次在k+k-1層往下扔,如果第二次仍沒碎,第三次在k+k-1+k-2=3k-3層上仍,依此類推。 所以得出,2個雞蛋的時候,k次機會,最多可以從\(k+k-1+k-2+k-3+....+1 = \frac{k(k+1)} {2}\)層扔下,只要找到最小的k,使$\frac{k(k+1)} {2} >= 100 $,就找到了第一次扔的k層,容易得到k=14。 這樣就能保證在找到M時,扔的次數最多不超過14次。
第一種思路:
假設f[n][m]表示n個雞蛋,m層時,最壞情況下,至多扔的次數(f是一個二維陣列)。 \(f[2][100]=1+max(f[1][k-1],f[2][100-k];(k為第一次仍的樓層)\)
- 常數1表示第一次在k層仍下了一個雞蛋。
- f[1][k-1]表示當第一次在k層仍下第一個雞蛋時,碎了,還剩一個雞蛋,只能在k-1層樓範圍扔了。
- f[2][100-k]表示第一次在k層仍下第一個雞蛋時沒有碎,那麼還剩下2個雞蛋,100-k層樓。
如果有3個雞蛋,100層樓時,\(f[3][100]=1+max(f[2][k-1],f[3][100-k]);\) 可以類推得到\(f[n][m]=1+max(f[n-1][k-1],f[n][m-k])\)
第二種思路:
上面已經得到2個雞蛋,k次機會,最多可以測試k(k+1)/2層樓。 假如有3個雞蛋,k次機會,第一次測試碎了後,只剩下k-1次機會,必須要把剩下的樓層測試完。2個雞蛋,k-1機會,最多測試\(\frac{(k-1)k} {2}\)
總結: 用f(n,k)表示n個雞蛋,第一次在k層樓時,最多扔的樓層數(f是一個函式)。 \(f(1,k)=k;\) \(f(2,k)=f(1,k-1)+f(1,k-2)+....+f(1,0)+k;\) \(f(3,k)=f(2,k-1)+f(2,k-2)+f(2,k-3)+....+f(2,0)+k\) \(……\) \(……\) \(f(n,k)=f(n-1,k-1)+f(n-1,k-2)+....f(n-1,0)+k;\)
兩種思路總結
第一種思路是一種直接的方式,直接求解。
第二種思路是一種迂迴的方式,求n個雞蛋,k次最多能測試多少層。
編碼實現
自己對於java最熟悉,就使用java進行編碼
先給出兩種思路的實現程式碼,最後再解釋。程式碼中省略對樓層和雞蛋數量有效性的檢查。
第一種思路
這一種思路是大多數部落格常用的思路,解法也都是動態規劃,這裡仍然使用動態規劃。
- 動態規劃
int getFloor(int floorNum,int eggNum){
if(eggNum < 1 || floorNum < 1) return 0;
//f二維資料儲存著eggNum個雞蛋,從floorNum樓層扔下來最懷情況下,所需最多的次數
int[][] f = new int[eggNum+1][floorNum+1];
for(int i=1;i<=eggNum; i++){
for(int j=1; j<=floorNum; j++)
f[i][j] = j;//初始化,最壞的次數
}
for(int n=2; n<=eggNum; n++){
for(int m=1; m<=floorNum; m++){
for(int k=1; k<m; k++){
f[n][m] = Math.min(f[n][m],1+Math.max(f[n-1][k-1],f[n][m-k]));
}
}
}
return f[eggNum][floorNum];
}
第二種思路
這一種思路,考慮使用遞迴和動態規劃,動態規劃用了兩種方式實現。
- 遞迴(1)
/**
* 遞迴
* @param floorNum 樓層數
* @param eggNum 雞蛋數
* @return 在最懷情況下,雞蛋最多下落的次數
*/
int getFloor(int floorNum,int eggNum){
//從1層依次往上計算最大測試樓層
for(int i=1;i<=floorNum;i++){
if(maxFloor(eggNum,i)>=floorNum){
return i;
}
}
return 0;
}
/**
* eggNum雞蛋,k次嘗試最大能測試的樓層數
* @param eggNum 雞蛋數量
* @param k 嘗試次數
* @return 最大測試的樓層數
*/
int maxFloor(int eggNum,int k){
//f(1,k)=k
if (eggNum==1) return k ;
int result=0;
//計算f(eggNum,k)=f(eggNum-1,k-1)+f(eggNum-1,k-2)+....f(eggNum-1,0)+k
for(int i=0;i<k;i++){
result += maxFloor(eggNum-1,i);
}
result += k;
return result;
}
- 動態規劃(1)
/**
* 動態規劃
* @param floorNum 樓層數
* @param eggNum 雞蛋數
* @return 在最懷情況下,雞蛋最多下落的次數
*/
int getFloor(int floorNum,int eggNum){
int[][] f=new int[eggNum+1][floorNum+1];
for(int j=0;j<=floorNum;j++){
f[1][j]=j;
f[0][j]=0;
}
if (eggNum==1){
return floorNum;
}
for(int i=2;i<=eggNum;i++){
f[i][0]=0;
//從低層依次住上下落
for(int j=1;j<=floorNum;j++){
f[i][j]=0;
//計算f(eggNum,k)=f(eggNum-1,k-1)+f(eggNum-1,k-2)+....+f(eggNum-1,0)+k
for(int q=1;q<=j;q++){
f[i][j] += f[i-1][q-1];
}
f[i][j] +=j;//此處使用j,開始寫成了k
//比較第一次在j層落下時,最大測試的樓層數與總樓層數
if(f[i][j]>=floorNum){
//如果超過總樓層數且等於雞蛋數量,則返回,否則不必再計算
if(i==eggNum) {
return j;
}else{
break;
}
}
}
}
return 0;
}
- 動態規劃(2)
/**
*
* @param floorNum 樓層數
* @param eggNum 雞蛋數
* @return 最壞情況下,至多測試的次數
*/
int getFloor(int floorNum,int eggNum){
for(int i=1;i<=floorNum;i++){
if(f(eggNum,i)>=floorNum){
return i;
}
}
return 0;
}
/**
*
* @param eggNum 雞蛋數量
* @param k K次嘗試
* @return 最大測試的樓層數
*/
int f(int eggNum,int k){
int[][] f=new int[eggNum+1][k+1];
for(int j=0;j<=k;j++){
f[1][j]=j;
f[0][j]=0;
}
if (eggNum==1){
return f[1][k];
}
for(int i=2;i<=eggNum;i++){
f[i][0]=0;
for(int j=1;j<=k;j++){
f[i][j]=0;
//計算f(eggNum,k)=f(eggNum-1,k-1)+f(eggNum-1,k-2)+....+f(eggNum-1,0)+k
for(int q=1;q<=j;q++){
f[i][j] += f[i-1][q-1];
}
f[i][j] +=j;//此處使用j,開始寫成了k
}
}
return f[eggNum][k];
}
測試
- 3個雞蛋,100層樓
第二種思路-遞迴:第9層,耗時0ms
第二種思路-動態規劃1:第9層,耗時0ms
第二種思路-動態規劃2:第9層,耗時0ms
第一種思路-動態規劃:第9層,耗時1ms
- 10個雞蛋,10000層樓
第二種思路-遞迴:第14層,耗時0ms
第二種思路-動態規劃1:第14層,耗時1ms
第二種思路-動態規劃2:第14層,耗時0ms
第一種思路-動態規劃:第14層,耗時478ms
- 2個雞蛋,100000層樓
第二種思路-遞迴:第447層,耗時2ms
第二種思路-動態規劃1:第447層,耗時2ms
第二種思路-動態規劃2:第447層,耗時36ms
第一種思路-動態規劃:第447層,耗時5281ms
- 60雞蛋,10000000層樓
第二種思路-遞迴:第24層,耗時102ms
第二種思路-動態規劃1:第24層,耗時641ms
第二種思路-動態規劃2:第24層,耗時16ms
第一種思路執行中.....
可以看出,第一種思路實現方式執行是最慢的,因為需要從小到大(eggNum從2開始,floorNum從1開始)迴圈巢狀計算二維陣列第一項的值。而第二種思路動態規劃2,當得出的層數較矮時,優勢明顯,層數比較多時,就慢於第二種思路動態規劃1,因為動態規劃2,得到的結果樓層越矮時計算的越快,而動態規劃1也是巢狀迴圈計算,但只要計算到可測試最大樓層大於或等於總樓層就停止計算,比第一種思路的動態規劃要快。所以沒有哪一種演算法是最優的,需要根據資料量的多少來決定採取哪一種實現方法。