2019.11.27 LeetCode 從零單刷個人筆記整理(持續更新)
github:https://github.com/ChopinXBP/LeetCode-Babel
這是一道經典的智力題。由於有雞蛋摔碎的限定條件,因此不能依靠單純的二分查找來解決,可以從遞歸開始,不斷優化和升級方法。
1.遞歸(超時)
在手握K個雞蛋時從第i層樓[0,N]向下丟雞蛋,會有兩種可能:
1.雞蛋摔碎,將剩餘K-1個雞蛋移動到樓[0,i-1]進行測遞歸測試。
2.雞蛋沒碎,將剩餘K個雞蛋移動到樓[i,N]進行測試,可將i層看成0層,對[0,N-i]進行遞歸測試。
遞歸公式
result = min(max(superEggDrop(K - 1, i - 1), superEggDrop(K, N - i)) + 1)
2.動態規劃:二分搜索
觀察遞歸公式
result = min(max(superEggDrop(K - 1, i - 1), superEggDrop(K, N - i)) + 1)
其中i在[1.N]變化。
而fun1=superEggDrop(K - 1, i - 1)隨i單調增,fun2=superEggDrop(K, N - i)隨i單調減,兩條單調函數的交點即爲兩者最大值的最小取值點。
因此可以對i在[1,N]上進行二分查找。在二分查找過程中,可以設計一個哈希表避免重複計算。
3.動態規劃:自底向上
觀察轉爲動態規劃的遞歸公式
dp[i][j] = min(max(dp[i-1][loc-1], dp[i][j-loc]) + 1)
其中i在[1.j]變化,j在[1, N]變化。
而fun1=dp[i-1][loc-1]隨loc單調增,fun2=dp[i][j-loc]隨loc單調減,兩條單調函數的交點即爲兩者最大值的最小取值點。
尋找loc的思路轉換爲while循環遍歷查找。
令left爲loc-1時的函數值,right爲loc時的函數值,循環遍歷找到最小值的拐點。由於後序j值上限不斷增大,loc拐點值只可能逐漸向右移動,以此可以簡化後序查找。
3.動態規劃(超時):求能夠測得的最少樓層
dp[i][j]代表有i個雞蛋和j次移動時一定能夠測得的最少樓層數。沒有雞蛋時或沒有移動次數時無法測試dp=0;只有1個雞蛋但有j次移動,dp=j(從0層開始逐層測試)。
有i個雞蛋和j次移動時,前j-1次移動的測試結果可能由多種測試路徑得來,將其劃分爲k次和(j-1)-k次遍歷取最大。第j-1次移動雞蛋碎的結果爲dp[i-1][k],雞蛋未碎的結果爲dp[i][(j - 1) - k],當前樓層第j次移動的最少測試結果爲1(摔碎)。
dp[i][j] = max(dp[i - 1][k], dp[i][(j - 1) - k]) + 1
4.動態規劃(次優):求能夠測得的最多樓層
dp[i][j]代表i個雞蛋在j次移動內能測試確定的最多樓層數。
從第一層樓開始更新dp數組。0次移動無法進行測試,初始值dp[i][0]=0。
每多一次移動,可以多進行一次測試。在手中有i-1個雞蛋的第j-1次移動時已經測出樓高dp[i-1][j-1],一次移動最差情況也可以測出當前樓層的情況,因此逆推出當前能夠保證第j次移動得出結果的測試樓層爲dp[i-1][j-1]+1。
第j次移動雞蛋摔碎,可以確定當前層(1層)的結果(碎),剩餘i-1個雞蛋和j-1次移動能得出dp[i-1][j-1]層。
h1 = dp[i][j] = dp[i-1][j-1] + 1;
第j次移動雞蛋未碎,可以確定前dp[i-1][j-1]+1層的結果(不碎),剩餘i個雞蛋和j-1次移動能得出dp[i][j-1]層。
h2 = dp[i][j] = dp[i-1][j-1] + 1 + dp[i-1][j];
綜合結果:
dp = h2 >= max(h1, h2)
5.數學法(最優)
最差情況下(一個雞蛋)用N次移動可以測得N層樓高,因此可以移動次數x必在[1,N]之間,可以對x進行二分查找。
遞歸函數fun(x)代表x次移動能夠測試的最大樓高。
You are given K eggs, and you have access to a building with N floors from 1 to N.
Each egg is identical in function, and if an egg breaks, you cannot drop it again.
You know that there exists a floor F with 0 <= F <= N such that any egg dropped at a floor higher than F will break, and any egg dropped at or below floor F will not break.
Each move, you may take an egg (if you have an unbroken one) and drop it from any floor X (with 1 <= X <= N).
Your goal is to know with certainty what the value of F is.
What is the minimum number of moves that you need to know with certainty what F is, regardless of the initial value of F?
你將獲得 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
import java.util.HashMap;
/**
*
* You are given K eggs, and you have access to a building with N floors from 1 to N.
* Each egg is identical in function, and if an egg breaks, you cannot drop it again.
* You know that there exists a floor F with 0 <= F <= N such that any egg dropped at a floor higher than F will break,
* and any egg dropped at or below floor F will not break.
* Each move, you may take an egg (if you have an unbroken one) and drop it from any floor X (with 1 <= X <= N).
* Your goal is to know with certainty what the value of F is.
* What is the minimum number of moves that you need to know with certainty what F is, regardless of the initial value of F?
* 你將獲得 K 個雞蛋,並可以使用一棟從 1 到 N 共有 N 層樓的建築。
* 每個蛋的功能都是一樣的,如果一個蛋碎了,你就不能再把它掉下去。
* 你知道存在樓層 F ,滿足 0 <= F <= N 任何從高於 F 的樓層落下的雞蛋都會碎,從 F 樓層或比它低的樓層落下的雞蛋都不會破。
* 每次移動,你可以取一個雞蛋(如果你有完整的雞蛋)並把它從任一樓層 X 扔下(滿足 1 <= X <= N)。
* 你的目標是確切地知道 F 的值是多少。
* 無論 F 的初始值如何,你確定 F 的值的最小移動次數是多少?
*
*/
public class SuperEggDrop {
//遞歸(超時)
public int superEggDrop2(int K, int N) {
if(N < 2 || K == 1){
return N;
}
int result = N;
//在手握K個雞蛋時從第i層樓[0,N]向下丟雞蛋,會有兩種可能:
//1.雞蛋摔碎,將剩餘K-1個雞蛋移動到樓[0,i-1]進行測遞歸測試。
//2.雞蛋沒碎,將剩餘K個雞蛋移動到樓[i,N]進行測試,可將i層看成0層,對[0,N-i]進行遞歸測試。
for(int i = 1; i <= N; i++){
int curMin = Math.max(superEggDrop2(K - 1, i - 1), superEggDrop2(K, N - i)) + 1;
result = curMin < result ? curMin : result;
}
return result;
}
//動態規劃:二分搜索
//觀察遞歸公式result = min(max(superEggDrop(K - 1, i - 1), superEggDrop(K, N - i)) + 1),其中i在[1.N]變化
//而fun1=superEggDrop(K - 1, i - 1)隨i單調增,fun2=superEggDrop(K, N - i)隨i單調減,兩條單調函數的交點即爲兩者最大值的最小取值點
//因此可以對i在[1,N]上進行二分查找
HashMap<Integer, Integer> map = new HashMap<>();
public int superEggDrop3(int K, int N) {
if (N == 0){
return 0;
}
if (K == 1){
return N;
}
//因爲K<=100,可以設計一個key值的取值方法,將已經計算過的K,N組合存入哈希表避免重複計算
int key = N * 1000 + K;
if (map.containsKey(key)){
return map.get(key);
}
int begin = 1;
int end = N;
while (begin + 1 < end) {
int mid = (begin + end) >> 1;
int lowVal = superEggDrop(K - 1, mid - 1);
int highVal = superEggDrop(K, N - mid);
if (lowVal < highVal){
begin = mid;
}
else if (lowVal > highVal){
end = mid;
}
else{
end = mid;
begin = mid;
}
}
int minimum = 1 + Math.min(Math.max(superEggDrop(K - 1, begin - 1), superEggDrop(K, N - begin)),
Math.max(superEggDrop(K - 1, end - 1), superEggDrop(K, N - end)));
map.put(key, minimum);
return minimum;
}
//動態規劃:自底向上
//觀察轉爲動態規劃的遞歸公式dp[i][j] = min(max(dp[i-1][loc-1], dp[i][j-loc]) + 1),其中i在[1.j]變化,j在[1, N]變化
//而fun1=dp[i-1][loc-1]隨loc單調增,fun2=dp[i][j-loc]隨loc單調減,兩條單調函數的交點即爲兩者最大值的最小取值點
//尋找loc的思路轉換爲while循環遍歷查找
public int superEggDrop4(int K, int N) {
int[][] dp = new int[K + 1][N + 1];
for(int i = 1; i <= K; i++){
int loc = 1;
for(int j = 1; j <= N; j++){
if(i == 1){
dp[i][j] = j;
continue;
}
//令left爲loc-1時的函數值,right爲loc時的函數值,循環遍歷找到最小值的拐點
//由於後序j值上限不斷增大,loc拐點值只可能逐漸向右移動,以此可以簡化後序查找
while (loc < j){
int left = Math.max(dp[i - 1][loc - 1], dp[i][j - loc]);
int right = Math.max(dp[i - 1][loc], dp[i][j - loc - 1]);
if(left <= right){
break;
}
loc++;
}
dp[i][j] = Math.max(dp[i - 1][loc - 1], dp[i][j - loc]) + 1;
}
}
return dp[K][N];
}
//動態規劃(超時):求能夠測得的最少樓層
public int superEggDrop5(int K, int N) {
//dp[i][j]代表有i個雞蛋和j次移動時一定能夠測得的最少樓層數
int[][] dp = new int[K + 1][N + 1];
//沒有雞蛋時或沒有移動次數時無法測試dp=0;只有1個雞蛋但有j次移動,dp=j(從0層開始逐層測試)
for (int i = 1; i <= N; i++) {
dp[1][i] = i;
dp[0][i] = 0;
}
for (int i = 1; i <= K; i++) {
dp[i][0] = 0;
}
for (int i = 2; i <= K; i++) {
for (int j = 1; j <= N; j++) {
//有i個雞蛋和j次移動時,前j-1次移動的測試結果可能由多種測試路徑得來,將其劃分爲k次和(j-1)-k次遍歷取最大
//第j-1次移動雞蛋碎的結果爲dp[i-1][k],雞蛋未碎的結果爲dp[i][(j - 1) - k],當前樓層第j次移動的最少測試結果爲1(摔碎)
//dp[i][j] = max(dp[i - 1][k], dp[i][(j - 1) - k]) + 1
int tMinDrop = N * N;
for (int k = 0; k < j; k++) {
tMinDrop = Math.min(tMinDrop, Math.max(dp[i - 1][k], dp[i][(j - 1) - k]) + 1);
}
dp[i][j] = tMinDrop;
}
}
return dp[K][N];
}
//動態規劃(次優):求能夠測得的最多樓層
public int superEggDrop6(int K, int N) {
//dp[i][j]代表i個雞蛋在j次移動內能測試確定的最多樓層數
int[][] dp = new int[N + 1][K + 1];
//從第一層樓開始更新dp數組
for(int i = 1; i <= N; i++){
//0次移動無法進行測試,初始值dp[i][0]=0
dp[i][0] = 0;
for(int j = 1; j <= K; j++){
//每多一次移動,可以多進行一次測試。在手中有i-1個雞蛋的第j-1次移動時已經測出樓高dp[i-1][j-1],
//一次移動最差情況也可以測出當前樓層的情況,因此逆推出當前能夠保證第j次移動得出結果的測試樓層爲dp[i-1][j-1]+1
//第j次移動雞蛋摔碎,可以確定當前層(1層)的結果(碎),剩餘i-1個雞蛋和j-1次移動能得出dp[i-1][j-1]層
//h1 = dp[i][j] = dp[i-1][j-1] + 1;
//第j次移動雞蛋未碎,可以確定前dp[i-1][j-1]+1層的結果(不碎),剩餘i個雞蛋和j-1次移動能得出dp[i][j-1]層
//h2 = dp[i][j] = dp[i-1][j-1] + 1 + dp[i-1][j];
//dp = h2 >= max(h1, h2)
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - 1] + 1;
if(dp[i][j] >= N){
return i;
}
}
}
return N;
}
//數學(最優)
//最差情況下(一個雞蛋)用N次移動可以測得N層樓高,因此可以移動次數x必在[1,N]之間,可以對x進行二分查找
public int superEggDrop(int K, int N) {
int begin = 1;
int end = N;
while(begin < end){
int mid = (begin + end) >> 1;
if(fun(mid, K, N) < N){
begin = mid + 1;
}else {
end = mid;
}
}
return begin;
}
//遞歸函數fun(x)代表x次移動能夠測試的最大樓高
//https://leetcode-cn.com/problems/super-egg-drop/solution/ji-dan-diao-luo-by-leetcode/
//https://leetcode-cn.com/problems/super-egg-drop/solution/shu-xue-fa-jie-shi-by-lycao/
private int fun(int x, int k, int n){
int result = 0;
int r = 1;
for(int i = 1; i <= k; ++i){
r *= x - i + 1;
r /= i;
result += r;
if(result >= n){
break;
}
}
return result;
}
}
#Coding一小時,Copying一秒鐘。留個言點個讚唄,謝謝你#