【區間DP+二分優化】雞蛋掉落
【題目連結】
【題目描述】
給你 k 枚相同的雞蛋,並可以使用一棟從第 1 層到第 n 層共有 n 層樓的建築。
已知存在樓層 f ,滿足0 <= f <= n ,任何從 高於 f 的樓層落下的雞蛋都會碎,從 f 樓層或比它低的樓層落下的雞蛋都不會破。
每次操作,你可以取一枚沒有碎的雞蛋並把它從任一樓層 x 扔下(滿足1 <= x <= n)。如果雞蛋碎了,你就不能再次使用它。如果某枚雞蛋扔下後沒有摔碎,則可以在之後的操作中 重複使用 這枚雞蛋。
請你計算並返回要確定 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 <= 104
為了更好地理解題意,可以考慮從以下情況入手:
如果手上擁有無限個雞蛋,那麼對於本題來說,樓層是單調遞增的,故可以使用二分的方法得出樓層 f ,所需要的步數是 O(logn);
如果手上只擁有一個雞蛋,那麼我們就需要從一樓開始逐層往上嘗試才能得出樓層f,故所需要的步數是 O(n);
以上兩種情況的答案都是比較直觀的,接下來看一種答案不那麼直觀的情況:
假設我們的樓層共有 100 層,而我們的手上僅有兩個雞蛋時,怎麼做才能得出最少的步數呢?
一個比較直觀的想法就是首先每 10 層進行一次嘗試,當遇到某一個 10 的倍數的樓層的雞蛋破碎了,再從上一個 10 的倍數的樓層開始往上嘗試,直到找到樓層 f 為止,大致思想如下:
使用這種方法,我們可以算出最少需要的步數,也就是最壞情況下當 f = 99 時,共需要移動 19 步。
即10,20,30,40,50,60,70,80,90,100,91,92,93,94,95,96,97,98,99。
但其實仔細思考,我們就會發現這樣的步數分配是不那麼平均的,如果樓層在如 19,29,39 等這些位置的話,那麼第二步需要走的次數就遠大於第一步;
因此我們可以考慮採用不等間隔的方式來使得前後兩步所走的次數儘可能地相等。
我們可以採用以下方案:
在初始時讓第一步多跨越一些樓層,之後的跨度逐次減少,到最後只走一步,使得當第一步每多走一步,則第二步的所需要步數也會跟著減少。
通過這種方式,可以讓第一步和第二步所走的步數儘量相等。
那麼這個第一步最開始應當跨越多少樓層呢?設最開始應當跨越 n 層樓,那麼可得n + (n-1) + (n-2) + (n-3) + ... + 1 >= 100,即 n >= 13.6 ,取整可得 n = 14 ,也就是第一步的路徑應當是 (14,27,39,50,60,69,77,84,90,94,99,100) 。
這種策略的最壞情況下即是當 f = 13 時,第一步先走到14,之後第二步從1遍歷到13,共14步,比上面的方案好了不少。
大致理解題意後,繼續回到題目中來:
對於一個雞蛋在某一樓層$k$上丟下,所能得到的僅有兩種結果,就是碎與不碎。
如果雞蛋碎掉,答案樓層 $f$ 就應當在$1~k$中,如果雞蛋不碎,答案樓層 $f$ 就應當在 $k + 1 ~ n$ 中;
顯然,這是一個動態規劃的問題,狀態表示:$f[i,j]$表示共$i$層樓,手中擁有$j$個雞蛋,找出答案樓層$f$的最少步數。
那麼當一個雞蛋在第$k$層丟下,如果雞蛋碎掉,那麼答案就應當是$f[k - 1,j - 1] + 1$(雞蛋少了一個,且需要加上當前這一步的操作次數一次);
如果雞蛋沒有碎掉,那麼答案就應當是$f[i - k,j] + 1$(雞蛋數目不變,樓層轉化為共有$i - k$層,手中有$j$個雞蛋的最少步數)。
顯然,所要求的最小步數應當就是在第$k$層扔下雞蛋後,這兩種情況中的最大值的最小值,即$f[i,j] = min{f[i,j],max(f[k - 1,j - 1] , f[i - k,j]) + 1}$
得出了狀態轉移方程後,問題又來了,應當在哪一層丟下雞蛋才能獲得這個最小步數呢?一種簡單粗暴的思路就是列舉,列舉從$1~i$之間所有的樓層,從這些樓層中找到一個步數最小值。
算一算時間複雜度,樓層共$n$層,雞蛋共$k$個,還需要加上樓層的列舉$n$,故時間複雜度為O($kn^2$);
實現一下:
1 const int N = 10010,M = 110; 2 int f[N][M]; 3 class Solution { 4 public: 5 int superEggDrop(int k, int n) { 6 memset(f,0,sizeof f); 7 for(int i = 1;i <= n;++i) 8 for(int j = 1;j <= k;++j) 9 f[i][j] = i; 10 11 for(int i = 1;i <= n;++i) 12 for(int j = 2;j <= k;++j) 13 for(int k = 1;k < i;++k) 14 f[i][j] = min(f[i][j],max(f[k - 1][j - 1],f[i - k][j]) + 1); 15 return f[n][k]; 16 } 17 };
樣例測完直接提交,TLE了,果然1010過不了。
接下來考慮如何優化,樓層與雞蛋數目是無法優化的,唯一可以優化的就應當是樓層的枚舉了。
回顧一下轉移方程:$f[i,j] = min{f[i,j],max(f[k - 1,j - 1] , f[i - k,j]) + 1}$,我們希望在樓層$1~i$中找到一個樓層$k$,使得咱們能夠從$f[k - 1,j - 1] , f[i - k,j]$中選出一個最大值來更新$f[i,j]$,而為了能夠讓$f[i,j]$儘可能地小,這個值就應當儘可能地小。
那麼我們就來觀察一下這兩個式子$f[k - 1,j - 1] , f[i - k,j]$。
對於$f[x,y]$來說,$x$是樓層數目,設雞蛋數目$y$不變,那麼當樓層$x$越高,顯然我們所需要的步數只會與之前持平或者更多,因此可以發現$f[x,y]$在$y$不變的情況下,是隨著$x$非嚴格遞增的。
回到式子來,$f[k - 1,j - 1] , f[i - k,j]$,隨著$k$的增加,$f[k-1,j-1]$應當是遞增的,而$f[i - k,j]$則應當是遞減的,因此可以得出下面這張圖:
對於$p$點,顯然有$f[i-p,j] < f[p-1,j-1]$;
對於$q$點,顯然有$f[i-q,j] < f[q-1,j-1]$;
而$max(f[k - 1,j - 1] , f[i - k,j])$要找的正是點$t$,在點$t$處,能夠使得$max(f[k - 1,j - 1] , f[i - k,j])$儘可能地小,以便它能夠更新$f[i,j]$,但需要考慮的一點是樓層數是正整數,但點$t$並不一定是整數,不好直接求解;
可以考慮讓點$p$和點$q$儘可能地逼近$t$,使用二分的方法找到最大的點$p$,但需要滿足$f[i-p,j] <= f[p-1,j-1]$,當點$p$找到後,點$q$也自然而然地得出了,根據上述性質點$q$必然在點$p$的右側,而為了讓讓點$p$和點$q$儘可能地逼近$t$,
點$q$要麼恰好等於點$p$,要麼就在$p$的下一個位置,即$p+1=q$。
確定思路後,程式碼的實現比較簡單,使用二分的板子即可。時間複雜度也將為了O($knlogn$)。
1 const int N = 10010,M = 110; 2 int f[N][M]; 3 class Solution { 4 public: 5 int superEggDrop(int k, int n) { 6 memset(f,0,sizeof f); 7 for(int i = 1;i <= n;++i) 8 for(int j = 1;j <= k;++j) 9 f[i][j] = i; 10 11 for(int i = 1;i <= n;++i) 12 for(int j = 2;j <= k;++j) 13 { 14 // for(int k = 1;k < i;++k) 15 // f[i][j] = min(f[i][j],max(f[k - 1][j - 1],f[i - k][j]) + 1); 16 int l = 1,r = i; 17 while(l < r) 18 { 19 int mid = l + r + 1 >> 1; 20 if(f[mid - 1][j - 1] <= f[i - mid][j]) 21 l = mid; 22 else 23 r = mid - 1; 24 } 25 int t = max(f[l - 1][j - 1],f[i - l][j]); 26 if(l + 1 <= k) t = max(t,max(f[r - 1][j - 1],f[i - r][j])); 27 f[i][j] = min(f[i][j],t + 1); 28 } 29 return f[n][k]; 30 } 31 };