【重温经典】鸡蛋掉落
背景
- 这是一道经典的谷歌面试题,本文没有涉及「决策单调性」和「数学法」来解决本题
方法1:暴力递归
- 方法3的分析中,可以得出一个结论:需要不断的根据鸡蛋的碎与不碎来向下或者向上继续搜索,递归的最终则是要找到
base case
:- 当前楼层是0,则需要0次尝试
- 当前楼层是1,则需要1次尝试
- 当前楼层为n,但鸡蛋的个数为1,需要从1到n层一层一层往上试,尝试的次数即是楼层
public int superEggDrop(int K, int N) {
return helper(K, N);
}
private int helper(int k, int n) {
//楼层为0和1的时候
if (n == 0 || n == 1) return n;
//鸡蛋只有1个的时候
if (k == 1) return n;
int res = Integer.MAX_VALUE / 2;
for (int i = 1; i <= n; i++) {
//碎与不碎的情况
int t = Math.max(helper(k - 1, i - 1), helper(k, n - i)) + 1;
res = Math.min(res, t);
}
return res;
}
- 上面的普通的暴力递归很显然不能通过,考虑方法2采用记忆化的方式
方法2:自顶向下记忆化递归(Top-down)
Integer[][] memo;
public int superEggDrop(int K, int N) {
memo = new Integer[K + 1][N + 1];
return helper(K, N);
}
private int helper(int k, int n) {
if (n == 0 || n == 1) return n;
if (k == 1) return n;
if (memo[k][n] != null) return memo[k][n];
int res = Integer.MAX_VALUE / 2;
for (int i = 1; i <= n; i++) {
int t = Math.max(helper(k - 1, i - 1), helper(k, n - i)) + 1;
res = Math.min(res, t);
}
return memo[k][n] = res;
}
- 还是TLE了,不过有进步了,方法1过了34个用例,方法2过了74个,需要继续优化
优化
- 方法3中介绍了二分优化的方式查找楼层, O ( n ) O(n) O(n)的复杂度能退化到 O ( l o g N ) O(logN) O(logN)
Integer[][] memo;
public int superEggDrop(int K, int N) {
memo = new Integer[K + 1][N + 1];
return helper(K, N);
}
private int helper(int k, int n) {
if (n == 0 || n == 1) return n;
if (k == 1) return n;
if (memo[k][n] != null) return memo[k][n];
int lo = 1, hi = n, t = 0;
int res = Integer.MAX_VALUE / 2;
while (lo <= hi) {
int mid = lo + hi >> 1;
//两部分
int t1 = helper(k - 1, mid - 1);
int t2 = helper(k, n - mid);
t = Math.max(t1, t2) + 1;//去子问题最大
if (t1 < t2) {
lo = mid + 1;
} else {
hi = mid - 1;
}
res = Math.min(res, t);//更新res
}
return memo[k][n] = res;//记忆化
}
方法3:自底向上填表DP(Bottom-up)
定义状态
f [ i ] [ j ] f[i][j] f[i][j]表示还有 i i i层楼时并且当前持有的鸡蛋个数为 j j j个时,确定出临界楼层需要的最少操作数
- 注意这里,还有的 i i i层楼的时候,可以往上数,也可以往下数
状态转移
当在当前楼层 k k k扔下一个鸡蛋的时候,有两种结果:「broken」与「not broken」
- 鸡蛋没有碎:当前层 k k k以下都不会碎,因为当前层没有碎,往下的楼层不会碎,因此需要往上找,当前鸡蛋是完好的,往上找便是 f [ i − k ] [ j ] f[i-k][j] f[i−k][j]
- 鸡蛋碎了:当前层 k k k以上都是会碎的,因为楼层更高,当前 k k k层都碎了,我们要找的临界的楼层不在 k k k以上,因此需要往下找,而当前 k k k已经用掉了一个鸡蛋,往下找便是 f [ k − 1 ] [ j − 1 ] f[k-1][j-1] f[k−1][j−1]
在满足最坏的情况下,取上述两个讨论的最大值,而 k k k在1和 i i i之间,在这个整个区间中取最小值,没操作一次需要记录一次操作,执行+1操作
f [ i ] [ j ] = lim 1 ≤ k ≤ i m i n ( m a x ( f [ k − 1 ] [ j − 1 ] , f [ i − k ] [ j ] ) + 1 ) f[i][j]=\lim \limits_{1\leq k \leq i}min(max(f[k-1][j-1],f[i-k][j])+1) f[i][j]=1≤k≤ilimmin(max(f[k−1][j−1],f[i−k][j])+1)
初始化
-
f
[
N
+
1
]
[
K
+
1
]
f[N+1][K+1]
f[N+1][K+1] 其中
N
N
N为总的楼层,
K
为鸡蛋的个数-
f
[
0
]
[
j
]
f[0][j]
f[0][j]:当前层是0层,
j
个鸡蛋,值为0 - f [ i ] [ 0 ] f[i][0] f[i][0]:当前层从0到N,但没有鸡蛋,没有办法做测试,值为0
- f [ 1 ] [ j ] f[1][j] f[1][j]:当前层是1层,但是手里有j个鸡蛋, j ≥ 0 j\geq0 j≥0,只需要扔1次即可确定出结果
- f [ i ] [ 1 ] f[i][1] f[i][1]:当前层从0到N,只有一个鸡蛋,这种时候最好的方式是从0到N层每一层挨个试,值为楼层的高度
-
f
[
0
]
[
j
]
f[0][j]
f[0][j]:当前层是0层,
实现
int INF = Integer.MAX_VALUE / 2;
public int superEggDrop(int K, int N) {
int[][] f = new int[N + 1][K + 1];
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= K; j++) {
f[i][j] = INF;
}
}
f[0][0] = 0;//0层0个鸡蛋
for (int i = 1; i <= N; i++) f[i][1] = i;//1层以上1个鸡蛋
for (int j = 1; j <= K; j++) f[1][j] = 1;//1层超过1个鸡蛋
for (int i = 2; i <= N; i++) {
for (int j = 2; j <= K; j++) {
for (int k = 1; k <= i; k++) {
//一般情况
f[i][j] = Math.min(f[i][j], Math.max(f[k - 1][j - 1], f[i - k][j]) + 1);
}
}
}
return f[N][K];//返回N层K个鸡蛋的结果
}
- 上述实现TLE了,证明思路是对的,开始优化
优化
-
上述的做法,时间复杂度是 O ( N 2 ∗ K ) O(N^2*K) O(N2∗K),对于每个楼层来说,需要 O ( N ) O(N) O(N)的时间,是线性的,需要优化。
-
f [ k ] [ j ] f[k][j] f[k][j] 是一个关于
k
的单调递增函数,也就是说在鸡蛋数j
固定的情况下,楼层数k
越多,需要的操作数会越来越多。
重新再审视一遍状态转移方程
f [ i ] [ j ] = lim 1 ≤ k ≤ i m i n ( m a x ( f [ k − 1 ] [ j − 1 ] , f [ i − k ] [ j ] ) + 1 ) f[i][j]=\lim \limits_{1\leq k \leq i}min(max(f[k-1][j-1],f[i-k][j])+1) f[i][j]=1≤k≤ilimmin(max(f[k−1][j−1],f[i−k][j])+1)
- 在上述的状态转移方程中,第一项
T
1
(
x
)
=
f
[
x
−
1
]
[
j
−
1
]
T_1(x) = f[x-1][j-1]
T1(x)=f[x−1][j−1]是一个随
x
增加而单调递增函数,第二项 T 2 ( x ) = f [ i − x ] [ j ] T_2(x) = f[i-x][j] T2(x)=f[i−x][j]
是一个随x
增加而单调递减函数
简言之,需要找到两项在交叉处的点,筛选掉不要的
实现
int INF = Integer.MAX_VALUE / 2;
public int superEggDrop(int K, int N) {
int[][] f = new int[N + 1][K + 1];
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= K; j++) {
f[i][j] = INF;
}
}
f[1][0] = 0;
for (int i = 1; i <= N; i++) f[i][1] = i;
for (int j = 1; j <= K; j++) f[1][j] = 1;
for (int i = 2; i <= N; i++) {
for (int j = 2; j <= K; j++) {
int l = 1, r = i;
while (l < r) {
int m = l + (r - l + 1) / 2;
//二分,比较t1部分和t2部分
int t1 = f[m - 1][j - 1], t2 = f[i - m][j];
if (t1 > t2) {
r = m - 1;
} else {
l = m;
}
}
f[i][j] = Math.min(f[i][j], Math.max(f[l - 1][j - 1], f[i - l][j]) + 1);
}
}
return f[N][K];
}
FollowUp
- 利用上面的结论就很容易解决下面的这个问题了「双蛋掉落」
public int twoEggDrop(int n) {
return superEggDrop(2,n);
}