1. 程式人生 > >一道老生常談有意思的面試題思考

一道老生常談有意思的面試題思考

題目

有一棟樓共N層,一個雞蛋從第M層及以上的樓層落下來會摔破, 在第M層以下的樓層落下不會摔破。給你Q個雞蛋,設計方案找出M,並且保證在最壞情況下, 最小化雞蛋下落的次數。

這道題目經常在面試中問到,很多部落格也給出了答案,但總感覺不全面,沒有講透徹,依據前人經驗和自己的理解,從思路和實現兩個方面進行思考,看一看採取哪一種演算法合適。

為了簡化問題,先假定有2個雞蛋,100層樓。

假設最壞情況下,至多仍k次,那第一次需要在第k層仍下,會有兩種情況:

  1. 碎了。這時只剩下一個雞蛋,只能從1層,一層層往上仍,最壞情況下仍到第k-1層,如果在k-1層碎了,那N=k-1,總共仍了k次,如果沒碎,那N=k,總共也仍了k次。
  2. 沒碎。這時手上還有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}\)

層樓,所以第一次測試的樓層為\(\frac{k(k-1)} {2}+1\),如果第一次測試沒有碎,第二次增加\(\frac{(k-1)(k-2)} {2}+1\)層,所以三個雞蛋,k次機會,總共能夠測試的樓層為\[\frac{k(k-1)} {2}+1+ \frac{(k-1)(k-2)} {2}+1+....+\frac{1*0} {2}+1 = \frac{k^3+5k} {6}\]

總結: 用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也是巢狀迴圈計算,但只要計算到可測試最大樓層大於或等於總樓層就停止計算,比第一種思路的動態規劃要快。所以沒有哪一種演算法是最優的,需要根據資料量的多少來決定採取哪一種實現方法。