題目
有一棟樓共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次機會,最多可以測試\(\frac{k(k+1)} {2}\)層樓。
假如有3個雞蛋,k次機會,第一次測試碎了后,只剩下k-1次機會,必須要把剩下的樓層測試完。2個雞蛋,k-1機會,最多測試\(\frac{(k-1)k} {2}\)層樓,所以第一次測試的樓層為\(\frac{k(k-1)} {2}+1\),如果第一次測試沒有碎,第二次增加\(\frac{(k-1)(k-2)} {2}+1\)層,所以三個雞蛋,k次機會,總共能夠測試的樓層為
總結
用\(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也是嵌套循環計算,但只要計算到可測試最大樓層大於或等於總樓層就停止計算,比第一種思路的動態規划要快。所以沒有哪一種算法是最優的,需要根據數據量的多少和算法具體的實現方式來決定采取哪一種實現方法。