題目描述:
你將獲得 K
個雞蛋,並可以使用一棟從 1
到 N
共有 N
層樓的建築。
每個蛋的功能都是一樣的,如果一個蛋碎了,你就不能再把它掉下去。
你知道存在樓層 F
,滿足 0 <= F <= N
任何從高於 F
的樓層落下的雞蛋都會碎,從 F
樓層或比它低的樓層落下的雞蛋都不會破。
每次移動,你可以取一個雞蛋(如果你有完整的雞蛋)並把它從任一樓層 X
扔下(滿足 1 <= X <= N
)。
你的目標是確切地知道 F
的值是多少。
無論 F
的初始值如何,你確定 F
的值的最小移動次數是多少?
示例 1:
輸入:K = 1, N = 2 輸出:2 解釋: 雞蛋從 1 樓掉落。如果它碎了,我們肯定知道 F = 0 。 否則,雞蛋從 2 樓掉落。如果它碎了,我們肯定知道 F = 1 。 如果它沒碎,那么我們肯定知道 F = 2 。 因此,在最壞的情況下我們需要移動 2 次以確定 F 是多少。
示例 2:
輸入:K = 2, N = 6 輸出:3
示例 3:
輸入:K = 3, N = 14 輸出:4
提示:
1 <= K <= 100
1 <= N <= 10000
要完成的函數:
int superEggDrop(int K, int N)
說明:
1、這道題給定K個雞蛋和N層樓,想要測試雞蛋殼的保護能力,最高從第幾層樓摔下來雞蛋不會碎,而從高一層樓摔下來雞蛋就會碎。最高不會碎的這一層稱為第F層。
現在F層具體是哪一層不知道,而你有K個雞蛋,一共有N層樓,問需要嘗試多少次扔雞蛋才能知道F是多少。
如果雞蛋摔碎了就不能再用,如果雞蛋沒碎那么還可以繼續扔雞蛋下樓做測試。
雞蛋個數K在1到100的閉區間,樓層個數N在1到10000的閉區間,F是0到N的閉區間。
2、(下面的思路分享很長,但是思路很詳細,基本記錄了筆者從粗淺到最終解決方案的整個思考過程)
題意有點繞,舉個例子說明一下。
假設你只有1個雞蛋,2層樓,那么你只能從一樓開始扔,扔下去如果摔碎了,那么說明F是0,因為如果F是1的話,從一樓摔下去雞蛋不會碎的。此時只需要1次扔雞蛋。
如果從一樓摔下去不會碎,那么F是1或者2,那么就嘗試從二樓扔下來,如果碎了,那么F是1;如果沒碎,那么F是2。此時需要扔2次雞蛋。
所以在不知道F具體是多少的情況下,最小需要扔2次雞蛋才能確定F是多少。
有的同學可能會問,為什么只能從一樓開始?不能從二樓開始嗎?
二樓扔下去如果沒碎,那么還好,F肯定是2。
但是如果碎了呢?F有可能是1,也有可能是0,而你唯一的一個雞蛋碎了,測試失敗。
所以在只有1個雞蛋的情況下,只能老老實實從一樓開始測試,逐漸增加樓層高度。
如果有2個雞蛋,100層樓呢,最少的扔雞蛋次數是多少?
筆者最開始的想法是,可以先拿一個雞蛋做測試,盡可能減少測試空間。比如在50層拋下來,如果碎了那么測試空間就是[1,49];如果沒碎,那么測試空間是[51,100]。
這里解釋一下為什么碎了,測試空間就是[1,49];沒碎,測試空間就是[51,100]。
如果雞蛋碎了,那么只剩下一個雞蛋,F的取值范圍是[0,49],我們唯一的雞蛋只能從一樓開始扔。按照我們的經驗,1個雞蛋49層樓,那么一共需要扔49次才能確定F的具體值。
也就是說當F的取值范圍是[0,49]的時候,扔雞蛋的測試空間是[1,49]。
那么如果雞蛋沒碎,F的取值范圍是[50,100],那么扔雞蛋的測試空間就是[51,100],從51樓開始扔,一直到100樓扔,必定可以確定F的具體值。
筆者這種想法比較直接,但是扔雞蛋的次數不是最少的。
比如F是49,在50層扔下去,雞蛋碎了,只剩下一個雞蛋,要從1樓開始扔,扔49次到達49層,最終確定F的值。
一共需要扔1+49次。
第二個雞蛋扔49次也太憋屈了吧!第一個雞蛋只發揮了一次價值就碎了。
那我們讓兩個雞蛋發揮價值能發揮得均衡一點。
100層樓,開方,10,第一個雞蛋嘗試10次來確定測試區間,第二個雞蛋也只需要在確定的小區間中嘗試。
這種方法的最壞情況就是F為99,第一個雞蛋10層扔一下,20層扔一下,……100層扔一下,一共需要10次。確定了F的取值范圍是[90,99]。
那么第二個雞蛋從91開始嘗試,一直扔到99,一共需要9次。
所以采取這種均衡的方案,最壞情況只需要10+9=19次,比起二分的方法好多了。
但是這種均衡的方案也不是最佳的,同學們可以參考一下這篇文章:https://juejin.im/post/5b98785de51d450e71250aab
具體到這道題,我們不再是只有2個雞蛋和100層樓了,我們現在有K個雞蛋和N層樓,要求最少的扔雞蛋次數。
其實換個思路,每次扔雞蛋下樓,不就是碎和沒碎兩種結果嗎。
假設我們在X層扔,如果碎了,那么還有K-1個雞蛋,X-1層樓待驗證;如果沒碎,那么還有K個雞蛋,N-X層樓待驗證。這個時候我們已經扔了一次雞蛋了。
所以其實我們可以把問題轉化為兩個子問題,K個雞蛋N層樓最少的扔雞蛋次數=K-1個雞蛋X-1層樓最少的扔雞蛋次數+1,或者是=K個雞蛋N-X層樓最少的扔雞蛋次數+1。
這兩個子問題肯定有一個的扔雞蛋次數比較大,我們要取那個大的,所以列出式子就是
record[K][N] = max ( record[K-1][X-1] , record[K][N-X] ) + 1
X不確定要取多少,經過二分法和均衡法的比較,我們發揮二分的方法也不是最佳的……那要不X就從1到N都試一下吧!如果在某一層扔,可以求得record[K][N] = max ( record[K-1][X-1] , record[K][N-X] ) + 1是最小的,那么我們就能確定X在這一層扔是最佳方案。
所以式子最終可以寫成record[K][N] = min ( max ( record[K-1][X-1] , record[K][N-X] ) + 1 ) , 1<=X<=N
也就是說,這道題可以用分治法的思想來做,把一個問題分成兩個子問題,最終把兩個子問題的解匯總,得到原問題的解。
同時,由於record記錄的這些數值要多次使用,所以為了減少時間復雜度,我們就不用分治法,改用動態規划的查表法來做。
完美符合凌應標老師在課上多次強調的動態規划三個特點哈哈哈:
1、最優子結構性質——原問題的最優解可以由多個子問題的最優解得到
2、重復子問題(分治法與動態規划的最大區別)
3、子問題的個數有限
動態規划的代碼如下:
int superEggDrop(int K, int N) { vector<vector<int>>record(K+1,vector<int>(N+1,0));//需要從0個雞蛋或者0層樓開始算起,所以申請了K+1行N+1列的空間 for(int i=1;i<=K;i++) record[K][1]=1;//如果只有一層樓,那么無論多少個雞蛋都只需要扔一次雞蛋 for(int i=1;i<=N;i++) record[1][i]=i;//如果只有一個雞蛋,那么有多少層樓就需要扔多少次雞蛋 for(int i=2;i<=K;i++)//從兩個雞蛋兩層樓的情況開始算起 { for(int j=2;j<=N;j++) { int t=INT_MAX; for(int l=1;l<=j;l++) t=min(t,max(record[i-1][l-1],record[i][j-l])+1); record[i][j]=t;//記錄最小的扔雞蛋次數到record[i][j]中 } } return record[K][N];//最后返回record[K][N]就是我們要找的扔雞蛋次數了 }
上述代碼沒有通過所有的樣例測試……因為超時了……
可以看到三重循環,肯定耗時很多,比如當K=5,N=10000。
3、如何優化?hhh
同樣還是受上面分享的掘金那篇文章的啟發,我們換個角度來想這個問題。
如果給你K個雞蛋和M次嘗試摔雞蛋的次數,那么你最多可以測算出多高的樓層,無論F具體是在哪一層。
假設可以測算的最高樓層是N,那么題意也就是說給K個雞蛋和M次次數,必定可以測算得到N層樓的F的具體值。
那還是分治法的思路來看,在某一層X摔下去,如果碎了,那么只剩下M-1次次數和K-1個雞蛋,而用這剩下的M-1次次數和K-1個雞蛋,必定可以測算得到X-1層的F的具體值。
如果沒碎,那么還剩下M-1次次數和K個雞蛋,而用這M-1次次數和K個雞蛋,必定可以測算得到N-X的樓層的F的具體值。
我們用record[M][K]代表,用M次次數和K個雞蛋,最高能測算的樓層高度。
record[M-1][K-1]代表,用M-1次次數和K-1個雞蛋,最高能測算的樓層高度。
record[M-1][K]代表,用M-1次次數和K個雞蛋,最高能測算的樓層高度。
可以有record[M][K] = record[M-1][K-1] + record[M-1][K] + 1
+1是因為加上當前層的樓層高度。
這個式子理解得不是很清晰的話,同學們自己再琢磨一下,再回看3中的話。這個式子想要清晰理解需要費些思索。
可以結合record[2][2]的情況來理解,再試下record[3][2]的情況。
有了這個式子,我們能求M次次數和K個雞蛋的情況下,最高能測多少層。
但題目求的是層數確定了,雞蛋個數確定了,要求M的具體值。
其實一樣的,比如確定雞蛋個數K是3,樓層高度N是14。
假如只有一次嘗試次數,3個雞蛋,那么最高也就能測一層。達不到14的高度。
如果兩次嘗試次數,3個雞蛋,那么record[2][3] = record[1][2] + record[1][3] + 1。
record[1][2]和record[1][3]代表一次嘗試次數,那么最高只能測一層,所以上式結果是3。同樣達不到14的高度。
如果三次嘗試次數,3個雞蛋,那么record[3][3] = record[2][2] + record[2][3] + 1=3+3+1=7。同樣達不到14的高度。
如果四次嘗試次數,3個雞蛋,我們會發現record[4][3] = record[3][2] + record[3][3] + 1=6+7+1=14。剛好達到14的高度。
所以其實我們只需要不斷嘗試下去,最終嘗試第M次的時候,發現record[M][K]>=N,那么就可以了。
具體寫代碼的時候,發現我們沒辦法提前確定M的次數,所以沒辦法定義一個M行K列的vector來存儲數據。
但我們發現其實每次增大嘗試次數的時候,都是基於上一次嘗試的結果來求解。
所以我們可以只定義一個1行K列的vector,然后不斷地更新這一行vector的數值,直到在某次更新之后vector[K]>=N。
具體之后的代碼如下:
int superEggDrop(int K, int N) { vector<int>record(K+1,1);//包含0個雞蛋的情況,所以需要申請K+1個空間。只嘗試一次的時候,無論多少個雞蛋,最高都只能測1層 record[0]=0;//0個雞蛋的情況特殊化處理,為0 int move=2;//如果嘗試2次 while(record[K]<N)//當record[K]大於等於N的時候就退出循環 { for(int i=K;i>=1;i--)//從vector的后面開始更新,這樣不影響其他位置的vector元素的更新 record[i]=record[i]+record[i-1]+1; move++;//move+1,再嘗試一次 } return move-1;//返回需要的嘗試次數 }
可以說是非常簡潔了。題目十分復雜,但是想明白之后,寫一個二重循環就可以解決這道題目。
花了兩個小時寫出這篇思路比較詳細的文章,作為一個自我記錄,也希望可以給后來者些許啟發。
因為時間緊,排版和語言組織什么的可能不是很好,有哪里寫得不好歡迎同學們在評論區提出~